difftreelog
feat(collator-selection) add+remove invulnerable methods + tests + miscellaneous changes before more refactoring
in: master
11 files changed
node/cli/src/chain_spec.rsdiffbeforeafterboth--- a/node/cli/src/chain_spec.rs
+++ b/node/cli/src/chain_spec.rs
@@ -24,7 +24,7 @@
use serde_json::map::Map;
use up_common::types::opaque::*;
-use up_common::constants::GENESIS_CANDIDACY_BOND;
+use up_common::constants::{GENESIS_CANDIDACY_BOND, SESSION_LENGTH};
#[cfg(feature = "unique-runtime")]
pub use unique_runtime as default_runtime;
@@ -197,6 +197,7 @@
.map(|(acc, _)| acc)
.collect(),
candidacy_bond: GENESIS_CANDIDACY_BOND,
+ kick_threshold: SESSION_LENGTH,
..Default::default()
},
session: SessionConfig {
pallets/collator-selection/src/benchmarking.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/benchmarking.rs
+++ b/pallets/collator-selection/src/benchmarking.rs
@@ -112,8 +112,13 @@
}
fn register_candidates<T: Config>(count: u32) {
- let candidates = (0..count).map(|c| account("candidate", c, SEED)).collect::<Vec<_>>();
- assert!(<CandidacyBond<T>>::get() > 0u32.into(), "Bond cannot be zero!");
+ let candidates = (0..count)
+ .map(|c| account("candidate", c, SEED))
+ .collect::<Vec<_>>();
+ assert!(
+ <CandidacyBond<T>>::get() > 0u32.into(),
+ "Bond cannot be zero!"
+ );
for who in candidates {
T::Currency::make_free_balance_be(&who, <CandidacyBond<T>>::get() * 2u32.into());
@@ -200,7 +205,8 @@
whitelist!(leaving);
}: _(RawOrigin::Signed(leaving.clone()))
verify {
- assert_last_event::<T>(Event::CandidateRemoved{account_id: leaving}.into());
+ // todo:collator verify these
+ assert_last_event::<T>(Event::CandidateRemoved{account_id: leaving, deposit_returned: bond / 2u32.into() }.into());
}
// worse case is paying a non-existing candidate account.
@@ -272,4 +278,8 @@
}
}
-impl_benchmark_test_suite!(CollatorSelection, crate::mock::new_test_ext(), crate::mock::Test,);
+impl_benchmark_test_suite!(
+ CollatorSelection,
+ crate::mock::new_test_ext(),
+ crate::mock::Test,
+);
pallets/collator-selection/src/lib.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/lib.rs
+++ b/pallets/collator-selection/src/lib.rs
@@ -30,6 +30,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+// todo:collator documentation
//! Collator Selection pallet.
//!
//! A pallet to manage collators in a parachain.
@@ -109,7 +110,10 @@
};
use frame_system::{pallet_prelude::*, Config as SystemConfig};
use pallet_session::SessionManager;
- use sp_runtime::traits::Convert;
+ use sp_runtime::{
+ Perbill,
+ traits::{One, Convert},
+ };
use sp_staking::SessionIndex;
type BalanceOf<T> =
@@ -136,6 +140,9 @@
/// Origin that can dictate updating parameters of this pallet.
type UpdateOrigin: EnsureOrigin<Self::RuntimeOrigin>;
+ /// Account Identifier that holds the chain's treasury.
+ type TreasuryAccountId: Get<Self::AccountId>;
+
/// Account Identifier from which the internal Pot is generated.
type PotId: Get<PalletId>;
@@ -152,8 +159,8 @@
/// Maximum number of invulnerables. This is enforced in code.
type MaxInvulnerables: Get<u32>;
- // Will be kicked if block is not produced in threshold.
- type KickThreshold: Get<Self::BlockNumber>;
+ /// If kicked, how much of the collator's deposit will be slashed and sent to the slash destination.
+ type SlashRatio: Get<Perbill>;
/// A stable ID for a validator.
type ValidatorId: Member + Parameter;
@@ -200,6 +207,13 @@
ValueQuery,
>;
+ /// Collator will be kicked if it does not produce a block within the threshold (does not apply to invulnerables).
+ ///
+ /// Should be a multiple of session or things will get inconsistent. todo:collator reword?
+ #[pallet::storage]
+ #[pallet::getter(fn kick_threshold)]
+ pub type KickThreshold<T: Config> = StorageValue<_, T::BlockNumber, ValueQuery>;
+
/// Last block authored by collator.
#[pallet::storage]
#[pallet::getter(fn last_authored_block)]
@@ -224,6 +238,7 @@
pub struct GenesisConfig<T: Config> {
pub invulnerables: Vec<T::AccountId>,
pub candidacy_bond: BalanceOf<T>,
+ pub kick_threshold: T::BlockNumber,
pub desired_candidates: u32,
}
@@ -233,6 +248,7 @@
Self {
invulnerables: Default::default(),
candidacy_bond: Default::default(),
+ kick_threshold: T::BlockNumber::one(),
desired_candidates: Default::default(),
}
}
@@ -241,8 +257,10 @@
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
- let duplicate_invulnerables =
- self.invulnerables.iter().collect::<std::collections::BTreeSet<_>>();
+ let duplicate_invulnerables = self
+ .invulnerables
+ .iter()
+ .collect::<std::collections::BTreeSet<_>>();
assert!(
duplicate_invulnerables.len() == self.invulnerables.len(),
"duplicate invulnerables in genesis."
@@ -258,6 +276,7 @@
<DesiredCandidates<T>>::put(&self.desired_candidates);
<CandidacyBond<T>>::put(&self.candidacy_bond);
+ <KickThreshold<T>>::put(&self.kick_threshold);
<Invulnerables<T>>::put(bounded_invulnerables);
}
}
@@ -265,11 +284,29 @@
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
- NewInvulnerables { invulnerables: Vec<T::AccountId> },
- NewDesiredCandidates { desired_candidates: u32 },
- NewCandidacyBond { bond_amount: BalanceOf<T> },
- CandidateAdded { account_id: T::AccountId, deposit: BalanceOf<T> },
- CandidateRemoved { account_id: T::AccountId },
+ NewDesiredCandidates {
+ desired_candidates: u32,
+ },
+ NewCandidacyBond {
+ bond_amount: BalanceOf<T>,
+ },
+ NewKickThreshold {
+ length_in_blocks: T::BlockNumber,
+ },
+ InvulnerableAdded {
+ invulnerable: T::AccountId,
+ },
+ InvulnerableRemoved {
+ invulnerable: T::AccountId,
+ },
+ CandidateAdded {
+ account_id: T::AccountId,
+ deposit: BalanceOf<T>,
+ },
+ CandidateRemoved {
+ account_id: T::AccountId,
+ deposit_returned: BalanceOf<T>,
+ },
}
// Errors inform users that something went wrong.
@@ -289,8 +326,12 @@
NotCandidate,
/// Too many invulnerables
TooManyInvulnerables,
+ /// Too few invulnerables
+ TooFewInvulnerables,
/// User is already an Invulnerable
AlreadyInvulnerable,
+ /// User is not an Invulnerable
+ NotInvulnerable,
/// Account has no associated validator ID
NoAssociatedValidatorId,
/// Validator ID is not yet registered
@@ -302,33 +343,61 @@
#[pallet::call]
impl<T: Config> Pallet<T> {
- /// Set the list of invulnerable (fixed) collators.
- #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]
- pub fn set_invulnerables(
+ /// Add a collator to the list of invulnerable (fixed) collators.
+ #[pallet::weight(T::WeightInfo::set_invulnerables(1 as u32))] // todo:collator weight
+ pub fn add_invulnerable(
origin: OriginFor<T>,
- new: Vec<T::AccountId>,
+ new: T::AccountId,
) -> DispatchResultWithPostInfo {
T::UpdateOrigin::ensure_origin(origin)?;
- let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
- .map_err(|_| Error::<T>::TooManyInvulnerables)?;
- // check if the invulnerables have associated validator keys before they are set
- for account_id in bounded_invulnerables.iter() {
- let validator_key = T::ValidatorIdOf::convert(account_id.clone())
- .ok_or(Error::<T>::NoAssociatedValidatorId)?;
- ensure!(
- T::ValidatorRegistration::is_registered(&validator_key),
- Error::<T>::ValidatorNotRegistered
- );
+ // check if the new invulnerable has associated validator keys before it is added
+ let validator_key = T::ValidatorIdOf::convert(new.clone())
+ .ok_or(Error::<T>::NoAssociatedValidatorId)?;
+ ensure!(
+ T::ValidatorRegistration::is_registered(&validator_key),
+ Error::<T>::ValidatorNotRegistered
+ );
+ // ensure!(!Self::invulnerables().contains(&new), Error::<T>::AlreadyInvulnerable);
+ if Self::invulnerables().contains(&new) {
+ return Ok(().into());
}
- <Invulnerables<T>>::put(&bounded_invulnerables);
- Self::deposit_event(Event::NewInvulnerables {
- invulnerables: bounded_invulnerables.to_vec(),
- });
+ <Invulnerables<T>>::try_append(new.clone())
+ .map_err(|_| Error::<T>::TooManyInvulnerables)?;
+ Self::deposit_event(Event::InvulnerableAdded { invulnerable: new });
Ok(().into())
}
+ /// Remove a collator from the list of invulnerable (fixed) collators.
+ #[pallet::weight(T::WeightInfo::set_invulnerables(1))] // todo:collator weight
+ pub fn remove_invulnerable(
+ origin: OriginFor<T>,
+ who: T::AccountId,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+
+ // let index = Self::invulnerables().into_iter().position(|r| r == who).ok_or(Error::<T>::NotInvulnerable)?;
+ <Invulnerables<T>>::try_mutate(|invulnerables| -> DispatchResult {
+ if invulnerables.len() <= 1 {
+ return Err(Error::<T>::TooFewInvulnerables.into());
+ }
+
+ let index = invulnerables
+ .into_iter()
+ .position(|r| *r == who)
+ .ok_or(Error::<T>::NotInvulnerable)?;
+ invulnerables.remove(index);
+ Ok(())
+ })?;
+ /*let bounded_invulnerables = BoundedVec::<_, T::MaxInvulnerables>::try_from(new)
+ .map_err(|_| Error::<T>::TooManyInvulnerables)?;
+
+ <Invulnerables<T>>::put(&bounded_invulnerables);*/
+ Self::deposit_event(Event::InvulnerableRemoved { invulnerable: who });
+ Ok(().into())
+ }
+
/// Set the ideal number of collators (not including the invulnerables).
/// If lowering this number, then the number of running collators could be higher than this figure.
/// Aside from that edge case, there should be no other way to have more collators than the desired number.
@@ -343,7 +412,9 @@
log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");
}
<DesiredCandidates<T>>::put(&max);
- Self::deposit_event(Event::NewDesiredCandidates { desired_candidates: max });
+ Self::deposit_event(Event::NewDesiredCandidates {
+ desired_candidates: max,
+ });
Ok(().into())
}
@@ -359,6 +430,22 @@
Ok(().into())
}
+ /// Set the length of the kick threshold.
+ /// Note that if the length is not a multiple of the session period, it might get inconsistent.
+ #[pallet::weight(T::WeightInfo::set_candidacy_bond())] // todo:collator weight
+ pub fn set_kick_threshold(
+ origin: OriginFor<T>,
+ kick_threshold: T::BlockNumber,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ // todo:collator insert something to guarantee consistency?
+ <KickThreshold<T>>::put(kick_threshold);
+ Self::deposit_event(Event::NewKickThreshold {
+ length_in_blocks: kick_threshold,
+ });
+ Ok(().into())
+ }
+
/// Register this account as a collator candidate. The account must (a) already have
/// registered session keys and (b) be able to reserve the `CandidacyBond`.
///
@@ -369,8 +456,15 @@
// ensure we are below limit.
let length = <Candidates<T>>::decode_len().unwrap_or_default();
- ensure!((length as u32) < Self::desired_candidates(), Error::<T>::TooManyCandidates);
- ensure!(!Self::invulnerables().contains(&who), Error::<T>::AlreadyInvulnerable);
+ ensure!(
+ (length as u32) < Self::desired_candidates(),
+ Error::<T>::TooManyCandidates
+ );
+ // todo:collator really need it?
+ ensure!(
+ !Self::invulnerables().contains(&who),
+ Error::<T>::AlreadyInvulnerable
+ );
let validator_key = T::ValidatorIdOf::convert(who.clone())
.ok_or(Error::<T>::NoAssociatedValidatorId)?;
@@ -381,7 +475,10 @@
let deposit = Self::candidacy_bond();
// First authored block is current block plus kick threshold to handle session delay
- let incoming = CandidateInfo { who: who.clone(), deposit };
+ let incoming = CandidateInfo {
+ who: who.clone(),
+ deposit,
+ };
let current_count =
<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {
@@ -389,16 +486,21 @@
Err(Error::<T>::AlreadyCandidate)?
} else {
T::Currency::reserve(&who, deposit)?;
- candidates.try_push(incoming).map_err(|_| Error::<T>::TooManyCandidates)?;
+ candidates
+ .try_push(incoming)
+ .map_err(|_| Error::<T>::TooManyCandidates)?;
<LastAuthoredBlock<T>>::insert(
who.clone(),
- frame_system::Pallet::<T>::block_number() + T::KickThreshold::get(),
+ frame_system::Pallet::<T>::block_number() + Self::kick_threshold(),
);
Ok(candidates.len())
}
})?;
- Self::deposit_event(Event::CandidateAdded { account_id: who, deposit });
+ Self::deposit_event(Event::CandidateAdded {
+ account_id: who,
+ deposit,
+ });
Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())
}
@@ -411,11 +513,12 @@
#[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
pub fn leave_intent(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
+ // todo:collator invulnerables and candidates should count against min candidates together
ensure!(
Self::candidates().len() as u32 > T::MinCandidates::get(),
Error::<T>::TooFewCandidates
);
- let current_count = Self::try_remove_candidate(&who)?;
+ let current_count = Self::try_remove_candidate(&who, false)?;
Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
}
@@ -427,8 +530,12 @@
T::PotId::get().into_account_truncating()
}
- /// Removes a candidate if they exist and sends them back their deposit
- fn try_remove_candidate(who: &T::AccountId) -> Result<usize, DispatchError> {
+ /// Removes a candidate if they exist and sends them back their deposit, optionally slashed.
+ fn try_remove_candidate(
+ who: &T::AccountId,
+ should_slash: bool,
+ ) -> Result<usize, DispatchError> {
+ let mut deposit_returned = BalanceOf::<T>::default();
let current_count =
<Candidates<T>>::try_mutate(|candidates| -> Result<usize, DispatchError> {
let index = candidates
@@ -436,11 +543,33 @@
.position(|candidate| candidate.who == *who)
.ok_or(Error::<T>::NotCandidate)?;
let candidate = candidates.remove(index);
- T::Currency::unreserve(who, candidate.deposit);
+ let deposit = candidate.deposit;
+
+ if should_slash {
+ let slashed = T::SlashRatio::get() * deposit;
+ let remaining = deposit - slashed;
+
+ let (imbalance, _) = T::Currency::slash_reserved(who, slashed);
+ //T::Currency::unreserve(who, remaining);
+ deposit_returned = remaining;
+
+ T::Currency::resolve_creating(&T::TreasuryAccountId::get(), imbalance);
+
+ // Self::deposit_event(Event::CandidateSlashed(who.clone()));
+ } else {
+ //T::Currency::unreserve(who, deposit);
+ deposit_returned = deposit;
+ }
+
+ T::Currency::unreserve(who, deposit_returned);
+ // candidates.remove(index);
<LastAuthoredBlock<T>>::remove(who.clone());
Ok(candidates.len())
})?;
- Self::deposit_event(Event::CandidateRemoved { account_id: who.clone() });
+ Self::deposit_event(Event::CandidateRemoved {
+ account_id: who.clone(),
+ deposit_returned,
+ });
Ok(current_count)
}
@@ -456,12 +585,12 @@
}
/// Kicks out candidates that did not produce a block in the kick threshold
- /// and refund their deposits.
+ /// and **confiscates** their deposits to the treasury.
pub fn kick_stale_candidates(
candidates: BoundedVec<CandidateInfo<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,
) -> BoundedVec<T::AccountId, T::MaxCandidates> {
let now = frame_system::Pallet::<T>::block_number();
- let kick_threshold = T::KickThreshold::get();
+ let kick_threshold = Self::kick_threshold();
candidates
.into_iter()
.filter_map(|c| {
@@ -472,7 +601,7 @@
{
Some(c.who)
} else {
- let outcome = Self::try_remove_candidate(&c.who);
+ let outcome = Self::try_remove_candidate(&c.who, true);
if let Err(why) = outcome {
log::warn!("Failed to remove candidate {:?}", why);
debug_assert!(false, "failed to remove candidate {:?}", why);
pallets/collator-selection/src/mock.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/mock.rs
+++ b/pallets/collator-selection/src/mock.rs
@@ -43,7 +43,7 @@
use sp_runtime::{
testing::{Header, UintAuthorityId},
traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
- RuntimeAppPublic,
+ Perbill, RuntimeAppPublic,
};
type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
@@ -210,6 +210,7 @@
pub const MaxInvulnerables: u32 = 20;
pub const MinCandidates: u32 = 1;
pub const MaxAuthorities: u32 = 100_000;
+ pub const SlashRatio: Perbill = Perbill::one();
}
pub struct IsRegistered;
@@ -224,6 +225,7 @@
}
impl Config for Test {
+ // todo:collator mocks and stocks
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type UpdateOrigin = EnsureSignedBy<RootAccount, u64>;
@@ -231,7 +233,9 @@
type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables;
- type KickThreshold = Period;
+ // type KickThreshold = Period;
+ type SlashRatio = SlashRatio;
+ type TreasuryAccountId = ();
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = IdentityCollator;
type ValidatorRegistration = IsRegistered;
@@ -240,17 +244,28 @@
pub fn new_test_ext() -> sp_io::TestExternalities {
sp_tracing::try_init_simple();
- let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
+ let mut t = frame_system::GenesisConfig::default()
+ .build_storage::<Test>()
+ .unwrap();
let invulnerables = vec![1, 2];
let balances = vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)];
let keys = balances
.iter()
- .map(|&(i, _)| (i, i, MockSessionKeys { aura: UintAuthorityId(i) }))
+ .map(|&(i, _)| {
+ (
+ i,
+ i,
+ MockSessionKeys {
+ aura: UintAuthorityId(i),
+ },
+ )
+ })
.collect::<Vec<_>>();
let collator_selection = collator_selection::GenesisConfig::<Test> {
desired_candidates: 2,
candidacy_bond: 10,
+ kick_threshold: 1,
invulnerables,
};
let session = pallet_session::GenesisConfig::<Test> { keys };
pallets/collator-selection/src/tests.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/tests.rs
+++ b/pallets/collator-selection/src/tests.rs
@@ -113,7 +113,10 @@
assert_eq!(CollatorSelection::candidacy_bond(), 7);
// rejects bad origin.
- assert_noop!(CollatorSelection::set_candidacy_bond(RuntimeOrigin::signed(1), 8), BadOrigin);
+ assert_noop!(
+ CollatorSelection::set_candidacy_bond(RuntimeOrigin::signed(1), 8),
+ BadOrigin
+ );
});
}
@@ -131,7 +134,9 @@
// reset desired candidates:
<crate::DesiredCandidates<Test>>::put(1);
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
// but no more
assert_noop!(
@@ -146,7 +151,9 @@
new_test_ext().execute_with(|| {
// reset desired candidates:
<crate::DesiredCandidates<Test>>::put(1);
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
// can not remove too few
assert_noop!(
@@ -184,8 +191,13 @@
fn cannot_register_dupe_candidate() {
new_test_ext().execute_with(|| {
// can add 3 as candidate
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
- let addition = CandidateInfo { who: 3, deposit: 10 };
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
+ let addition = CandidateInfo {
+ who: 3,
+ deposit: 10,
+ };
assert_eq!(CollatorSelection::candidates(), vec![addition]);
assert_eq!(CollatorSelection::last_authored_block(3), 10);
assert_eq!(Balances::free_balance(3), 90);
@@ -205,7 +217,9 @@
assert_eq!(Balances::free_balance(&33), 0);
// works
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
// poor
assert_noop!(
@@ -228,8 +242,12 @@
assert_eq!(Balances::free_balance(&3), 100);
assert_eq!(Balances::free_balance(&4), 100);
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
assert_eq!(Balances::free_balance(&3), 90);
assert_eq!(Balances::free_balance(&4), 90);
@@ -242,11 +260,15 @@
fn leave_intent() {
new_test_ext().execute_with(|| {
// register a candidate.
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
assert_eq!(Balances::free_balance(3), 90);
// register too so can leave above min candidates
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(5)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(5)
+ ));
assert_eq!(Balances::free_balance(5), 90);
// cannot leave if not candidate.
@@ -270,11 +292,16 @@
// 4 is the default author.
assert_eq!(Balances::free_balance(4), 100);
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
// triggers `note_author`
Authorship::on_initialize(1);
- let collator = CandidateInfo { who: 4, deposit: 10 };
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
assert_eq!(CollatorSelection::candidates(), vec![collator]);
assert_eq!(CollatorSelection::last_authored_block(4), 0);
@@ -295,11 +322,16 @@
Balances::make_free_balance_be(&CollatorSelection::account_id(), 5);
// 4 is the default author.
assert_eq!(Balances::free_balance(4), 100);
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
// triggers `note_author`
Authorship::on_initialize(1);
- let collator = CandidateInfo { who: 4, deposit: 10 };
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
assert_eq!(CollatorSelection::candidates(), vec![collator]);
assert_eq!(CollatorSelection::last_authored_block(4), 0);
@@ -324,7 +356,9 @@
assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
// add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
// session won't see this.
assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
@@ -351,8 +385,12 @@
fn kick_mechanism() {
new_test_ext().execute_with(|| {
// add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(4)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(4)
+ ));
initialize_to_block(10);
assert_eq!(CollatorSelection::candidates().len(), 2);
initialize_to_block(20);
@@ -361,7 +399,10 @@
assert_eq!(CollatorSelection::candidates().len(), 1);
// 3 will be kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]);
- let collator = CandidateInfo { who: 4, deposit: 10 };
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
assert_eq!(CollatorSelection::candidates(), vec![collator]);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
initialize_to_block(30);
@@ -376,8 +417,12 @@
fn should_not_kick_mechanism_too_few() {
new_test_ext().execute_with(|| {
// add a new collator
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(3)));
- assert_ok!(CollatorSelection::register_as_candidate(RuntimeOrigin::signed(5)));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(3)
+ ));
+ assert_ok!(CollatorSelection::register_as_candidate(
+ RuntimeOrigin::signed(5)
+ ));
initialize_to_block(10);
assert_eq!(CollatorSelection::candidates().len(), 2);
initialize_to_block(20);
@@ -386,7 +431,10 @@
assert_eq!(CollatorSelection::candidates().len(), 1);
// 3 will be kicked after 1 session delay
assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]);
- let collator = CandidateInfo { who: 5, deposit: 10 };
+ let collator = CandidateInfo {
+ who: 5,
+ deposit: 10,
+ };
assert_eq!(CollatorSelection::candidates(), vec![collator]);
assert_eq!(CollatorSelection::last_authored_block(4), 20);
initialize_to_block(30);
@@ -401,7 +449,9 @@
#[should_panic = "duplicate invulnerables in genesis."]
fn cannot_set_genesis_value_twice() {
sp_tracing::try_init_simple();
- let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
+ let mut t = frame_system::GenesisConfig::default()
+ .build_storage::<Test>()
+ .unwrap();
let invulnerables = vec![1, 1];
let collator_selection = collator_selection::GenesisConfig::<Test> {
pallets/collator-selection/src/weights.rsdiffbeforeafterboth--- a/pallets/collator-selection/src/weights.rs
+++ b/pallets/collator-selection/src/weights.rs
@@ -41,6 +41,7 @@
};
use sp_std::marker::PhantomData;
+// todo:collator re-generate weights
// The weight info trait for `pallet_collator_selection`.
pub trait WeightInfo {
fn set_invulnerables(_b: u32) -> Weight;
primitives/common/src/constants.rsdiffbeforeafterboth--- a/primitives/common/src/constants.rs
+++ b/primitives/common/src/constants.rs
@@ -46,6 +46,8 @@
pub const EXISTENTIAL_DEPOSIT: u128 = 0;
/// Amount of Balance reserved for candidate registration.
pub const GENESIS_CANDIDACY_BOND: u128 = EXISTENTIAL_DEPOSIT;
+/// How long a periodic session lasts in blocks.
+pub const SESSION_LENGTH: BlockNumber = MINUTES;
// Targeting 0.1 UNQ per transfer
pub const WEIGHT_TO_FEE_COEFF: u32 = /*<weight2fee>*/175_199_920/*</weight2fee>*/;
runtime/common/config/pallets/collator_selection.rsdiffbeforeafterboth--- a/runtime/common/config/pallets/collator_selection.rs
+++ b/runtime/common/config/pallets/collator_selection.rs
@@ -18,12 +18,13 @@
use frame_system::EnsureRoot;
use crate::{
AccountId, BlockNumber, Runtime, RuntimeEvent, Balances, Aura, Session, SessionKeys,
- CollatorSelection,
+ CollatorSelection, config::pallets::TreasuryAccountId,
};
+use sp_runtime::Perbill;
use up_common::constants::*;
parameter_types! {
- pub const SessionPeriod: BlockNumber = HOURS;
+ pub const SessionPeriod: BlockNumber = SESSION_LENGTH;
pub const SessionOffset: BlockNumber = 0;
}
@@ -54,9 +55,10 @@
parameter_types! {
pub const PotId: PalletId = PalletId(*b"PotStake");
- pub const MaxCandidates: u32 = 1000;
- pub const MinCandidates: u32 = 5;
- pub const MaxInvulnerables: u32 = 100;
+ pub const MaxCandidates: u32 = 30; // todo:collator 30 collator slots - 3 planned invulnerables
+ pub const MinCandidates: u32 = 1;
+ pub const MaxInvulnerables: u32 = 30;
+ pub const SlashRatio: Perbill = Perbill::from_percent(100);
}
impl pallet_collator_selection::Config for Runtime {
@@ -64,13 +66,13 @@
type Currency = Balances;
// We allow root only to execute privileged collator selection operations.
type UpdateOrigin = EnsureRoot<AccountId>;
+ type TreasuryAccountId = TreasuryAccountId;
type PotId = PotId;
type MaxCandidates = MaxCandidates;
type MinCandidates = MinCandidates;
type MaxInvulnerables = MaxInvulnerables;
// todo:collator kick threshold should be in storage and configured only by root -- or rather UpdateOrigin
- // Should be a multiple of session or things will get inconsistent.
- type KickThreshold = SessionPeriod;
+ type SlashRatio = SlashRatio;
type ValidatorId = <Self as frame_system::Config>::AccountId;
type ValidatorIdOf = pallet_collator_selection::IdentityCollator;
type ValidatorRegistration = Session;
runtime/common/runtime_apis.rsdiffbeforeafterboth--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -703,6 +703,10 @@
#[cfg(feature = "rmrk")]
list_benchmark!(list, extra, pallet_proxy_rmrk_equip, RmrkEquip);
+ // todo:collator check benchmarks
+ #[cfg(feature = "collator-selection")]
+ list_benchmark!(list, extra, pallet_collator_selection, CollatorSelection);
+
#[cfg(feature = "foreign-assets")]
list_benchmark!(list, extra, pallet_foreign_assets, ForeignAssets);
@@ -766,6 +770,10 @@
#[cfg(feature = "rmrk")]
add_benchmark!(params, batches, pallet_proxy_rmrk_equip, RmrkEquip);
+ // todo:collator check benchmarks
+ #[cfg(feature = "collator-selection")]
+ add_benchmark!(params, batches, pallet_collator_selection, CollatorSelection);
+
#[cfg(feature = "foreign-assets")]
add_benchmark!(params, batches, pallet_foreign_assets, ForeignAssets);
tests/src/collatorSelection.test.tsdiffbeforeafterboth--- a/tests/src/collatorSelection.test.ts
+++ b/tests/src/collatorSelection.test.ts
@@ -17,90 +17,256 @@
import {IKeyringPair} from '@polkadot/types/types';
import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from './util';
-// todo Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
-describe('Integration Test: Dynamic shuffling of collators', () => {
+async function resetInvulnerables() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const superuser = await privateKey('//Alice');
+ const alice = await privateKey('//Alice');
+ const bob = await privateKey('//Bob');
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ if (!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {
+ console.warn('Alice and Bob are not the invulnerables! Reinstating them back. '
+ + 'Current invulnerables\' size: ' + invulnerables.length);
+
+ let nonce = await helper.chain.getNonce(alice.address);
+ await Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: nonce++}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: nonce++}),
+ ]);
+
+ nonce = await helper.chain.getNonce(alice.address);
+ await Promise.all(invulnerables.map((invulnerable: any) => {
+ if (invulnerable == alice.address || invulnerable == bob.address) return new Promise<void>(res => res());
+ return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});
+ }));
+ }
+ });
+}
+
+// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
+// + 18 tests: 5 (1+4) on session change
+describe('Integration Test: Collator Selection', () => {
let superuser: IKeyringPair;
// These are the default invulnerables, and should return to be invulnerables after this suite.
- let aliceAddress: string;
- let bobAddress: string;
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
let charlie: IKeyringPair;
let dave: IKeyringPair;
//let eve: IKeyringPair;
- before(async function() {
+ before(async function() {
await usingPlaygrounds(async (helper, privateKey) => {
requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);
+ //todo:collator
//const donor = await privateKey({filename: __filename});
//[charlie, dave] = await helper.arrange.createAccounts([100n, 100n], donor);
+ alice = await privateKey('//Alice');
+ bob = await privateKey('//Bob');
charlie = await privateKey('//Charlie');
dave = await privateKey('//Dave');
superuser = await privateKey('//Alice');
- aliceAddress = (await privateKey('//Alice')).address;
- bobAddress = (await privateKey('//Bob')).address;
+ });
+ });
+
+ describe('Dynamic shuffling of collators', () => {
+ before(async function() {
+ await usingPlaygrounds(async (helper) => {
+ expect((await helper.collatorSelection.setOwnKeys(charlie))
+ .status.toLowerCase()).to.be.equal('success');
+ expect((await helper.collatorSelection.setOwnKeys(dave))
+ .status.toLowerCase()).to.be.equal('success');
+
+ // todo:collator check necessity + add RPC for invulnerables / just improve in general
+ // validators = await helper.callRpc('api.query.session.validators');
+ const invulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
+ if (!invulnerables.includes(alice.address) || !invulnerables.includes(bob.address) || invulnerables.length != 2) {
+ console.warn('Alice and Bob are not the invulnerables! Reinstating them back. '
+ + 'Current invulnerables\' size: ' + invulnerables.length);
+
+ await Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: 0}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: 1}),
+ ]);
+
+ let nonce = 0;
+ await Promise.all(invulnerables.map((invulnerable: any) => {
+ if (invulnerable == alice.address || invulnerable == bob.address) return new Promise((res) => res);
+ return helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++});
+ }));
+ }
+ });
+ });
+
+ itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
+ await expect(Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [charlie.address], true, {nonce: 0}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [dave.address], true, {nonce: 1}),
+ ])).to.be.fulfilled;
+
+ await expect(Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alice.address], true, {nonce: 0}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [bob.address], true, {nonce: 1}),
+ ])).to.be.fulfilled;
+
+ const newInvulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
+ expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+
+ const expectedSessionIndex = (await helper.callRpc('api.query.session.currentIndex')).toNumber() + 2;
+ let currentSessionIndex = -1;
+ console.log('Waiting for the session after the next.'
+ + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
+
+ while (currentSessionIndex < expectedSessionIndex) {
+ // eslint-disable-next-line no-async-promise-executor
+ currentSessionIndex = await expect(helper.wait.withTimeout(new Promise(async (resolve) => {
+ //todo:collator
+ console.log('starting wait...');
+ console.time('ein');
+ await helper.wait.newBlocks(1);
+ console.timeLog('ein');
+ const res = (await helper.callRpc('api.query.session.currentIndex')).toNumber();
+ console.timeEnd('ein');
+ resolve(res);
+ }), 24000, 'The chain has stopped producing blocks!')).to.be.fulfilled;
+ }
+
+ const newValidators = await helper.callRpc('api.query.session.validators');
+ expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+
+ const lastBlockNumber = await helper.chain.getLatestBlockNumber();
+ await helper.wait.newBlocks(1);
+ const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();
+ const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();
+ expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;
+ });
+
+ // todo:collator keyless invulnerables? will hang, so, a breaking test, eh
+ // register candidate without sudos and the like
+
+ after(async () => {
+ await usingPlaygrounds(async (helper) => {
+ if (helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;
+
+ await Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [alice.address], true, {nonce: 0}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [bob.address], true, {nonce: 1}),
+ ]);
+
+ await Promise.all([
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [charlie.address], true, {nonce: 0}),
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [dave.address], true, {nonce: 1}),
+ ]);
+ });
+ });
+ });
+
+ // todo:collator make sure that there is enough session time for a set of tests
+ // 28 non-functioning collators, teehee.
+
+ describe('Addition and removal of invulnerables', () => {
+ before(async function() {
+ await resetInvulnerables();
+ });
- expect((await helper.executeExtrinsic(charlie, 'api.tx.session.setKeys', [
- '0x' + Buffer.from(charlie.addressRaw).toString('hex'),
- '0x0',
- ])).status.toLowerCase()).to.be.equal('success');
+ describe('Positive', () => {
+ itSub('Adds an invulnerable', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([10n], superuser);
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
- expect((await helper.executeExtrinsic(dave, 'api.tx.session.setKeys', [
- '0x' + Buffer.from(dave.addressRaw).toString('hex'),
- '0x0',
- ])).status.toLowerCase()).to.be.equal('success');
+ await helper.collatorSelection.setOwnKeys(account);
+ await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);
+ });
- const validators = await helper.callRpc('api.query.session.validators');
- expect(validators).to.not.contain(charlie.address).and.not.contain(dave.address);
+ itSub('Removes an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ const lastInvulnerable = invulnerables.pop();
+
+ await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ // invulnerables had its last element removed, so they should be equal
+ expect(newInvulnerables).to.have.all.members(invulnerables);
+ });
});
- });
- itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
+ describe('Negative', () => {
+ itSub('Does not duplicate an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ // adding an already invulnerable should not fail, but should not duplicate it either
+ await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))
+ .to.be.fulfilled;
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.have.all.members(invulnerables);
+ });
+
+ itSub('Cannot allow invulnerables to be empty', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ const lastInvulnerable = invulnerables.pop();
+
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(invulnerables.map((i: any) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));
- const tx = helper.constructApiCall('api.tx.collatorSelection.setInvulnerables', [[
- charlie.address,
- dave.address,
- ]]);
- await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.be.fulfilled;
+ await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))
+ .to.be.rejected;//todo:collator With(/collatorSelection.TooFewInvulnerables/);
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);
+
+ // restore the invulnerables to the previous state
+ nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(invulnerables.map((i: any) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));
+ });
+
+ itSub('Cannot have too many invulnerables', async ({helper}) => {
+ const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
+ const invulnerablesUntilLimit = 30 - invulnerablesLength;
+ const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
+ const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
- const newInvulnerables = await helper.callRpc('api.query.collatorSelection.invulnerables');
- expect(newInvulnerables).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.collatorSelection.setOwnKeys(i)));
+ await helper.collatorSelection.setOwnKeys(lastInvulnerable);
- const expectedSessionIndex = (await helper.callRpc('api.query.session.currentIndex')).toNumber() + 2;
- let currentSessionIndex = -1;
- console.log('Waiting for the session after the next.'
- + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));
- while (currentSessionIndex < expectedSessionIndex) {
- // eslint-disable-next-line no-async-promise-executor
- currentSessionIndex = await expect(helper.wait.withTimeout(new Promise(async (resolve) => {
- await helper.wait.newBlocks(1);
- const res = (await helper.callRpc('api.query.session.currentIndex')).toNumber();
- resolve(res);
- }), 24000, 'The chain has stopped producing blocks!')).to.be.fulfilled;
- }
+ await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))
+ .to.be.rejected; // todo:collator With(/collatorSelection.TooManyInvulnerables/);
+
+ // restore the invulnerables to the previous state
+ nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));
+ });
- const newValidators = await helper.callRpc('api.query.session.validators');
- expect(newValidators).to.contain(charlie.address).and.contain(dave.address).and.be.length(2);
+ itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([10n], bob);
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
- const lastBlockNumber = await helper.chain.getLatestBlockNumber();
- await helper.wait.newBlocks(1);
- const lastCharlieBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [charlie.address])).toNumber();
- const lastDaveBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [dave.address])).toNumber();
- expect(lastCharlieBlock >= lastBlockNumber || lastDaveBlock >= lastBlockNumber).to.be.true;
- });
+ await helper.collatorSelection.setOwnKeys(account);
+ await expect(helper.collatorSelection.addInvulnerable(bob, account.address))
+ .to.be.rejectedWith(/BadOrigin/);
- after(async () => {
- await usingPlaygrounds(async (helper) => {
- if (helper.fetchMissingPalletNames([Pallets.AppPromotion]).length != 0) return;
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.be.members(invulnerables);
+ });
- const tx = helper.constructApiCall('api.tx.collatorSelection.setInvulnerables', [[
- aliceAddress,
- bobAddress,
- ]]);
- await expect(helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [tx])).to.be.fulfilled;
+ itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))
+ .to.be.rejectedWith(/BadOrigin/);
+ expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);
+ });
});
+
+ // todo:collator after
});
-});
+});
\ No newline at end of file
tests/src/util/playgrounds/unique.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034/* eslint-disable @typescript-eslint/no-var-requires */5/* eslint-disable function-call-argument-newline */6/* eslint-disable no-prototype-builtins */78import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';9import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types';10import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm, base58Encode, blake2AsU8a} from '@polkadot/util-crypto';11import {IKeyringPair} from '@polkadot/types/types';12import {hexToU8a} from '@polkadot/util/hex';13import {u8aConcat} from '@polkadot/util/u8a';14import {BN} from '@polkadot/util/bn';15import {16 IApiListeners,17 IBlock,18 IEvent,19 IChainProperties,20 ICollectionCreationOptions,21 ICollectionLimits,22 ICollectionPermissions,23 ICrossAccountId,24 ICrossAccountIdLower,25 ILogger,26 INestingPermissions,27 IProperty,28 IStakingInfo,29 ISchedulerOptions,30 ISubstrateBalance,31 IToken,32 ITokenPropertyPermission,33 ITransactionResult,34 IUniqueHelperLog,35 TApiAllowedListeners,36 TEthereumAccount,37 TSigner,38 TSubstrateAccount,39 TNetworks,40 IForeignAssetMetadata,41 AcalaAssetMetadata,42 MoonbeamAssetInfo,43 DemocracyStandardAccountVote,44 IEthCrossAccountId,45} from './types';46import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';47import type {Vec} from '@polkadot/types-codec';48import {FrameSystemEventRecord} from '@polkadot/types/lookup';4950export class CrossAccountId implements ICrossAccountId {51 Substrate?: TSubstrateAccount;52 Ethereum?: TEthereumAccount;5354 constructor(account: ICrossAccountId) {55 if (account.Substrate) this.Substrate = account.Substrate;56 if (account.Ethereum) this.Ethereum = account.Ethereum;57 }5859 static fromKeyring(account: IKeyringPair, domain: 'Substrate' | 'Ethereum' = 'Substrate') {60 switch (domain) {61 case 'Substrate': return new CrossAccountId({Substrate: account.address});62 case 'Ethereum': return new CrossAccountId({Substrate: account.address}).toEthereum();63 }64 }6566 static fromLowerCaseKeys(address: ICrossAccountIdLower): CrossAccountId {67 return new CrossAccountId({Substrate: address.substrate, Ethereum: address.ethereum});68 }6970 static normalizeSubstrateAddress(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {71 return encodeAddress(decodeAddress(address), ss58Format);72 }7374 static withNormalizedSubstrate(address: TSubstrateAccount, ss58Format = 42): CrossAccountId {75 return new CrossAccountId({Substrate: CrossAccountId.normalizeSubstrateAddress(address, ss58Format)});76 }7778 withNormalizedSubstrate(ss58Format = 42): CrossAccountId {79 if (this.Substrate) return CrossAccountId.withNormalizedSubstrate(this.Substrate, ss58Format);80 return this;81 }8283 static translateSubToEth(address: TSubstrateAccount): TEthereumAccount {84 return nesting.toChecksumAddress('0x' + Array.from(addressToEvm(address), i => i.toString(16).padStart(2, '0')).join(''));85 }8687 toEthereum(): CrossAccountId {88 if (this.Substrate) return new CrossAccountId({Ethereum: CrossAccountId.translateSubToEth(this.Substrate)});89 return this;90 }9192 static translateEthToSub(address: TEthereumAccount, ss58Format?: number): TSubstrateAccount {93 return evmToAddress(address, ss58Format);94 }9596 toSubstrate(ss58Format?: number): CrossAccountId {97 if (this.Ethereum) return new CrossAccountId({Substrate: CrossAccountId.translateEthToSub(this.Ethereum, ss58Format)});98 return this;99 }100101 toLowerCase(): CrossAccountId {102 if (this.Substrate) this.Substrate = this.Substrate.toLowerCase();103 if (this.Ethereum) this.Ethereum = this.Ethereum.toLowerCase();104 return this;105 }106}107108const nesting = {109 toChecksumAddress(address: string): string {110 if (typeof address === 'undefined') return '';111112 if(!/^(0x)?[0-9a-f]{40}$/i.test(address)) throw new Error(`Given address "${address}" is not a valid Ethereum address.`);113114 address = address.toLowerCase().replace(/^0x/i,'');115 const addressHash = keccakAsHex(address).replace(/^0x/i,'');116 const checksumAddress = ['0x'];117118 for (let i = 0; i < address.length; i++) {119 // If ith character is 8 to f then make it uppercase120 if (parseInt(addressHash[i], 16) > 7) {121 checksumAddress.push(address[i].toUpperCase());122 } else {123 checksumAddress.push(address[i]);124 }125 }126 return checksumAddress.join('');127 },128 tokenIdToAddress(collectionId: number, tokenId: number) {129 return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8,'0')}${tokenId.toString(16).padStart(8,'0')}`);130 },131};132133class UniqueUtil {134 static transactionStatus = {135 NOT_READY: 'NotReady',136 FAIL: 'Fail',137 SUCCESS: 'Success',138 };139140 static chainLogType = {141 EXTRINSIC: 'extrinsic',142 RPC: 'rpc',143 };144145 static getTokenAccount(token: IToken): CrossAccountId {146 return new CrossAccountId({Ethereum: this.getTokenAddress(token)});147 }148149 static getTokenAddress(token: IToken): string {150 return nesting.tokenIdToAddress(token.collectionId, token.tokenId);151 }152153 static getDefaultLogger(): ILogger {154 return {155 log(msg: any, level = 'INFO') {156 console[level.toLocaleLowerCase() === 'error' ? 'error' : 'log'](...(Array.isArray(msg) ? msg : [msg]));157 },158 level: {159 ERROR: 'ERROR',160 WARNING: 'WARNING',161 INFO: 'INFO',162 },163 };164 }165166 static vec2str(arr: string[] | number[]) {167 return arr.map(x => String.fromCharCode(parseInt(x.toString()))).join('');168 }169170 static str2vec(string: string) {171 if (typeof string !== 'string') return string;172 return Array.from(string).map(x => x.charCodeAt(0));173 }174175 static fromSeed(seed: string, ss58Format = 42) {176 const keyring = new Keyring({type: 'sr25519', ss58Format});177 return keyring.addFromUri(seed);178 }179180 static extractCollectionIdFromCreationResult(creationResult: ITransactionResult): number {181 if (creationResult.status !== this.transactionStatus.SUCCESS) {182 throw Error('Unable to create collection!');183 }184185 let collectionId = null;186 creationResult.result.events.forEach(({event: {data, method, section}}) => {187 if ((section === 'common') && (method === 'CollectionCreated')) {188 collectionId = parseInt(data[0].toString(), 10);189 }190 });191192 if (collectionId === null) {193 throw Error('No CollectionCreated event was found!');194 }195196 return collectionId;197 }198199 static extractTokensFromCreationResult(creationResult: ITransactionResult): {200 success: boolean,201 tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[],202 } {203 if (creationResult.status !== this.transactionStatus.SUCCESS) {204 throw Error('Unable to create tokens!');205 }206 let success = false;207 const tokens = [] as {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[];208 creationResult.result.events.forEach(({event: {data, method, section}}) => {209 if (method === 'ExtrinsicSuccess') {210 success = true;211 } else if ((section === 'common') && (method === 'ItemCreated')) {212 tokens.push({213 collectionId: parseInt(data[0].toString(), 10),214 tokenId: parseInt(data[1].toString(), 10),215 owner: data[2].toHuman(),216 amount: data[3].toBigInt(),217 });218 }219 });220 return {success, tokens};221 }222223 static extractTokensFromBurnResult(burnResult: ITransactionResult): {224 success: boolean,225 tokens: {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[],226 } {227 if (burnResult.status !== this.transactionStatus.SUCCESS) {228 throw Error('Unable to burn tokens!');229 }230 let success = false;231 const tokens = [] as {collectionId: number, tokenId: number, owner: CrossAccountId, amount: bigint}[];232 burnResult.result.events.forEach(({event: {data, method, section}}) => {233 if (method === 'ExtrinsicSuccess') {234 success = true;235 } else if ((section === 'common') && (method === 'ItemDestroyed')) {236 tokens.push({237 collectionId: parseInt(data[0].toString(), 10),238 tokenId: parseInt(data[1].toString(), 10),239 owner: data[2].toHuman(),240 amount: data[3].toBigInt(),241 });242 }243 });244 return {success, tokens};245 }246247 static findCollectionInEvents(events: {event: IEvent}[], collectionId: number, expectedSection: string, expectedMethod: string): boolean {248 let eventId = null;249 events.forEach(({event: {data, method, section}}) => {250 if ((section === expectedSection) && (method === expectedMethod)) {251 eventId = parseInt(data[0].toString(), 10);252 }253 });254255 if (eventId === null) {256 throw Error(`No ${expectedMethod} event was found!`);257 }258 return eventId === collectionId;259 }260261 static isTokenTransferSuccess(events: {event: IEvent}[], collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {262 const normalizeAddress = (address: string | ICrossAccountId) => {263 if(typeof address === 'string') return address;264 const obj = {} as any;265 Object.keys(address).forEach(k => {266 obj[k.toLocaleLowerCase()] = address[k as 'Substrate' | 'Ethereum'];267 });268 if(obj.substrate) return CrossAccountId.withNormalizedSubstrate(obj.substrate);269 if(obj.ethereum) return CrossAccountId.fromLowerCaseKeys(obj).toLowerCase();270 return address;271 };272 let transfer = {collectionId: null, tokenId: null, from: null, to: null, amount: 1} as any;273 events.forEach(({event: {data, method, section}}) => {274 if ((section === 'common') && (method === 'Transfer')) {275 const hData = (data as any).toJSON();276 transfer = {277 collectionId: hData[0],278 tokenId: hData[1],279 from: normalizeAddress(hData[2]),280 to: normalizeAddress(hData[3]),281 amount: BigInt(hData[4]),282 };283 }284 });285 let isSuccess = parseInt(collectionId.toString()) === transfer.collectionId && parseInt(tokenId.toString()) === transfer.tokenId;286 isSuccess = isSuccess && JSON.stringify(normalizeAddress(fromAddressObj)) === JSON.stringify(transfer.from);287 isSuccess = isSuccess && JSON.stringify(normalizeAddress(toAddressObj)) === JSON.stringify(transfer.to);288 isSuccess = isSuccess && amount === transfer.amount;289 return isSuccess;290 }291292 static bigIntToDecimals(number: bigint, decimals = 18) {293 const numberStr = number.toString();294 const dotPos = numberStr.length - decimals;295296 if (dotPos <= 0) {297 return '0.' + '0'.repeat(Math.abs(dotPos)) + numberStr;298 } else {299 const intPart = numberStr.substring(0, dotPos);300 const fractPart = numberStr.substring(dotPos);301 return intPart + '.' + fractPart;302 }303 }304}305306class UniqueEventHelper {307 private static extractIndex(index: any): [number, number] | string {308 if(index.toRawType() === '[u8;2]') return [index[0], index[1]];309 return index.toJSON();310 }311312 private static extractSub(data: any, subTypes: any): {[key: string]: any} {313 let obj: any = {};314 let index = 0;315316 if (data.entries) {317 for(const [key, value] of data.entries()) {318 obj[key] = this.extractData(value, subTypes[index]);319 index++;320 }321 } else obj = data.toJSON();322323 return obj;324 }325326 private static toHuman(data: any) {327 return data && data.toHuman ? data.toHuman() : `${data}`;328 }329330 private static extractData(data: any, type: any): any {331 if(!type) return this.toHuman(data);332 if (['u16', 'u32'].indexOf(type.type) > -1) return data.toNumber();333 if (['u64', 'u128', 'u256'].indexOf(type.type) > -1) return data.toBigInt();334 if(type.hasOwnProperty('sub')) return this.extractSub(data, type.sub);335 return this.toHuman(data);336 }337338 public static extractEvents(events: {event: any, phase: any}[]): IEvent[] {339 const parsedEvents: IEvent[] = [];340341 events.forEach((record) => {342 const {event, phase} = record;343 const types = event.typeDef;344345 const eventData: IEvent = {346 section: event.section.toString(),347 method: event.method.toString(),348 index: this.extractIndex(event.index),349 data: [],350 phase: phase.toJSON(),351 };352353 event.data.forEach((val: any, index: number) => {354 eventData.data.push(this.extractData(val, types[index]));355 });356357 parsedEvents.push(eventData);358 });359360 return parsedEvents;361 }362}363364export class ChainHelperBase {365 helperBase: any;366367 transactionStatus = UniqueUtil.transactionStatus;368 chainLogType = UniqueUtil.chainLogType;369 util: typeof UniqueUtil;370 eventHelper: typeof UniqueEventHelper;371 logger: ILogger;372 api: ApiPromise | null;373 forcedNetwork: TNetworks | null;374 network: TNetworks | null;375 chainLog: IUniqueHelperLog[];376 children: ChainHelperBase[];377 address: AddressGroup;378 chain: ChainGroup;379380 constructor(logger?: ILogger, helperBase?: any) {381 this.helperBase = helperBase;382383 this.util = UniqueUtil;384 this.eventHelper = UniqueEventHelper;385 if (typeof logger == 'undefined') logger = this.util.getDefaultLogger();386 this.logger = logger;387 this.api = null;388 this.forcedNetwork = null;389 this.network = null;390 this.chainLog = [];391 this.children = [];392 this.address = new AddressGroup(this);393 this.chain = new ChainGroup(this);394 }395396 clone(helperCls: ChainHelperBaseConstructor, options: {[key: string]: any} = {}) {397 Object.setPrototypeOf(helperCls.prototype, this);398 const newHelper = new helperCls(this.logger, options);399400 newHelper.api = this.api;401 newHelper.network = this.network;402 newHelper.forceNetwork = this.forceNetwork;403404 this.children.push(newHelper);405406 return newHelper;407 }408409 getApi(): ApiPromise {410 if(this.api === null) throw Error('API not initialized');411 return this.api;412 }413414 async subscribeEvents(expectedEvents: {section: string, names: string[]}[]) {415 const collectedEvents: IEvent[] = [];416 const unsubscribe = await this.getApi().query.system.events((events: Vec<FrameSystemEventRecord>) => {417 const ievents = this.eventHelper.extractEvents(events);418 ievents.forEach((event) => {419 expectedEvents.forEach((e => {420 if (event.section === e.section && e.names.includes(event.method)) {421 collectedEvents.push(event);422 }423 }));424 });425 });426 return {unsubscribe: unsubscribe as any, collectedEvents};427 }428429 clearChainLog(): void {430 this.chainLog = [];431 }432433 forceNetwork(value: TNetworks): void {434 this.forcedNetwork = value;435 }436437 async connect(wsEndpoint: string, listeners?: IApiListeners) {438 if (this.api !== null) throw Error('Already connected');439 const {api, network} = await ChainHelperBase.createConnection(wsEndpoint, listeners, this.forcedNetwork);440 this.api = api;441 this.network = network;442 }443444 async disconnect() {445 for (const child of this.children) {446 child.clearApi();447 }448449 if (this.api === null) return;450 await this.api.disconnect();451 this.clearApi();452 }453454 clearApi() {455 this.api = null;456 this.network = null;457 }458459 static async detectNetwork(api: ApiPromise): Promise<TNetworks> {460 const spec = (await api.query.system.lastRuntimeUpgrade()).toJSON() as any;461 const xcmChains = ['rococo', 'westend', 'westmint', 'acala', 'karura', 'moonbeam', 'moonriver'];462463 if(xcmChains.indexOf(spec.specName) > -1) return spec.specName;464465 if(['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName;466 return 'opal';467 }468469 static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise<TNetworks> {470 const api = new ApiPromise({provider: new WsProvider(wsEndpoint)});471 await api.isReady;472473 const network = await this.detectNetwork(api);474475 await api.disconnect();476477 return network;478 }479480 static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TNetworks | null): Promise<{481 api: ApiPromise;482 network: TNetworks;483 }> {484 if(typeof network === 'undefined' || network === null) network = 'opal';485 const supportedRPC = {486 opal: {487 unique: require('@unique-nft/opal-testnet-types/definitions').unique.rpc,488 },489 quartz: {490 unique: require('@unique-nft/quartz-mainnet-types/definitions').unique.rpc,491 },492 unique: {493 unique: require('@unique-nft/unique-mainnet-types/definitions').unique.rpc,494 },495 rococo: {},496 westend: {},497 moonbeam: {},498 moonriver: {},499 acala: {},500 karura: {},501 westmint: {},502 };503 if(!supportedRPC.hasOwnProperty(network)) network = await this.detectNetworkByWsEndpoint(wsEndpoint);504 const rpc = supportedRPC[network];505506 // TODO: investigate how to replace rpc in runtime507 // api._rpcCore.addUserInterfaces(rpc);508509 const api = new ApiPromise({provider: new WsProvider(wsEndpoint), rpc});510511 await api.isReadyOrError;512513 if (typeof listeners === 'undefined') listeners = {};514 for (const event of ['connected', 'disconnected', 'error', 'ready', 'decorated']) {515 if (!listeners.hasOwnProperty(event) || typeof listeners[event as TApiAllowedListeners] === 'undefined') continue;516 api.on(event as ApiInterfaceEvents, listeners[event as TApiAllowedListeners] as (...args: any[]) => any);517 }518519 return {api, network};520 }521522 getTransactionStatus(data: {events: {event: IEvent}[], status: any}) {523 const {events, status} = data;524 if (status.isReady) {525 return this.transactionStatus.NOT_READY;526 }527 if (status.isBroadcast) {528 return this.transactionStatus.NOT_READY;529 }530 if (status.isInBlock || status.isFinalized) {531 const errors = events.filter(e => e.event.method === 'ExtrinsicFailed');532 if (errors.length > 0) {533 return this.transactionStatus.FAIL;534 }535 if (events.filter(e => e.event.method === 'ExtrinsicSuccess').length > 0) {536 return this.transactionStatus.SUCCESS;537 }538 }539540 return this.transactionStatus.FAIL;541 }542543 signTransaction(sender: TSigner, transaction: any, options: Partial<SignerOptions> | null = null, label = 'transaction') {544 const sign = (callback: any) => {545 if(options !== null) return transaction.signAndSend(sender, options, callback);546 return transaction.signAndSend(sender, callback);547 };548 // eslint-disable-next-line no-async-promise-executor549 return new Promise(async (resolve, reject) => {550 try {551 const unsub = await sign((result: any) => {552 const status = this.getTransactionStatus(result);553554 if (status === this.transactionStatus.SUCCESS) {555 this.logger.log(`${label} successful`);556 unsub();557 resolve({result, status});558 } else if (status === this.transactionStatus.FAIL) {559 let moduleError = null;560561 if (result.hasOwnProperty('dispatchError')) {562 const dispatchError = result['dispatchError'];563564 if (dispatchError) {565 if (dispatchError.isModule) {566 const modErr = dispatchError.asModule;567 const errorMeta = dispatchError.registry.findMetaError(modErr);568569 moduleError = `${errorMeta.section}.${errorMeta.name}`;570 } else {571 moduleError = dispatchError.toHuman();572 }573 } else {574 this.logger.log(result, this.logger.level.ERROR);575 }576 }577578 this.logger.log(`Something went wrong with ${label}. Status: ${status}`, this.logger.level.ERROR);579 unsub();580 reject({status, moduleError, result});581 }582 });583 } catch (e) {584 this.logger.log(e, this.logger.level.ERROR);585 reject(e);586 }587 });588 }589590 async getPaymentInfo(signer: TSigner, tx: any, len: number | null) {591 const api = this.getApi();592 const signingInfo = await api.derive.tx.signingInfo(signer.address);593594 // We need to sign the tx because595 // unsigned transactions does not have an inclusion fee596 tx.sign(signer, {597 blockHash: api.genesisHash,598 genesisHash: api.genesisHash,599 runtimeVersion: api.runtimeVersion,600 nonce: signingInfo.nonce,601 });602603 if (len === null) {604 return (await this.callRpc('api.rpc.payment.queryInfo', [tx.toHex()])) as RuntimeDispatchInfo;605 } else {606 return (await api.call.transactionPaymentApi.queryInfo(tx, len)) as RuntimeDispatchInfo;607 }608 }609610 constructApiCall(apiCall: string, params: any[]) {611 if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`);612 let call = this.getApi() as any;613 for(const part of apiCall.slice(4).split('.')) {614 call = call[part];615 }616 return call(...params);617 }618619 async executeExtrinsic(sender: TSigner, extrinsic: string, params: any[], expectSuccess=true, options: Partial<SignerOptions>|null = null/*, failureMessage='expected success'*/) {620 if(this.api === null) throw Error('API not initialized');621 if(!extrinsic.startsWith('api.tx.')) throw Error(`${extrinsic} is not transaction`);622623 const startTime = (new Date()).getTime();624 let result: ITransactionResult;625 let events: IEvent[] = [];626 try {627 result = await this.signTransaction(sender, this.constructApiCall(extrinsic, params), options, extrinsic) as ITransactionResult;628 events = this.eventHelper.extractEvents(result.result.events);629 }630 catch(e) {631 if(!(e as object).hasOwnProperty('status')) throw e;632 result = e as ITransactionResult;633 }634635 const endTime = (new Date()).getTime();636637 const log = {638 executedAt: endTime,639 executionTime: endTime - startTime,640 type: this.chainLogType.EXTRINSIC,641 status: result.status,642 call: extrinsic,643 signer: this.getSignerAddress(sender),644 params,645 } as IUniqueHelperLog;646647 if(result.status !== this.transactionStatus.SUCCESS) {648 if (result.moduleError) log.moduleError = result.moduleError;649 else if (result.result.dispatchError) log.dispatchError = result.result.dispatchError;650 }651 if(events.length > 0) log.events = events;652653 this.chainLog.push(log);654655 if(expectSuccess && result.status !== this.transactionStatus.SUCCESS) {656 if (result.moduleError) throw Error(`${result.moduleError}`);657 else if (result.result.dispatchError) throw Error(JSON.stringify(result.result.dispatchError));658 }659 return result;660 }661662 async callRpc(rpc: string, params?: any[]) {663 if(typeof params === 'undefined') params = [];664 if(this.api === null) throw Error('API not initialized');665 if(!rpc.startsWith('api.rpc.') && !rpc.startsWith('api.query.')) throw Error(`${rpc} is not RPC call`);666667 const startTime = (new Date()).getTime();668 let result;669 let error = null;670 const log = {671 type: this.chainLogType.RPC,672 call: rpc,673 params,674 } as IUniqueHelperLog;675676 try {677 result = await this.constructApiCall(rpc, params);678 }679 catch(e) {680 error = e;681 }682683 const endTime = (new Date()).getTime();684685 log.executedAt = endTime;686 log.status = (error === null ? this.transactionStatus.SUCCESS : this.transactionStatus.FAIL) as 'Fail' | 'Success';687 log.executionTime = endTime - startTime;688689 this.chainLog.push(log);690691 if(error !== null) throw error;692693 return result;694 }695696 getSignerAddress(signer: IKeyringPair | string): string {697 if(typeof signer === 'string') return signer;698 return signer.address;699 }700701 fetchAllPalletNames(): string[] {702 if(this.api === null) throw Error('API not initialized');703 return this.api.runtimeMetadata.asLatest.pallets.map(m => m.name.toString().toLowerCase());704 }705706 fetchMissingPalletNames(requiredPallets: string[]): string[] {707 const palletNames = this.fetchAllPalletNames();708 return requiredPallets.filter(p => !palletNames.includes(p));709 }710}711712713class HelperGroup<T extends ChainHelperBase> {714 helper: T;715716 constructor(uniqueHelper: T) {717 this.helper = uniqueHelper;718 }719}720721722class CollectionGroup extends HelperGroup<UniqueHelper> {723 /**724 * Get number of blocks when sponsored transaction is available.725 *726 * @param collectionId ID of collection727 * @param tokenId ID of token728 * @param addressObj address for which the sponsorship is checked729 * @example await getTokenNextSponsored(1, 2, {Substrate: '5DfhbVfww7ThF8q6f3...'});730 * @returns number of blocks or null if sponsorship hasn't been set731 */732 async getTokenNextSponsored(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<number | null> {733 return (await this.helper.callRpc('api.rpc.unique.nextSponsored', [collectionId, addressObj, tokenId])).toJSON();734 }735736 /**737 * Get the number of created collections.738 *739 * @returns number of created collections740 */741 async getTotalCount(): Promise<number> {742 return (await this.helper.callRpc('api.rpc.unique.collectionStats')).created.toNumber();743 }744745 /**746 * Get information about the collection with additional data,747 * including the number of tokens it contains, its administrators,748 * the normalized address of the collection's owner, and decoded name and description.749 *750 * @param collectionId ID of collection751 * @example await getData(2)752 * @returns collection information object753 */754 async getData(collectionId: number): Promise<{755 id: number;756 name: string;757 description: string;758 tokensCount: number;759 admins: CrossAccountId[];760 normalizedOwner: TSubstrateAccount;761 raw: any762 } | null> {763 const collection = await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId]);764 const humanCollection = collection.toHuman(), collectionData = {765 id: collectionId, name: null, description: null, tokensCount: 0, admins: [],766 raw: humanCollection,767 } as any, jsonCollection = collection.toJSON();768 if (humanCollection === null) return null;769 collectionData.raw.limits = jsonCollection.limits;770 collectionData.raw.permissions = jsonCollection.permissions;771 collectionData.normalizedOwner = this.helper.address.normalizeSubstrate(collectionData.raw.owner);772 for (const key of ['name', 'description']) {773 collectionData[key] = this.helper.util.vec2str(humanCollection[key]);774 }775776 collectionData.tokensCount = (['RFT', 'NFT'].includes(humanCollection.mode))777 ? await this.helper[humanCollection.mode.toLocaleLowerCase() as 'nft' | 'rft'].getLastTokenId(collectionId)778 : 0;779 collectionData.admins = await this.getAdmins(collectionId);780781 return collectionData;782 }783784 /**785 * Get the addresses of the collection's administrators, optionally normalized.786 *787 * @param collectionId ID of collection788 * @param normalize whether to normalize the addresses to the default ss58 format789 * @example await getAdmins(1)790 * @returns array of administrators791 */792 async getAdmins(collectionId: number, normalize = false): Promise<CrossAccountId[]> {793 const admins = (await this.helper.callRpc('api.rpc.unique.adminlist', [collectionId])).toHuman();794795 return normalize796 ? admins.map((address: CrossAccountId) => address.withNormalizedSubstrate())797 : admins;798 }799800 /**801 * Get the addresses added to the collection allow-list, optionally normalized.802 * @param collectionId ID of collection803 * @param normalize whether to normalize the addresses to the default ss58 format804 * @example await getAllowList(1)805 * @returns array of allow-listed addresses806 */807 async getAllowList(collectionId: number, normalize = false): Promise<CrossAccountId[]> {808 const allowListed = (await this.helper.callRpc('api.rpc.unique.allowlist', [collectionId])).toHuman();809 return normalize810 ? allowListed.map((address: CrossAccountId) => address.withNormalizedSubstrate())811 : allowListed;812 }813814 /**815 * Get the effective limits of the collection instead of null for default values816 *817 * @param collectionId ID of collection818 * @example await getEffectiveLimits(2)819 * @returns object of collection limits820 */821 async getEffectiveLimits(collectionId: number): Promise<ICollectionLimits> {822 return (await this.helper.callRpc('api.rpc.unique.effectiveCollectionLimits', [collectionId])).toJSON();823 }824825 /**826 * Burns the collection if the signer has sufficient permissions and collection is empty.827 *828 * @param signer keyring of signer829 * @param collectionId ID of collection830 * @example await helper.collection.burn(aliceKeyring, 3);831 * @returns ```true``` if extrinsic success, otherwise ```false```832 */833 async burn(signer: TSigner, collectionId: number): Promise<boolean> {834 const result = await this.helper.executeExtrinsic(835 signer,836 'api.tx.unique.destroyCollection', [collectionId],837 true,838 );839840 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionDestroyed');841 }842843 /**844 * Sets the sponsor for the collection (Requires the Substrate address). Needs confirmation by the sponsor.845 *846 * @param signer keyring of signer847 * @param collectionId ID of collection848 * @param sponsorAddress Sponsor substrate address849 * @example setSponsor(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")850 * @returns ```true``` if extrinsic success, otherwise ```false```851 */852 async setSponsor(signer: TSigner, collectionId: number, sponsorAddress: TSubstrateAccount): Promise<boolean> {853 const result = await this.helper.executeExtrinsic(854 signer,855 'api.tx.unique.setCollectionSponsor', [collectionId, sponsorAddress],856 true,857 );858859 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionSponsorSet');860 }861862 /**863 * Confirms consent to sponsor the collection on behalf of the signer.864 *865 * @param signer keyring of signer866 * @param collectionId ID of collection867 * @example confirmSponsorship(aliceKeyring, 10)868 * @returns ```true``` if extrinsic success, otherwise ```false```869 */870 async confirmSponsorship(signer: TSigner, collectionId: number): Promise<boolean> {871 const result = await this.helper.executeExtrinsic(872 signer,873 'api.tx.unique.confirmSponsorship', [collectionId],874 true,875 );876877 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'SponsorshipConfirmed');878 }879880 /**881 * Removes the sponsor of a collection, regardless if it consented or not.882 *883 * @param signer keyring of signer884 * @param collectionId ID of collection885 * @example removeSponsor(aliceKeyring, 10)886 * @returns ```true``` if extrinsic success, otherwise ```false```887 */888 async removeSponsor(signer: TSigner, collectionId: number): Promise<boolean> {889 const result = await this.helper.executeExtrinsic(890 signer,891 'api.tx.unique.removeCollectionSponsor', [collectionId],892 true,893 );894895 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionSponsorRemoved');896 }897898 /**899 * Sets the limits of the collection. At least one limit must be specified for a correct call.900 *901 * @param signer keyring of signer902 * @param collectionId ID of collection903 * @param limits collection limits object904 * @example905 * await setLimits(906 * aliceKeyring,907 * 10,908 * {909 * sponsorTransferTimeout: 0,910 * ownerCanDestroy: false911 * }912 * )913 * @returns ```true``` if extrinsic success, otherwise ```false```914 */915 async setLimits(signer: TSigner, collectionId: number, limits: ICollectionLimits): Promise<boolean> {916 const result = await this.helper.executeExtrinsic(917 signer,918 'api.tx.unique.setCollectionLimits', [collectionId, limits],919 true,920 );921922 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionLimitSet');923 }924925 /**926 * Changes the owner of the collection to the new Substrate address.927 *928 * @param signer keyring of signer929 * @param collectionId ID of collection930 * @param ownerAddress substrate address of new owner931 * @example changeOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...")932 * @returns ```true``` if extrinsic success, otherwise ```false```933 */934 async changeOwner(signer: TSigner, collectionId: number, ownerAddress: TSubstrateAccount): Promise<boolean> {935 const result = await this.helper.executeExtrinsic(936 signer,937 'api.tx.unique.changeCollectionOwner', [collectionId, ownerAddress],938 true,939 );940941 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionOwnerChanged');942 }943944 /**945 * Adds a collection administrator.946 *947 * @param signer keyring of signer948 * @param collectionId ID of collection949 * @param adminAddressObj Administrator address (substrate or ethereum)950 * @example addAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})951 * @returns ```true``` if extrinsic success, otherwise ```false```952 */953 async addAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {954 const result = await this.helper.executeExtrinsic(955 signer,956 'api.tx.unique.addCollectionAdmin', [collectionId, adminAddressObj],957 true,958 );959960 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionAdminAdded');961 }962963 /**964 * Removes a collection administrator.965 *966 * @param signer keyring of signer967 * @param collectionId ID of collection968 * @param adminAddressObj Administrator address (substrate or ethereum)969 * @example removeAdmin(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."})970 * @returns ```true``` if extrinsic success, otherwise ```false```971 */972 async removeAdmin(signer: TSigner, collectionId: number, adminAddressObj: ICrossAccountId): Promise<boolean> {973 const result = await this.helper.executeExtrinsic(974 signer,975 'api.tx.unique.removeCollectionAdmin', [collectionId, adminAddressObj],976 true,977 );978979 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionAdminRemoved');980 }981982 /**983 * Check if user is in allow list.984 *985 * @param collectionId ID of collection986 * @param user Account to check987 * @example await getAdmins(1)988 * @returns is user in allow list989 */990 async allowed(collectionId: number, user: ICrossAccountId): Promise<boolean> {991 return (await this.helper.callRpc('api.rpc.unique.allowed', [collectionId, user])).toJSON();992 }993994 /**995 * Adds an address to allow list996 * @param signer keyring of signer997 * @param collectionId ID of collection998 * @param addressObj address to add to the allow list999 * @returns ```true``` if extrinsic success, otherwise ```false```1000 */1001 async addToAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {1002 const result = await this.helper.executeExtrinsic(1003 signer,1004 'api.tx.unique.addToAllowList', [collectionId, addressObj],1005 true,1006 );10071008 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'AllowListAddressAdded');1009 }10101011 /**1012 * Removes an address from allow list1013 *1014 * @param signer keyring of signer1015 * @param collectionId ID of collection1016 * @param addressObj address to remove from the allow list1017 * @returns ```true``` if extrinsic success, otherwise ```false```1018 */1019 async removeFromAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId): Promise<boolean> {1020 const result = await this.helper.executeExtrinsic(1021 signer,1022 'api.tx.unique.removeFromAllowList', [collectionId, addressObj],1023 true,1024 );10251026 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'AllowListAddressRemoved');1027 }10281029 /**1030 * Sets onchain permissions for selected collection.1031 *1032 * @param signer keyring of signer1033 * @param collectionId ID of collection1034 * @param permissions collection permissions object1035 * @example setPermissions(aliceKeyring, 10, {access:'AllowList', mintMode: true, nesting: {collectionAdmin: true, tokenOwner: true}});1036 * @returns ```true``` if extrinsic success, otherwise ```false```1037 */1038 async setPermissions(signer: TSigner, collectionId: number, permissions: ICollectionPermissions): Promise<boolean> {1039 const result = await this.helper.executeExtrinsic(1040 signer,1041 'api.tx.unique.setCollectionPermissions', [collectionId, permissions],1042 true,1043 );10441045 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPermissionSet');1046 }10471048 /**1049 * Enables nesting for selected collection. If `restricted` set, you can nest only tokens from specified collections.1050 *1051 * @param signer keyring of signer1052 * @param collectionId ID of collection1053 * @param permissions nesting permissions object1054 * @example enableNesting(aliceKeyring, 10, {collectionAdmin: true, tokenOwner: true});1055 * @returns ```true``` if extrinsic success, otherwise ```false```1056 */1057 async enableNesting(signer: TSigner, collectionId: number, permissions: INestingPermissions): Promise<boolean> {1058 return await this.setPermissions(signer, collectionId, {nesting: permissions});1059 }10601061 /**1062 * Disables nesting for selected collection.1063 *1064 * @param signer keyring of signer1065 * @param collectionId ID of collection1066 * @example disableNesting(aliceKeyring, 10);1067 * @returns ```true``` if extrinsic success, otherwise ```false```1068 */1069 async disableNesting(signer: TSigner, collectionId: number): Promise<boolean> {1070 return await this.setPermissions(signer, collectionId, {nesting: {tokenOwner: false, collectionAdmin: false}});1071 }10721073 /**1074 * Sets onchain properties to the collection.1075 *1076 * @param signer keyring of signer1077 * @param collectionId ID of collection1078 * @param properties array of property objects1079 * @example setProperties(aliceKeyring, 10, [{key: "gender", value: "male"}]);1080 * @returns ```true``` if extrinsic success, otherwise ```false```1081 */1082 async setProperties(signer: TSigner, collectionId: number, properties: IProperty[]): Promise<boolean> {1083 const result = await this.helper.executeExtrinsic(1084 signer,1085 'api.tx.unique.setCollectionProperties', [collectionId, properties],1086 true,1087 );10881089 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertySet');1090 }10911092 /**1093 * Get collection properties.1094 *1095 * @param collectionId ID of collection1096 * @param propertyKeys optionally filter the returned properties to only these keys1097 * @example getProperties(1219, ['location', 'date', 'time', 'isParadise']);1098 * @returns array of key-value pairs1099 */1100 async getProperties(collectionId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {1101 return (await this.helper.callRpc('api.rpc.unique.collectionProperties', [collectionId, propertyKeys])).toHuman();1102 }11031104 async getPropertiesConsumedSpace(collectionId: number): Promise<number> {1105 const api = this.helper.getApi();1106 const props = (await api.query.common.collectionProperties(collectionId)).toJSON();1107 1108 return (props! as any).consumedSpace;1109 }11101111 async getCollectionOptions(collectionId: number) {1112 return (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();1113 }11141115 /**1116 * Deletes onchain properties from the collection.1117 *1118 * @param signer keyring of signer1119 * @param collectionId ID of collection1120 * @param propertyKeys array of property keys to delete1121 * @example deleteProperties(aliceKeyring, 10, ["gender", "age"]);1122 * @returns ```true``` if extrinsic success, otherwise ```false```1123 */1124 async deleteProperties(signer: TSigner, collectionId: number, propertyKeys: string[]): Promise<boolean> {1125 const result = await this.helper.executeExtrinsic(1126 signer,1127 'api.tx.unique.deleteCollectionProperties', [collectionId, propertyKeys],1128 true,1129 );11301131 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'CollectionPropertyDeleted');1132 }11331134 /**1135 * Changes the owner of the token.1136 *1137 * @param signer keyring of signer1138 * @param collectionId ID of collection1139 * @param tokenId ID of token1140 * @param addressObj address of a new owner1141 * @param amount amount of tokens to be transfered. For NFT must be set to 1n1142 * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1143 * @returns true if the token success, otherwise false1144 */1145 async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {1146 const result = await this.helper.executeExtrinsic(1147 signer,1148 'api.tx.unique.transfer', [addressObj, collectionId, tokenId, amount],1149 true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,1150 );11511152 return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, {Substrate: typeof signer === 'string' ? signer : signer.address}, addressObj, amount);1153 }11541155 /**1156 *1157 * Change ownership of a token(s) on behalf of the owner.1158 *1159 * @param signer keyring of signer1160 * @param collectionId ID of collection1161 * @param tokenId ID of token1162 * @param fromAddressObj address on behalf of which the token will be sent1163 * @param toAddressObj new token owner1164 * @param amount amount of tokens to be transfered. For NFT must be set to 1n1165 * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg"}, {Ethereum: "0x9F0583DbB85..."})1166 * @returns true if the token success, otherwise false1167 */1168 async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1169 const result = await this.helper.executeExtrinsic(1170 signer,1171 'api.tx.unique.transferFrom', [fromAddressObj, toAddressObj, collectionId, tokenId, amount],1172 true, // `Unable to transfer token #${tokenId} from collection #${collectionId}`,1173 );1174 return this.helper.util.isTokenTransferSuccess(result.result.events, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1175 }11761177 /**1178 *1179 * Destroys a concrete instance of NFT/RFT or burns a specified amount of fungible tokens.1180 *1181 * @param signer keyring of signer1182 * @param collectionId ID of collection1183 * @param tokenId ID of token1184 * @param amount amount of tokens to be burned. For NFT must be set to 1n1185 * @example burnToken(aliceKeyring, 10, 5);1186 * @returns ```true``` if the extrinsic is successful, otherwise ```false```1187 */1188 async burnToken(signer: TSigner, collectionId: number, tokenId: number, amount=1n): Promise<boolean> {1189 const burnResult = await this.helper.executeExtrinsic(1190 signer,1191 'api.tx.unique.burnItem', [collectionId, tokenId, amount],1192 true, // `Unable to burn token for ${label}`,1193 );1194 const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1195 if (burnedTokens.tokens.length > 1) throw Error('Burned multiple tokens');1196 return burnedTokens.success;1197 }11981199 /**1200 * Destroys a concrete instance of NFT on behalf of the owner1201 *1202 * @param signer keyring of signer1203 * @param collectionId ID of collection1204 * @param tokenId ID of token1205 * @param fromAddressObj address on behalf of which the token will be burnt1206 * @param amount amount of tokens to be burned. For NFT must be set to 1n1207 * @example burnTokenFrom(aliceKeyring, 10, {Substrate: "5DyN4Y92vZCjv38fg..."}, 5, {Ethereum: "0x9F0583DbB85..."})1208 * @returns ```true``` if extrinsic success, otherwise ```false```1209 */1210 async burnTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1211 const burnResult = await this.helper.executeExtrinsic(1212 signer,1213 'api.tx.unique.burnFrom', [collectionId, fromAddressObj, tokenId, amount],1214 true, // `Unable to burn token from for ${label}`,1215 );1216 const burnedTokens = this.helper.util.extractTokensFromBurnResult(burnResult);1217 return burnedTokens.success && burnedTokens.tokens.length > 0;1218 }12191220 /**1221 * Set, change, or remove approved address to transfer the ownership of the NFT.1222 *1223 * @param signer keyring of signer1224 * @param collectionId ID of collection1225 * @param tokenId ID of token1226 * @param toAddressObj Substrate or Ethereum address which gets approved use of the signer's tokens1227 * @param amount amount of token to be approved. For NFT must be set to 1n1228 * @returns ```true``` if extrinsic success, otherwise ```false```1229 */1230 async approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1231 const approveResult = await this.helper.executeExtrinsic(1232 signer,1233 'api.tx.unique.approve', [toAddressObj, collectionId, tokenId, amount],1234 true, // `Unable to approve token for ${label}`,1235 );12361237 return this.helper.util.findCollectionInEvents(approveResult.result.events, collectionId, 'common', 'Approved');1238 }12391240 /**1241 * Get the amount of token pieces approved to transfer or burn. Normally 0.1242 *1243 * @param collectionId ID of collection1244 * @param tokenId ID of token1245 * @param toAccountObj address which is approved to use token pieces1246 * @param fromAccountObj address which may have allowed the use of its owned tokens1247 * @example getTokenApprovedPieces(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5ERZNF88Mm7UGfPP3mdG..."})1248 * @returns number of approved to transfer pieces1249 */1250 async getTokenApprovedPieces(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId, fromAccountObj: ICrossAccountId): Promise<bigint> {1251 return (await this.helper.callRpc('api.rpc.unique.allowance', [collectionId, fromAccountObj, toAccountObj, tokenId])).toBigInt();1252 }12531254 /**1255 * Get the last created token ID in a collection1256 *1257 * @param collectionId ID of collection1258 * @example getLastTokenId(10);1259 * @returns id of the last created token1260 */1261 async getLastTokenId(collectionId: number): Promise<number> {1262 return (await this.helper.callRpc('api.rpc.unique.lastTokenId', [collectionId])).toNumber();1263 }12641265 /**1266 * Check if token exists1267 *1268 * @param collectionId ID of collection1269 * @param tokenId ID of token1270 * @example doesTokenExist(10, 20);1271 * @returns true if the token exists, otherwise false1272 */1273 async doesTokenExist(collectionId: number, tokenId: number): Promise<boolean> {1274 return (await this.helper.callRpc('api.rpc.unique.tokenExists', [collectionId, tokenId])).toJSON();1275 }1276}12771278class NFTnRFT extends CollectionGroup {1279 /**1280 * Get tokens owned by account1281 *1282 * @param collectionId ID of collection1283 * @param addressObj tokens owner1284 * @example getTokensByAddress(10, {Substrate: "5DyN4Y92vZCjv38fg..."})1285 * @returns array of token ids owned by account1286 */1287 async getTokensByAddress(collectionId: number, addressObj: ICrossAccountId): Promise<number[]> {1288 return (await this.helper.callRpc('api.rpc.unique.accountTokens', [collectionId, addressObj])).toJSON();1289 }12901291 /**1292 * Get token data1293 *1294 * @param collectionId ID of collection1295 * @param tokenId ID of token1296 * @param propertyKeys optionally filter the token properties to only these keys1297 * @param blockHashAt optionally query the data at some block with this hash1298 * @example getToken(10, 5);1299 * @returns human readable token data1300 */1301 async getToken(collectionId: number, tokenId: number, propertyKeys: string[] = [], blockHashAt?: string): Promise<{1302 properties: IProperty[];1303 owner: CrossAccountId;1304 normalizedOwner: CrossAccountId;1305 }| null> {1306 let tokenData;1307 if(typeof blockHashAt === 'undefined') {1308 tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId]);1309 }1310 else {1311 if(propertyKeys.length == 0) {1312 const collection = (await this.helper.callRpc('api.rpc.unique.collectionById', [collectionId])).toHuman();1313 if(!collection) return null;1314 propertyKeys = collection.tokenPropertyPermissions.map((x: ITokenPropertyPermission) => x.key);1315 }1316 tokenData = await this.helper.callRpc('api.rpc.unique.tokenData', [collectionId, tokenId, propertyKeys, blockHashAt]);1317 }1318 tokenData = tokenData.toHuman();1319 if (tokenData === null || tokenData.owner === null) return null;1320 const owner = {} as any;1321 for (const key of Object.keys(tokenData.owner)) {1322 owner[key.toLocaleLowerCase()] = key.toLocaleLowerCase() == 'substrate'1323 ? CrossAccountId.normalizeSubstrateAddress(tokenData.owner[key])1324 : tokenData.owner[key];1325 }1326 tokenData.normalizedOwner = CrossAccountId.fromLowerCaseKeys(owner);1327 return tokenData;1328 }13291330 /**1331 * Set permissions to change token properties1332 *1333 * @param signer keyring of signer1334 * @param collectionId ID of collection1335 * @param permissions permissions to change a property by the collection admin or token owner1336 * @example setTokenPropertyPermissions(1337 * aliceKeyring, 10, [{key: "gender", permission: {tokenOwner: true, mutable: true, collectionAdmin: true}}]1338 * )1339 * @returns true if extrinsic success otherwise false1340 */1341 async setTokenPropertyPermissions(signer: TSigner, collectionId: number, permissions: ITokenPropertyPermission[]): Promise<boolean> {1342 const result = await this.helper.executeExtrinsic(1343 signer,1344 'api.tx.unique.setTokenPropertyPermissions', [collectionId, permissions],1345 true,1346 );13471348 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'PropertyPermissionSet');1349 }13501351 /**1352 * Get token property permissions.1353 *1354 * @param collectionId ID of collection1355 * @param propertyKeys optionally filter the returned property permissions to only these keys1356 * @example getPropertyPermissions(1219, ['location', 'date', 'time', 'isParadise']);1357 * @returns array of key-permission pairs1358 */1359 async getPropertyPermissions(collectionId: number, propertyKeys: string[] | null = null): Promise<ITokenPropertyPermission[]> {1360 return (await this.helper.callRpc('api.rpc.unique.propertyPermissions', [collectionId, ...(propertyKeys === null ? [] : [propertyKeys])])).toHuman();1361 }13621363 /**1364 * Set token properties1365 *1366 * @param signer keyring of signer1367 * @param collectionId ID of collection1368 * @param tokenId ID of token1369 * @param properties key-value pairs of metadata which to add to a token. Keys must be permitted in the collection1370 * @example setTokenProperties(aliceKeyring, 10, 5, [{key: "gender", value: "female"}, {key: "age", value: "23"}])1371 * @returns ```true``` if extrinsic success, otherwise ```false```1372 */1373 async setTokenProperties(signer: TSigner, collectionId: number, tokenId: number, properties: IProperty[]): Promise<boolean> {1374 const result = await this.helper.executeExtrinsic(1375 signer,1376 'api.tx.unique.setTokenProperties', [collectionId, tokenId, properties],1377 true,1378 );13791380 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertySet');1381 }13821383 /**1384 * Get properties, metadata assigned to a token.1385 *1386 * @param collectionId ID of collection1387 * @param tokenId ID of token1388 * @param propertyKeys optionally filter the returned properties to only these keys1389 * @example getTokenProperties(1219, ['location', 'date', 'time', 'isParadise']);1390 * @returns array of key-value pairs1391 */1392 async getTokenProperties(collectionId: number, tokenId: number, propertyKeys?: string[] | null): Promise<IProperty[]> {1393 return (await this.helper.callRpc('api.rpc.unique.tokenProperties', [collectionId, tokenId, propertyKeys])).toHuman();1394 }13951396 /**1397 * Delete the provided properties of a token1398 * @param signer keyring of signer1399 * @param collectionId ID of collection1400 * @param tokenId ID of token1401 * @param propertyKeys property keys to be deleted1402 * @example deleteTokenProperties(aliceKeyring, 10, 5, ["gender", "age"])1403 * @returns ```true``` if extrinsic success, otherwise ```false```1404 */1405 async deleteTokenProperties(signer: TSigner, collectionId: number, tokenId: number, propertyKeys: string[]): Promise<boolean> {1406 const result = await this.helper.executeExtrinsic(1407 signer,1408 'api.tx.unique.deleteTokenProperties', [collectionId, tokenId, propertyKeys],1409 true,1410 );14111412 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'TokenPropertyDeleted');1413 }14141415 /**1416 * Mint new collection1417 *1418 * @param signer keyring of signer1419 * @param collectionOptions basic collection options and properties1420 * @param mode NFT or RFT type of a collection1421 * @example mintCollection(aliceKeyring, {name: 'New', description: "New collection", tokenPrefix: "NEW"}, "NFT")1422 * @returns object of the created collection1423 */1424 async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions, mode: 'NFT' | 'RFT'): Promise<UniqueBaseCollection> {1425 collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1426 collectionOptions.mode = (mode === 'NFT') ? {nft: null} : {refungible: null};1427 for (const key of ['name', 'description', 'tokenPrefix']) {1428 if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1429 }1430 const creationResult = await this.helper.executeExtrinsic(1431 signer,1432 'api.tx.unique.createCollectionEx', [collectionOptions],1433 true, // errorLabel,1434 );1435 return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));1436 }14371438 getCollectionObject(_collectionId: number): any {1439 return null;1440 }14411442 getTokenObject(_collectionId: number, _tokenId: number): any {1443 return null;1444 }14451446 /**1447 * Tells whether the given `owner` approves the `operator`.1448 * @param collectionId ID of collection1449 * @param owner owner address1450 * @param operator operator addrees1451 * @returns true if operator is enabled1452 */1453 async allowanceForAll(collectionId: number, owner: ICrossAccountId, operator: ICrossAccountId): Promise<boolean> {1454 return (await this.helper.callRpc('api.rpc.unique.allowanceForAll', [collectionId, owner, operator])).toJSON();1455 }14561457 /** Sets or unsets the approval of a given operator.1458 * The `operator` is allowed to transfer all tokens of the `caller` on their behalf.1459 * @param operator Operator1460 * @param approved Should operator status be granted or revoked?1461 * @returns ```true``` if extrinsic success, otherwise ```false```1462 */1463 async setAllowanceForAll(signer: TSigner, collectionId: number, operator: ICrossAccountId, approved: boolean): Promise<boolean> {1464 const result = await this.helper.executeExtrinsic(1465 signer,1466 'api.tx.unique.setAllowanceForAll', [collectionId, operator, approved],1467 true,1468 );1469 return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'common', 'ApprovedForAll');1470 }1471}147214731474class NFTGroup extends NFTnRFT {1475 /**1476 * Get collection object1477 * @param collectionId ID of collection1478 * @example getCollectionObject(2);1479 * @returns instance of UniqueNFTCollection1480 */1481 getCollectionObject(collectionId: number): UniqueNFTCollection {1482 return new UniqueNFTCollection(collectionId, this.helper);1483 }14841485 /**1486 * Get token object1487 * @param collectionId ID of collection1488 * @param tokenId ID of token1489 * @example getTokenObject(10, 5);1490 * @returns instance of UniqueNFTToken1491 */1492 getTokenObject(collectionId: number, tokenId: number): UniqueNFToken {1493 return new UniqueNFToken(tokenId, this.getCollectionObject(collectionId));1494 }14951496 /**1497 * Get token's owner1498 * @param collectionId ID of collection1499 * @param tokenId ID of token1500 * @param blockHashAt optionally query the data at the block with this hash1501 * @example getTokenOwner(10, 5);1502 * @returns Address in CrossAccountId format, e.g. {Substrate: "5DnSF6RRjwteE3BrCj..."}1503 */1504 async getTokenOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId> {1505 let owner;1506 if (typeof blockHashAt === 'undefined') {1507 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId]);1508 } else {1509 owner = await this.helper.callRpc('api.rpc.unique.tokenOwner', [collectionId, tokenId, blockHashAt]);1510 }1511 return CrossAccountId.fromLowerCaseKeys(owner.toJSON());1512 }15131514 /**1515 * Is token approved to transfer1516 * @param collectionId ID of collection1517 * @param tokenId ID of token1518 * @param toAccountObj address to be approved1519 * @returns ```true``` if extrinsic success, otherwise ```false```1520 */1521 async isTokenApproved(collectionId: number, tokenId: number, toAccountObj: ICrossAccountId): Promise<boolean> {1522 return (await this.getTokenApprovedPieces(collectionId, tokenId, toAccountObj, await this.getTokenOwner(collectionId, tokenId))) === 1n;1523 }15241525 /**1526 * Changes the owner of the token.1527 *1528 * @param signer keyring of signer1529 * @param collectionId ID of collection1530 * @param tokenId ID of token1531 * @param addressObj address of a new owner1532 * @example transferToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1533 * @returns ```true``` if extrinsic success, otherwise ```false```1534 */1535 async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<boolean> {1536 return await super.transferToken(signer, collectionId, tokenId, addressObj, 1n);1537 }15381539 /**1540 *1541 * Change ownership of a NFT on behalf of the owner.1542 *1543 * @param signer keyring of signer1544 * @param collectionId ID of collection1545 * @param tokenId ID of token1546 * @param fromAddressObj address on behalf of which the token will be sent1547 * @param toAddressObj new token owner1548 * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Ethereum: "0x9F0583DbB85..."})1549 * @returns ```true``` if extrinsic success, otherwise ```false```1550 */1551 async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId): Promise<boolean> {1552 return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, 1n);1553 }15541555 /**1556 * Recursively find the address that owns the token1557 * @param collectionId ID of collection1558 * @param tokenId ID of token1559 * @param blockHashAt1560 * @example getTokenTopmostOwner(10, 5);1561 * @returns address in CrossAccountId format, e.g. {Substrate: "5DyN4Y92vZCjv38fg..."}1562 */1563 async getTokenTopmostOwner(collectionId: number, tokenId: number, blockHashAt?: string): Promise<CrossAccountId | null> {1564 let owner;1565 if (typeof blockHashAt === 'undefined') {1566 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId]);1567 } else {1568 owner = await this.helper.callRpc('api.rpc.unique.topmostTokenOwner', [collectionId, tokenId, blockHashAt]);1569 }15701571 if (owner === null) return null;15721573 return owner.toHuman();1574 }15751576 /**1577 * Get tokens nested in the provided token1578 * @param collectionId ID of collection1579 * @param tokenId ID of token1580 * @param blockHashAt optionally query the data at the block with this hash1581 * @example getTokenChildren(10, 5);1582 * @returns tokens whose depth of nesting is <= 51583 */1584 async getTokenChildren(collectionId: number, tokenId: number, blockHashAt?: string): Promise<IToken[]> {1585 let children;1586 if(typeof blockHashAt === 'undefined') {1587 children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId]);1588 } else {1589 children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId, blockHashAt]);1590 }15911592 return children.toJSON().map((x: any) => {1593 return {collectionId: x.collection, tokenId: x.token};1594 });1595 }15961597 /**1598 * Nest one token into another1599 * @param signer keyring of signer1600 * @param tokenObj token to be nested1601 * @param rootTokenObj token to be parent1602 * @example nestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4});1603 * @returns ```true``` if extrinsic success, otherwise ```false```1604 */1605 async nestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken): Promise<boolean> {1606 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1607 const result = await this.transferToken(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress);1608 if(!result) {1609 throw Error('Unable to nest token!');1610 }1611 return result;1612 }16131614 /**1615 * Remove token from nested state1616 * @param signer keyring of signer1617 * @param tokenObj token to unnest1618 * @param rootTokenObj parent of a token1619 * @param toAddressObj address of a new token owner1620 * @example unnestToken(aliceKeyring, {collectionId: 10, tokenId: 5}, {collectionId: 10, tokenId: 4}, {Substrate: "5DyN4Y92vZCjv38fg..."});1621 * @returns ```true``` if extrinsic success, otherwise ```false```1622 */1623 async unnestToken(signer: TSigner, tokenObj: IToken, rootTokenObj: IToken, toAddressObj: ICrossAccountId): Promise<boolean> {1624 const rootTokenAddress = this.helper.util.getTokenAccount(rootTokenObj);1625 const result = await this.transferTokenFrom(signer, tokenObj.collectionId, tokenObj.tokenId, rootTokenAddress, toAddressObj);1626 if(!result) {1627 throw Error('Unable to unnest token!');1628 }1629 return result;1630 }16311632 /**1633 * Mint new collection1634 * @param signer keyring of signer1635 * @param collectionOptions Collection options1636 * @example1637 * mintCollection(aliceKeyring, {1638 * name: 'New',1639 * description: 'New collection',1640 * tokenPrefix: 'NEW',1641 * })1642 * @returns object of the created collection1643 */1644 async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueNFTCollection> {1645 return await super.mintCollection(signer, collectionOptions, 'NFT') as UniqueNFTCollection;1646 }16471648 /**1649 * Mint new token1650 * @param signer keyring of signer1651 * @param data token data1652 * @returns created token object1653 */1654 async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; properties?: IProperty[]; }): Promise<UniqueNFToken> {1655 const creationResult = await this.helper.executeExtrinsic(1656 signer,1657 'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1658 nft: {1659 properties: data.properties,1660 },1661 }],1662 true,1663 );1664 const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1665 if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1666 if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1667 return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1668 }16691670 /**1671 * Mint multiple NFT tokens1672 * @param signer keyring of signer1673 * @param collectionId ID of collection1674 * @param tokens array of tokens with owner and properties1675 * @example1676 * mintMultipleTokens(aliceKeyring, 10, [{1677 * owner: {Substrate: "5DyN4Y92vZCjv38fg..."},1678 * properties: [{key: "gender", value: "male"},{key: "age", value: "45"}],1679 * },{1680 * owner: {Ethereum: "0x9F0583DbB855d..."},1681 * properties: [{key: "gender", value: "female"},{key: "age", value: "22"}],1682 * }]);1683 * @returns ```true``` if extrinsic success, otherwise ```false```1684 */1685 async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1686 const creationResult = await this.helper.executeExtrinsic(1687 signer,1688 'api.tx.unique.createMultipleItemsEx', [collectionId, {NFT: tokens}],1689 true,1690 );1691 const collection = this.getCollectionObject(collectionId);1692 return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1693 }16941695 /**1696 * Mint multiple NFT tokens with one owner1697 * @param signer keyring of signer1698 * @param collectionId ID of collection1699 * @param owner tokens owner1700 * @param tokens array of tokens with owner and properties1701 * @example1702 * mintMultipleTokensWithOneOwner(aliceKeyring, 10, "5DyN4Y92vZCjv38fg...", [{1703 * properties: [{1704 * key: "gender",1705 * value: "female",1706 * },{1707 * key: "age",1708 * value: "33",1709 * }],1710 * }]);1711 * @returns array of newly created tokens1712 */1713 async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {properties?: IProperty[]}[]): Promise<UniqueNFToken[]> {1714 const rawTokens = [];1715 for (const token of tokens) {1716 const raw = {NFT: {properties: token.properties}};1717 rawTokens.push(raw);1718 }1719 const creationResult = await this.helper.executeExtrinsic(1720 signer,1721 'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1722 true,1723 );1724 const collection = this.getCollectionObject(collectionId);1725 return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1726 }17271728 /**1729 * Set, change, or remove approved address to transfer the ownership of the NFT.1730 *1731 * @param signer keyring of signer1732 * @param collectionId ID of collection1733 * @param tokenId ID of token1734 * @param toAddressObj address to approve1735 * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."})1736 * @returns ```true``` if extrinsic success, otherwise ```false```1737 */1738 approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId) {1739 return super.approveToken(signer, collectionId, tokenId, toAddressObj, 1n);1740 }1741}174217431744class RFTGroup extends NFTnRFT {1745 /**1746 * Get collection object1747 * @param collectionId ID of collection1748 * @example getCollectionObject(2);1749 * @returns instance of UniqueRFTCollection1750 */1751 getCollectionObject(collectionId: number): UniqueRFTCollection {1752 return new UniqueRFTCollection(collectionId, this.helper);1753 }17541755 /**1756 * Get token object1757 * @param collectionId ID of collection1758 * @param tokenId ID of token1759 * @example getTokenObject(10, 5);1760 * @returns instance of UniqueNFTToken1761 */1762 getTokenObject(collectionId: number, tokenId: number): UniqueRFToken {1763 return new UniqueRFToken(tokenId, this.getCollectionObject(collectionId));1764 }17651766 /**1767 * Get top 10 token owners with the largest number of pieces1768 * @param collectionId ID of collection1769 * @param tokenId ID of token1770 * @example getTokenTop10Owners(10, 5);1771 * @returns array of top 10 owners1772 */1773 async getTokenTop10Owners(collectionId: number, tokenId: number): Promise<CrossAccountId[]> {1774 return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, tokenId])).toJSON().map(CrossAccountId.fromLowerCaseKeys);1775 }17761777 /**1778 * Get number of pieces owned by address1779 * @param collectionId ID of collection1780 * @param tokenId ID of token1781 * @param addressObj address token owner1782 * @example getTokenBalance(10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."});1783 * @returns number of pieces ownerd by address1784 */1785 async getTokenBalance(collectionId: number, tokenId: number, addressObj: ICrossAccountId): Promise<bigint> {1786 return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, tokenId])).toBigInt();1787 }17881789 /**1790 * Transfer pieces of token to another address1791 * @param signer keyring of signer1792 * @param collectionId ID of collection1793 * @param tokenId ID of token1794 * @param addressObj address of a new owner1795 * @param amount number of pieces to be transfered1796 * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2000n)1797 * @returns ```true``` if extrinsic success, otherwise ```false```1798 */1799 async transferToken(signer: TSigner, collectionId: number, tokenId: number, addressObj: ICrossAccountId, amount=1n): Promise<boolean> {1800 return await super.transferToken(signer, collectionId, tokenId, addressObj, amount);1801 }18021803 /**1804 * Change ownership of some pieces of RFT on behalf of the owner.1805 * @param signer keyring of signer1806 * @param collectionId ID of collection1807 * @param tokenId ID of token1808 * @param fromAddressObj address on behalf of which the token will be sent1809 * @param toAddressObj new token owner1810 * @param amount number of pieces to be transfered1811 * @example transferTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, {Substrate: "5DfhbVfww7ThF8q6f3i..."}, 2000n)1812 * @returns ```true``` if extrinsic success, otherwise ```false```1813 */1814 async transferTokenFrom(signer: TSigner, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1815 return await super.transferTokenFrom(signer, collectionId, tokenId, fromAddressObj, toAddressObj, amount);1816 }18171818 /**1819 * Mint new collection1820 * @param signer keyring of signer1821 * @param collectionOptions Collection options1822 * @example1823 * mintCollection(aliceKeyring, {1824 * name: 'New',1825 * description: 'New collection',1826 * tokenPrefix: 'NEW',1827 * })1828 * @returns object of the created collection1829 */1830 async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}): Promise<UniqueRFTCollection> {1831 return await super.mintCollection(signer, collectionOptions, 'RFT') as UniqueRFTCollection;1832 }18331834 /**1835 * Mint new token1836 * @param signer keyring of signer1837 * @param data token data1838 * @example mintToken(aliceKeyring, {collectionId: 10, owner: {Substrate: '5GHoZe9c73RYbVzq...'}, pieces: 10000n});1839 * @returns created token object1840 */1841 async mintToken(signer: TSigner, data: { collectionId: number; owner: ICrossAccountId | string; pieces: bigint; properties?: IProperty[]; }): Promise<UniqueRFToken> {1842 const creationResult = await this.helper.executeExtrinsic(1843 signer,1844 'api.tx.unique.createItem', [data.collectionId, (typeof data.owner === 'string') ? {Substrate: data.owner} : data.owner, {1845 refungible: {1846 pieces: data.pieces,1847 properties: data.properties,1848 },1849 }],1850 true,1851 );1852 const createdTokens = this.helper.util.extractTokensFromCreationResult(creationResult);1853 if (createdTokens.tokens.length > 1) throw Error('Minted multiple tokens');1854 if (createdTokens.tokens.length < 1) throw Error('No tokens minted');1855 return this.getTokenObject(data.collectionId, createdTokens.tokens[0].tokenId);1856 }18571858 async mintMultipleTokens(signer: TSigner, collectionId: number, tokens: {owner: ICrossAccountId, pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1859 throw Error('Not implemented');1860 const creationResult = await this.helper.executeExtrinsic(1861 signer,1862 'api.tx.unique.createMultipleItemsEx', [collectionId, {RefungibleMultipleOwners: tokens}],1863 true, // `Unable to mint RFT tokens for ${label}`,1864 );1865 const collection = this.getCollectionObject(collectionId);1866 return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1867 }18681869 /**1870 * Mint multiple RFT tokens with one owner1871 * @param signer keyring of signer1872 * @param collectionId ID of collection1873 * @param owner tokens owner1874 * @param tokens array of tokens with properties and pieces1875 * @example mintMultipleTokensWithOneOwner(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, [{pieces: 100000n, properties: [{key: "gender", value: "male"}]}]);1876 * @returns array of newly created RFT tokens1877 */1878 async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, owner: ICrossAccountId, tokens: {pieces: bigint, properties?: IProperty[]}[]): Promise<UniqueRFToken[]> {1879 const rawTokens = [];1880 for (const token of tokens) {1881 const raw = {ReFungible: {pieces: token.pieces, properties: token.properties}};1882 rawTokens.push(raw);1883 }1884 const creationResult = await this.helper.executeExtrinsic(1885 signer,1886 'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],1887 true,1888 );1889 const collection = this.getCollectionObject(collectionId);1890 return this.helper.util.extractTokensFromCreationResult(creationResult).tokens.map((x: IToken) => collection.getTokenObject(x.tokenId));1891 }18921893 /**1894 * Destroys a concrete instance of RFT.1895 * @param signer keyring of signer1896 * @param collectionId ID of collection1897 * @param tokenId ID of token1898 * @param amount number of pieces to be burnt1899 * @example burnToken(aliceKeyring, 10, 5);1900 * @returns ```true``` if the extrinsic is successful, otherwise ```false```1901 */1902 async burnToken(signer: IKeyringPair, collectionId: number, tokenId: number, amount=1n): Promise<boolean> {1903 return await super.burnToken(signer, collectionId, tokenId, amount);1904 }19051906 /**1907 * Destroys a concrete instance of RFT on behalf of the owner.1908 * @param signer keyring of signer1909 * @param collectionId ID of collection1910 * @param tokenId ID of token1911 * @param fromAddressObj address on behalf of which the token will be burnt1912 * @param amount number of pieces to be burnt1913 * @example burnTokenFrom(aliceKeyring, 10, 5, {Substrate: "5DyN4Y92vZCjv38fg..."}, 2n)1914 * @returns ```true``` if extrinsic success, otherwise ```false```1915 */1916 async burnTokenFrom(signer: IKeyringPair, collectionId: number, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {1917 return await super.burnTokenFrom(signer, collectionId, tokenId, fromAddressObj, amount);1918 }19191920 /**1921 * Set, change, or remove approved address to transfer the ownership of the RFT.1922 *1923 * @param signer keyring of signer1924 * @param collectionId ID of collection1925 * @param tokenId ID of token1926 * @param toAddressObj address to approve1927 * @param amount number of pieces to be approved1928 * @example approveToken(aliceKeyring, 10, 5, {Substrate: "5GHoZe9c73RYbVzq..."}, "", 10000n);1929 * @returns true if the token success, otherwise false1930 */1931 approveToken(signer: IKeyringPair, collectionId: number, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {1932 return super.approveToken(signer, collectionId, tokenId, toAddressObj, amount);1933 }19341935 /**1936 * Get total number of pieces1937 * @param collectionId ID of collection1938 * @param tokenId ID of token1939 * @example getTokenTotalPieces(10, 5);1940 * @returns number of pieces1941 */1942 async getTokenTotalPieces(collectionId: number, tokenId: number): Promise<bigint> {1943 return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, tokenId])).unwrap().toBigInt();1944 }19451946 /**1947 * Change number of token pieces. Signer must be the owner of all token pieces.1948 * @param signer keyring of signer1949 * @param collectionId ID of collection1950 * @param tokenId ID of token1951 * @param amount new number of pieces1952 * @example repartitionToken(aliceKeyring, 10, 5, 12345n);1953 * @returns true if the repartion was success, otherwise false1954 */1955 async repartitionToken(signer: TSigner, collectionId: number, tokenId: number, amount: bigint): Promise<boolean> {1956 const currentAmount = await this.getTokenTotalPieces(collectionId, tokenId);1957 const repartitionResult = await this.helper.executeExtrinsic(1958 signer,1959 'api.tx.unique.repartition', [collectionId, tokenId, amount],1960 true,1961 );1962 if(currentAmount < amount) return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemCreated');1963 return this.helper.util.findCollectionInEvents(repartitionResult.result.events, collectionId, 'common', 'ItemDestroyed');1964 }1965}196619671968class FTGroup extends CollectionGroup {1969 /**1970 * Get collection object1971 * @param collectionId ID of collection1972 * @example getCollectionObject(2);1973 * @returns instance of UniqueFTCollection1974 */1975 getCollectionObject(collectionId: number): UniqueFTCollection {1976 return new UniqueFTCollection(collectionId, this.helper);1977 }19781979 /**1980 * Mint new fungible collection1981 * @param signer keyring of signer1982 * @param collectionOptions Collection options1983 * @param decimalPoints number of token decimals1984 * @example1985 * mintCollection(aliceKeyring, {1986 * name: 'New',1987 * description: 'New collection',1988 * tokenPrefix: 'NEW',1989 * }, 18)1990 * @returns newly created fungible collection1991 */1992 async mintCollection(signer: TSigner, collectionOptions: ICollectionCreationOptions = {}, decimalPoints = 0): Promise<UniqueFTCollection> {1993 collectionOptions = JSON.parse(JSON.stringify(collectionOptions)) as ICollectionCreationOptions; // Clone object1994 if(collectionOptions.tokenPropertyPermissions) throw Error('Fungible collections has no tokenPropertyPermissions');1995 collectionOptions.mode = {fungible: decimalPoints};1996 for (const key of ['name', 'description', 'tokenPrefix']) {1997 if (typeof collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] === 'string') collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] = this.helper.util.str2vec(collectionOptions[key as 'name' | 'description' | 'tokenPrefix'] as string);1998 }1999 const creationResult = await this.helper.executeExtrinsic(2000 signer,2001 'api.tx.unique.createCollectionEx', [collectionOptions],2002 true,2003 );2004 return this.getCollectionObject(this.helper.util.extractCollectionIdFromCreationResult(creationResult));2005 }20062007 /**2008 * Mint tokens2009 * @param signer keyring of signer2010 * @param collectionId ID of collection2011 * @param owner address owner of new tokens2012 * @param amount amount of tokens to be meanted2013 * @example mintTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq"}, 1000n);2014 * @returns ```true``` if extrinsic success, otherwise ```false```2015 */2016 async mintTokens(signer: TSigner, collectionId: number, amount: bigint, owner: ICrossAccountId | string): Promise<boolean> {2017 const creationResult = await this.helper.executeExtrinsic(2018 signer,2019 'api.tx.unique.createItem', [collectionId, (typeof owner === 'string') ? {Substrate: owner} : owner, {2020 fungible: {2021 value: amount,2022 },2023 }],2024 true, // `Unable to mint fungible tokens for ${label}`,2025 );2026 return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');2027 }20282029 /**2030 * Mint multiple Fungible tokens with one owner2031 * @param signer keyring of signer2032 * @param collectionId ID of collection2033 * @param owner tokens owner2034 * @param tokens array of tokens with properties and pieces2035 * @returns ```true``` if extrinsic success, otherwise ```false```2036 */2037 async mintMultipleTokensWithOneOwner(signer: TSigner, collectionId: number, tokens: {value: bigint}[], owner: ICrossAccountId): Promise<boolean> {2038 const rawTokens = [];2039 for (const token of tokens) {2040 const raw = {Fungible: {Value: token.value}};2041 rawTokens.push(raw);2042 }2043 const creationResult = await this.helper.executeExtrinsic(2044 signer,2045 'api.tx.unique.createMultipleItems', [collectionId, owner, rawTokens],2046 true,2047 );2048 return this.helper.util.findCollectionInEvents(creationResult.result.events, collectionId, 'common', 'ItemCreated');2049 }20502051 /**2052 * Get the top 10 owners with the largest balance for the Fungible collection2053 * @param collectionId ID of collection2054 * @example getTop10Owners(10);2055 * @returns array of ```ICrossAccountId```2056 */2057 async getTop10Owners(collectionId: number): Promise<CrossAccountId[]> {2058 return (await this.helper.callRpc('api.rpc.unique.tokenOwners', [collectionId, 0])).toJSON().map(CrossAccountId.fromLowerCaseKeys);2059 }20602061 /**2062 * Get account balance2063 * @param collectionId ID of collection2064 * @param addressObj address of owner2065 * @example getBalance(10, {Substrate: "5GHoZe9c73RYbVzq..."})2066 * @returns amount of fungible tokens owned by address2067 */2068 async getBalance(collectionId: number, addressObj: ICrossAccountId): Promise<bigint> {2069 return (await this.helper.callRpc('api.rpc.unique.balance', [collectionId, addressObj, 0])).toBigInt();2070 }20712072 /**2073 * Transfer tokens to address2074 * @param signer keyring of signer2075 * @param collectionId ID of collection2076 * @param toAddressObj address recipient2077 * @param amount amount of tokens to be sent2078 * @example transfer(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);2079 * @returns ```true``` if extrinsic success, otherwise ```false```2080 */2081 async transfer(signer: TSigner, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {2082 return await super.transferToken(signer, collectionId, 0, toAddressObj, amount);2083 }20842085 /**2086 * Transfer some tokens on behalf of the owner.2087 * @param signer keyring of signer2088 * @param collectionId ID of collection2089 * @param fromAddressObj address on behalf of which tokens will be sent2090 * @param toAddressObj address where token to be sent2091 * @param amount number of tokens to be sent2092 * @example transferFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, {Substrate: "5DfhbVfww7ThF8q6f3ij..."}, 10000n);2093 * @returns ```true``` if extrinsic success, otherwise ```false```2094 */2095 async transferFrom(signer: TSigner, collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {2096 return await super.transferTokenFrom(signer, collectionId, 0, fromAddressObj, toAddressObj, amount);2097 }20982099 /**2100 * Destroy some amount of tokens2101 * @param signer keyring of signer2102 * @param collectionId ID of collection2103 * @param amount amount of tokens to be destroyed2104 * @example burnTokens(aliceKeyring, 10, 1000n);2105 * @returns ```true``` if extrinsic success, otherwise ```false```2106 */2107 async burnTokens(signer: IKeyringPair, collectionId: number, amount=1n): Promise<boolean> {2108 return await super.burnToken(signer, collectionId, 0, amount);2109 }21102111 /**2112 * Burn some tokens on behalf of the owner.2113 * @param signer keyring of signer2114 * @param collectionId ID of collection2115 * @param fromAddressObj address on behalf of which tokens will be burnt2116 * @param amount amount of tokens to be burnt2117 * @example burnTokensFrom(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n);2118 * @returns ```true``` if extrinsic success, otherwise ```false```2119 */2120 async burnTokensFrom(signer: IKeyringPair, collectionId: number, fromAddressObj: ICrossAccountId, amount=1n): Promise<boolean> {2121 return await super.burnTokenFrom(signer, collectionId, 0, fromAddressObj, amount);2122 }21232124 /**2125 * Get total collection supply2126 * @param collectionId2127 * @returns2128 */2129 async getTotalPieces(collectionId: number): Promise<bigint> {2130 return (await this.helper.callRpc('api.rpc.unique.totalPieces', [collectionId, 0])).unwrap().toBigInt();2131 }21322133 /**2134 * Set, change, or remove approved address to transfer tokens.2135 *2136 * @param signer keyring of signer2137 * @param collectionId ID of collection2138 * @param toAddressObj address to be approved2139 * @param amount amount of tokens to be approved2140 * @example approveTokens(aliceKeyring, 10, {Substrate: "5GHoZe9c73RYbVzq..."}, 1000n)2141 * @returns ```true``` if extrinsic success, otherwise ```false```2142 */2143 approveTokens(signer: IKeyringPair, collectionId: number, toAddressObj: ICrossAccountId, amount=1n) {2144 return super.approveToken(signer, collectionId, 0, toAddressObj, amount);2145 }21462147 /**2148 * Get amount of fungible tokens approved to transfer2149 * @param collectionId ID of collection2150 * @param fromAddressObj owner of tokens2151 * @param toAddressObj the address approved for the transfer of tokens on behalf of the owner2152 * @returns number of tokens approved for the transfer2153 */2154 getApprovedTokens(collectionId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {2155 return super.getTokenApprovedPieces(collectionId, 0, toAddressObj, fromAddressObj);2156 }2157}215821592160class ChainGroup extends HelperGroup<ChainHelperBase> {2161 /**2162 * Get system properties of a chain2163 * @example getChainProperties();2164 * @returns ss58Format, token decimals, and token symbol2165 */2166 getChainProperties(): IChainProperties {2167 const properties = (this.helper.getApi() as any).registry.getChainProperties().toJSON();2168 return {2169 ss58Format: properties.ss58Format.toJSON(),2170 tokenDecimals: properties.tokenDecimals.toJSON(),2171 tokenSymbol: properties.tokenSymbol.toJSON(),2172 };2173 }21742175 /**2176 * Get chain header2177 * @example getLatestBlockNumber();2178 * @returns the number of the last block2179 */2180 async getLatestBlockNumber(): Promise<number> {2181 return (await this.helper.callRpc('api.rpc.chain.getHeader')).number.toNumber();2182 }21832184 /**2185 * Get block hash by block number2186 * @param blockNumber number of block2187 * @example getBlockHashByNumber(12345);2188 * @returns hash of a block2189 */2190 async getBlockHashByNumber(blockNumber: number): Promise<string | null> {2191 const blockHash = (await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])).toJSON();2192 if(blockHash === '0x0000000000000000000000000000000000000000000000000000000000000000') return null;2193 return blockHash;2194 }21952196 // TODO add docs2197 async getBlock(blockHashOrNumber: string | number): Promise<IBlock | null> {2198 const blockHash = typeof blockHashOrNumber === 'string' ? blockHashOrNumber : await this.getBlockHashByNumber(blockHashOrNumber);2199 if (!blockHash) return null;2200 return (await this.helper.callRpc('api.rpc.chain.getBlock', [blockHash])).toHuman().block;2201 }22022203 /**2204 * Get account nonce2205 * @param address substrate address2206 * @example getNonce("5GrwvaEF5zXb26Fz...");2207 * @returns number, account's nonce2208 */2209 async getNonce(address: TSubstrateAccount): Promise<number> {2210 return (await this.helper.callRpc('api.query.system.account', [address])).nonce.toNumber();2211 }2212}22132214class SubstrateBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {2215 /**2216 * Get substrate address balance2217 * @param address substrate address2218 * @example getSubstrate("5GrwvaEF5zXb26Fz...")2219 * @returns amount of tokens on address2220 */2221 async getSubstrate(address: TSubstrateAccount): Promise<bigint> {2222 return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt();2223 }22242225 /**2226 * Transfer tokens to substrate address2227 * @param signer keyring of signer2228 * @param address substrate address of a recipient2229 * @param amount amount of tokens to be transfered2230 * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n);2231 * @returns ```true``` if extrinsic success, otherwise ```false```2232 */2233 async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {2234 const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true/*, `Unable to transfer balance from ${this.helper.getSignerAddress(signer)} to ${address}`*/);22352236 let transfer = {from: null, to: null, amount: 0n} as any;2237 result.result.events.forEach(({event: {data, method, section}}) => {2238 if ((section === 'balances') && (method === 'Transfer')) {2239 transfer = {2240 from: this.helper.address.normalizeSubstrate(data[0]),2241 to: this.helper.address.normalizeSubstrate(data[1]),2242 amount: BigInt(data[2]),2243 };2244 }2245 });2246 const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from2247 && this.helper.address.normalizeSubstrate(address) === transfer.to2248 && BigInt(amount) === transfer.amount;2249 return isSuccess;2250 }22512252 /**2253 * Get full substrate balance including free, miscFrozen, feeFrozen, and reserved2254 * @param address substrate address2255 * @returns2256 */2257 async getSubstrateFull(address: TSubstrateAccount): Promise<ISubstrateBalance> {2258 const accountInfo = (await this.helper.callRpc('api.query.system.account', [address])).data;2259 return {free: accountInfo.free.toBigInt(), miscFrozen: accountInfo.miscFrozen.toBigInt(), feeFrozen: accountInfo.feeFrozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()};2260 }2261}22622263class EthereumBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {2264 /**2265 * Get ethereum address balance2266 * @param address ethereum address2267 * @example getEthereum("0x9F0583DbB855d...")2268 * @returns amount of tokens on address2269 */2270 async getEthereum(address: TEthereumAccount): Promise<bigint> {2271 return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt();2272 }22732274 /**2275 * Transfer tokens to address2276 * @param signer keyring of signer2277 * @param address Ethereum address of a recipient2278 * @param amount amount of tokens to be transfered2279 * @example transferToEthereum(alithKeyring, "0x9F0583DbB855d...", 100_000_000_000n);2280 * @returns ```true``` if extrinsic success, otherwise ```false```2281 */2282 async transferToEthereum(signer: TSigner, address: TEthereumAccount, amount: bigint | string): Promise<boolean> {2283 const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true);22842285 let transfer = {from: null, to: null, amount: 0n} as any;2286 result.result.events.forEach(({event: {data, method, section}}) => {2287 if ((section === 'balances') && (method === 'Transfer')) {2288 transfer = {2289 from: data[0].toString(),2290 to: data[1].toString(),2291 amount: BigInt(data[2]),2292 };2293 }2294 });2295 const isSuccess = (typeof signer === 'string' ? signer : signer.address) === transfer.from2296 && address === transfer.to2297 && BigInt(amount) === transfer.amount;2298 return isSuccess;2299 }2300}23012302class BalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {2303 subBalanceGroup: SubstrateBalanceGroup<T>;2304 ethBalanceGroup: EthereumBalanceGroup<T>;23052306 constructor(helper: T) {2307 super(helper);2308 this.subBalanceGroup = new SubstrateBalanceGroup(helper);2309 this.ethBalanceGroup = new EthereumBalanceGroup(helper);2310 }23112312 getCollectionCreationPrice(): bigint {2313 return 2n * this.getOneTokenNominal();2314 }2315 /**2316 * Representation of the native token in the smallest unit - one OPAL (OPL), QUARTZ (QTZ), or UNIQUE (UNQ).2317 * @example getOneTokenNominal()2318 * @returns ```BigInt``` representation of the native token in the smallest unit, e.g. ```1_000_000_000_000_000_000n``` for QTZ.2319 */2320 getOneTokenNominal(): bigint {2321 const chainProperties = this.helper.chain.getChainProperties();2322 return 10n ** BigInt((chainProperties.tokenDecimals || [18])[0]);2323 }23242325 /**2326 * Get substrate address balance2327 * @param address substrate address2328 * @example getSubstrate("5GrwvaEF5zXb26Fz...")2329 * @returns amount of tokens on address2330 */2331 getSubstrate(address: TSubstrateAccount): Promise<bigint> {2332 return this.subBalanceGroup.getSubstrate(address);2333 }23342335 /**2336 * Get full substrate balance including free, miscFrozen, feeFrozen, and reserved2337 * @param address substrate address2338 * @returns2339 */2340 getSubstrateFull(address: TSubstrateAccount): Promise<ISubstrateBalance> {2341 return this.subBalanceGroup.getSubstrateFull(address);2342 }23432344 /**2345 * Get ethereum address balance2346 * @param address ethereum address2347 * @example getEthereum("0x9F0583DbB855d...")2348 * @returns amount of tokens on address2349 */2350 getEthereum(address: TEthereumAccount): Promise<bigint> {2351 return this.ethBalanceGroup.getEthereum(address);2352 }23532354 /**2355 * Transfer tokens to substrate address2356 * @param signer keyring of signer2357 * @param address substrate address of a recipient2358 * @param amount amount of tokens to be transfered2359 * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n);2360 * @returns ```true``` if extrinsic success, otherwise ```false```2361 */2362 transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {2363 return this.subBalanceGroup.transferToSubstrate(signer, address, amount);2364 }23652366 async forceTransferToSubstrate(signer: TSigner, from: TSubstrateAccount, to: TSubstrateAccount, amount: bigint | string): Promise<boolean> {2367 const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.forceTransfer', [from, to, amount], true);23682369 let transfer = {from: null, to: null, amount: 0n} as any;2370 result.result.events.forEach(({event: {data, method, section}}) => {2371 if ((section === 'balances') && (method === 'Transfer')) {2372 transfer = {2373 from: this.helper.address.normalizeSubstrate(data[0]),2374 to: this.helper.address.normalizeSubstrate(data[1]),2375 amount: BigInt(data[2]),2376 };2377 }2378 });2379 let isSuccess = this.helper.address.normalizeSubstrate(from) === transfer.from;2380 isSuccess = isSuccess && this.helper.address.normalizeSubstrate(to) === transfer.to;2381 isSuccess = isSuccess && BigInt(amount) === transfer.amount;2382 return isSuccess;2383 }2384}23852386class AddressGroup extends HelperGroup<ChainHelperBase> {2387 /**2388 * Normalizes the address to the specified ss58 format, by default ```42```.2389 * @param address substrate address2390 * @param ss58Format format for address conversion, by default ```42```2391 * @example normalizeSubstrate("unjKJQJrRd238pkUZZvzDQrfKuM39zBSnQ5zjAGAGcdRhaJTx") // returns 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY2392 * @returns substrate address converted to normalized (i.e., starting with 5) or specified explicitly representation2393 */2394 normalizeSubstrate(address: TSubstrateAccount, ss58Format = 42): TSubstrateAccount {2395 return CrossAccountId.normalizeSubstrateAddress(address, ss58Format);2396 }23972398 /**2399 * Get address in the connected chain format2400 * @param address substrate address2401 * @example normalizeSubstrateToChainFormat("5GrwvaEF5zXb26Fz...") // returns unjKJQJrRd238pkUZZ... for Unique Network2402 * @returns address in chain format2403 */2404 normalizeSubstrateToChainFormat(address: TSubstrateAccount): TSubstrateAccount {2405 return this.normalizeSubstrate(address, this.helper.chain.getChainProperties().ss58Format);2406 }24072408 /**2409 * Get substrate mirror of an ethereum address2410 * @param ethAddress ethereum address2411 * @param toChainFormat false for normalized account2412 * @example ethToSubstrate('0x9F0583DbB855d...')2413 * @returns substrate mirror of a provided ethereum address2414 */2415 ethToSubstrate(ethAddress: TEthereumAccount, toChainFormat=false): TSubstrateAccount {2416 return CrossAccountId.translateEthToSub(ethAddress, toChainFormat ? this.helper.chain.getChainProperties().ss58Format : undefined);2417 }24182419 /**2420 * Get ethereum mirror of a substrate address2421 * @param subAddress substrate account2422 * @example substrateToEth("5DnSF6RRjwteE3BrC...")2423 * @returns ethereum mirror of a provided substrate address2424 */2425 substrateToEth(subAddress: TSubstrateAccount): TEthereumAccount {2426 return CrossAccountId.translateSubToEth(subAddress);2427 }24282429 /**2430 * Encode key to substrate address2431 * @param key key for encoding address2432 * @param ss58Format prefix for encoding to the address of the corresponding network2433 * @returns encoded substrate address2434 */2435 encodeSubstrateAddress (key: Uint8Array | string | bigint, ss58Format = 42): string {2436 const u8a :Uint8Array = typeof key === 'string'2437 ? hexToU8a(key)2438 : typeof key === 'bigint'2439 ? hexToU8a(key.toString(16))2440 : key;2441 2442 if (ss58Format < 0 || ss58Format > 16383 || [46, 47].includes(ss58Format)) {2443 throw new Error(`ss58Format is not valid, received ${typeofss58Format} "${ss58Format}"`);2444 }2445 2446 const allowedDecodedLengths = [1, 2, 4, 8, 32, 33];2447 if (!allowedDecodedLengths.includes(u8a.length)) {2448 throw new Error(`key length is not valid, received ${u8a.length}, valid values are ${allowedDecodedLengths.join(', ')}`);2449 }2450 2451 const u8aPrefix = ss58Format < 642452 ? new Uint8Array([ss58Format])2453 : new Uint8Array([2454 ((ss58Format & 0xfc) >> 2) | 0x40,2455 (ss58Format >> 8) | ((ss58Format & 0x03) << 6),2456 ]);24572458 const input = u8aConcat(u8aPrefix, u8a);2459 2460 return base58Encode(u8aConcat(2461 input,2462 blake2AsU8a(input).subarray(0, [32, 33].includes(u8a.length) ? 2 : 1),2463 ));2464 }24652466 /**2467 * Restore substrate address from bigint representation2468 * @param number decimal representation of substrate address2469 * @returns substrate address2470 */2471 restoreCrossAccountFromBigInt(number: bigint): TSubstrateAccount {2472 if (this.helper.api === null) {2473 throw 'Not connected';2474 }2475 const res = this.helper.api.registry.createType('AccountId', '0x' + number.toString(16).padStart(64, '0')).toJSON();2476 if (res === undefined || res === null) {2477 throw 'Restore address error';2478 }2479 return res.toString();2480 }24812482 /**2483 * Convert etherium cross account id to substrate cross account id2484 * @param ethCrossAccount etherium cross account2485 * @returns substrate cross account id2486 */2487 convertCrossAccountFromEthCrossAccount(ethCrossAccount: IEthCrossAccountId): ICrossAccountId {2488 if (ethCrossAccount.sub === '0') {2489 return {Ethereum: ethCrossAccount.eth.toLocaleLowerCase()};2490 }2491 2492 const ss58 = this.restoreCrossAccountFromBigInt(BigInt(ethCrossAccount.sub));2493 return {Substrate: ss58};2494 }24952496 paraSiblingSovereignAccount(paraid: number) {2497 // We are getting a *sibling* parachain sovereign account,2498 // so we need a sibling prefix: encoded(b"sibl") == 0x7369626c2499 const siblingPrefix = '0x7369626c';25002501 const encodedParaId = this.helper.getApi().createType('u32', paraid).toHex(true).substring(2);2502 const suffix = '000000000000000000000000000000000000000000000000';25032504 return siblingPrefix + encodedParaId + suffix;2505 }2506}25072508class StakingGroup extends HelperGroup<UniqueHelper> {2509 /**2510 * Stake tokens for App Promotion2511 * @param signer keyring of signer2512 * @param amountToStake amount of tokens to stake2513 * @param label extra label for log2514 * @returns2515 */2516 async stake(signer: TSigner, amountToStake: bigint, label?: string): Promise<boolean> {2517 if(typeof label === 'undefined') label = `${signer.address} amount: ${amountToStake}`;2518 const _stakeResult = await this.helper.executeExtrinsic(2519 signer, 'api.tx.appPromotion.stake',2520 [amountToStake], true,2521 );2522 // TODO extract info from stakeResult2523 return true;2524 }25252526 /**2527 * Unstake tokens for App Promotion2528 * @param signer keyring of signer2529 * @param amountToUnstake amount of tokens to unstake2530 * @param label extra label for log2531 * @returns block number where balances will be unlocked2532 */2533 async unstake(signer: TSigner, label?: string): Promise<number> {2534 if(typeof label === 'undefined') label = `${signer.address}`;2535 const _unstakeResult = await this.helper.executeExtrinsic(2536 signer, 'api.tx.appPromotion.unstake',2537 [], true,2538 );2539 // TODO extract block number fron events2540 return 1;2541 }25422543 /**2544 * Get total staked amount for address2545 * @param address substrate or ethereum address2546 * @returns total staked amount2547 */2548 async getTotalStaked(address?: ICrossAccountId): Promise<bigint> {2549 if (address) return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked', [address])).toBigInt();2550 return (await this.helper.callRpc('api.rpc.appPromotion.totalStaked')).toBigInt();2551 }25522553 /**2554 * Get total staked per block2555 * @param address substrate or ethereum address2556 * @returns array of stakes. `block` – the number of the block in which the stake was made. `amount` - the number of tokens staked in the block2557 */2558 async getTotalStakedPerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2559 const rawTotalStakerdPerBlock = await this.helper.callRpc('api.rpc.appPromotion.totalStakedPerBlock', [address]);2560 return rawTotalStakerdPerBlock.map(([block, amount]: any[]) => {2561 return {2562 block: block.toBigInt(),2563 amount: amount.toBigInt(),2564 };2565 });2566 }25672568 /**2569 * Get total pending unstake amount for address2570 * @param address substrate or ethereum address2571 * @returns total pending unstake amount2572 */2573 async getPendingUnstake(address: ICrossAccountId): Promise<bigint> {2574 return (await this.helper.callRpc('api.rpc.appPromotion.pendingUnstake', [address])).toBigInt();2575 }25762577 /**2578 * Get pending unstake amount per block for address2579 * @param address substrate or ethereum address2580 * @returns array of pending stakes. `block` – the number of the block in which the unstake was made. `amount` - the number of tokens unstaked in the block2581 */2582 async getPendingUnstakePerBlock(address: ICrossAccountId): Promise<IStakingInfo[]> {2583 const rawUnstakedPerBlock = await this.helper.callRpc('api.rpc.appPromotion.pendingUnstakePerBlock', [address]);2584 const result = rawUnstakedPerBlock.map(([block, amount]: any[]) => {2585 return {2586 block: block.toBigInt(),2587 amount: amount.toBigInt(),2588 };2589 });2590 return result;2591 }2592}25932594class SchedulerGroup extends HelperGroup<UniqueHelper> {2595 constructor(helper: UniqueHelper) {2596 super(helper);2597 }25982599 cancelScheduled(signer: TSigner, scheduledId: string) {2600 return this.helper.executeExtrinsic(2601 signer,2602 'api.tx.scheduler.cancelNamed',2603 [scheduledId],2604 true,2605 );2606 }26072608 changePriority(signer: TSigner, scheduledId: string, priority: number) {2609 return this.helper.executeExtrinsic(2610 signer,2611 'api.tx.scheduler.changeNamedPriority',2612 [scheduledId, priority],2613 true,2614 );2615 }26162617 scheduleAt<T extends UniqueHelper>(2618 executionBlockNumber: number,2619 options: ISchedulerOptions = {},2620 ) {2621 return this.schedule<T>('schedule', executionBlockNumber, options);2622 }26232624 scheduleAfter<T extends UniqueHelper>(2625 blocksBeforeExecution: number,2626 options: ISchedulerOptions = {},2627 ) {2628 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);2629 }26302631 schedule<T extends UniqueHelper>(2632 scheduleFn: 'schedule' | 'scheduleAfter',2633 blocksNum: number,2634 options: ISchedulerOptions = {},2635 ) {2636 // eslint-disable-next-line @typescript-eslint/naming-convention2637 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);2638 return this.helper.clone(ScheduledHelperType, {2639 scheduleFn,2640 blocksNum,2641 options,2642 }) as T;2643 }2644}26452646class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {2647 //todo:collator documentation2648 setKeys(signer: TSigner, key: string) {2649 return this.helper.executeExtrinsic(2650 signer,2651 'api.tx.session.setKeys', 2652 [2653 key,2654 '0x0',2655 ],2656 true,2657 );2658 }26592660 setOwnKeys(signer: TSigner) {2661 return this.setKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));2662 }26632664 addInvulnerable(signer: TSigner, address: string) {2665 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);2666 }26672668 removeInvulnerable(signer: TSigner, address: string) {2669 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);2670 }26712672 async getInvulnerables() {2673 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());2674 }2675}26762677class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {2678 async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {2679 await this.helper.executeExtrinsic(2680 signer,2681 'api.tx.foreignAssets.registerForeignAsset',2682 [ownerAddress, location, metadata],2683 true,2684 );2685 }26862687 async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {2688 await this.helper.executeExtrinsic(2689 signer,2690 'api.tx.foreignAssets.updateForeignAsset',2691 [foreignAssetId, location, metadata],2692 true,2693 );2694 }2695}26962697class XcmGroup<T extends ChainHelperBase> extends HelperGroup<T> {2698 palletName: string;26992700 constructor(helper: T, palletName: string) {2701 super(helper);27022703 this.palletName = palletName;2704 }27052706 async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: number) {2707 await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, {Limited: weightLimit}], true);2708 }2709}27102711class XTokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {2712 async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: number) {2713 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true);2714 }27152716 async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: number) {2717 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true);2718 }27192720 async transferMulticurrencies(signer: TSigner, currencies: any[], feeItem: number, destLocation: any, destWeight: number) {2721 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMulticurrencies', [currencies, feeItem, destLocation, destWeight], true);2722 }2723}27242725class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {2726 async accounts(address: string, currencyId: any) {2727 const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;2728 return BigInt(free);2729 }2730}27312732class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {2733 async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {2734 await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);2735 }27362737 async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {2738 await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);2739 }27402741 async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {2742 await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);2743 }27442745 async account(assetId: string | number, address: string) {2746 const accountAsset = (2747 await this.helper.callRpc('api.query.assets.account', [assetId, address])2748 ).toJSON()! as any;27492750 if (accountAsset !== null) {2751 return BigInt(accountAsset['balance']);2752 } else {2753 return null;2754 }2755 }2756}27572758class AcalaAssetRegistryGroup extends HelperGroup<AcalaHelper> {2759 async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) {2760 await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true);2761 }2762}27632764class MoonbeamAssetManagerGroup extends HelperGroup<MoonbeamHelper> {2765 makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) {2766 const apiPrefix = 'api.tx.assetManager.';27672768 const registerTx = this.helper.constructApiCall(2769 apiPrefix + 'registerForeignAsset',2770 [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient],2771 );27722773 const setUnitsTx = this.helper.constructApiCall(2774 apiPrefix + 'setAssetUnitsPerSecond',2775 [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint],2776 );27772778 const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]);2779 const encodedProposal = batchCall?.method.toHex() || '';2780 return encodedProposal;2781 }27822783 async assetTypeId(location: any) {2784 return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]);2785 }2786}27872788class MoonbeamDemocracyGroup extends HelperGroup<MoonbeamHelper> {2789 async notePreimage(signer: TSigner, encodedProposal: string) {2790 await this.helper.executeExtrinsic(signer, 'api.tx.democracy.notePreimage', [encodedProposal], true);2791 }27922793 externalProposeMajority(proposalHash: string) {2794 return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposalHash]);2795 }27962797 fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) {2798 return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);2799 }28002801 async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {2802 await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);2803 }2804}28052806class MoonbeamCollectiveGroup extends HelperGroup<MoonbeamHelper> {2807 collective: string;28082809 constructor(helper: MoonbeamHelper, collective: string) {2810 super(helper);28112812 this.collective = collective;2813 }28142815 async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) {2816 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true);2817 }28182819 async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {2820 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true);2821 }28222823 async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: number, lengthBound: number) {2824 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true);2825 }28262827 async proposalCount() {2828 return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, []));2829 }2830}28312832export type ChainHelperBaseConstructor = new(...args: any[]) => ChainHelperBase;2833export type UniqueHelperConstructor = new(...args: any[]) => UniqueHelper;28342835export class UniqueHelper extends ChainHelperBase {2836 balance: BalanceGroup<UniqueHelper>;2837 collection: CollectionGroup;2838 nft: NFTGroup;2839 rft: RFTGroup;2840 ft: FTGroup;2841 staking: StakingGroup;2842 scheduler: SchedulerGroup;2843 collatorSelection: CollatorSelectionGroup;2844 foreignAssets: ForeignAssetsGroup;2845 xcm: XcmGroup<UniqueHelper>;2846 xTokens: XTokensGroup<UniqueHelper>;2847 tokens: TokensGroup<UniqueHelper>;28482849 constructor(logger?: ILogger, options: {[key: string]: any} = {}) {2850 super(logger, options.helperBase ?? UniqueHelper);28512852 this.balance = new BalanceGroup(this);2853 this.collection = new CollectionGroup(this);2854 this.nft = new NFTGroup(this);2855 this.rft = new RFTGroup(this);2856 this.ft = new FTGroup(this);2857 this.staking = new StakingGroup(this);2858 this.scheduler = new SchedulerGroup(this);2859 this.collatorSelection = new CollatorSelectionGroup(this);2860 this.foreignAssets = new ForeignAssetsGroup(this);2861 this.xcm = new XcmGroup(this, 'polkadotXcm');2862 this.xTokens = new XTokensGroup(this);2863 this.tokens = new TokensGroup(this);2864 }28652866 getSudo<T extends UniqueHelper>() {2867 // eslint-disable-next-line @typescript-eslint/naming-convention2868 const SudoHelperType = SudoHelper(this.helperBase);2869 return this.clone(SudoHelperType) as T;2870 }2871}28722873export class XcmChainHelper extends ChainHelperBase {2874 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {2875 const wsProvider = new WsProvider(wsEndpoint);2876 this.api = new ApiPromise({2877 provider: wsProvider,2878 });2879 await this.api.isReadyOrError;2880 this.network = await UniqueHelper.detectNetwork(this.api);2881 }2882}28832884export class RelayHelper extends XcmChainHelper {2885 xcm: XcmGroup<RelayHelper>;28862887 constructor(logger?: ILogger, options: {[key: string]: any} = {}) {2888 super(logger, options.helperBase ?? RelayHelper);28892890 this.xcm = new XcmGroup(this, 'xcmPallet');2891 }2892}28932894export class WestmintHelper extends XcmChainHelper {2895 balance: SubstrateBalanceGroup<WestmintHelper>;2896 xcm: XcmGroup<WestmintHelper>;2897 assets: AssetsGroup<WestmintHelper>;2898 xTokens: XTokensGroup<WestmintHelper>;28992900 constructor(logger?: ILogger, options: {[key: string]: any} = {}) {2901 super(logger, options.helperBase ?? WestmintHelper);29022903 this.balance = new SubstrateBalanceGroup(this);2904 this.xcm = new XcmGroup(this, 'polkadotXcm');2905 this.assets = new AssetsGroup(this);2906 this.xTokens = new XTokensGroup(this);2907 }2908}29092910export class MoonbeamHelper extends XcmChainHelper {2911 balance: EthereumBalanceGroup<MoonbeamHelper>;2912 assetManager: MoonbeamAssetManagerGroup;2913 assets: AssetsGroup<MoonbeamHelper>;2914 xTokens: XTokensGroup<MoonbeamHelper>;2915 democracy: MoonbeamDemocracyGroup;2916 collective: {2917 council: MoonbeamCollectiveGroup,2918 techCommittee: MoonbeamCollectiveGroup,2919 };29202921 constructor(logger?: ILogger, options: {[key: string]: any} = {}) {2922 super(logger, options.helperBase ?? MoonbeamHelper);29232924 this.balance = new EthereumBalanceGroup(this);2925 this.assetManager = new MoonbeamAssetManagerGroup(this);2926 this.assets = new AssetsGroup(this);2927 this.xTokens = new XTokensGroup(this);2928 this.democracy = new MoonbeamDemocracyGroup(this);2929 this.collective = {2930 council: new MoonbeamCollectiveGroup(this, 'councilCollective'),2931 techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'),2932 };2933 }2934}29352936export class AcalaHelper extends XcmChainHelper {2937 balance: SubstrateBalanceGroup<AcalaHelper>;2938 assetRegistry: AcalaAssetRegistryGroup;2939 xTokens: XTokensGroup<AcalaHelper>;2940 tokens: TokensGroup<AcalaHelper>;29412942 constructor(logger?: ILogger, options: {[key: string]: any} = {}) {2943 super(logger, options.helperBase ?? AcalaHelper);29442945 this.balance = new SubstrateBalanceGroup(this);2946 this.assetRegistry = new AcalaAssetRegistryGroup(this);2947 this.xTokens = new XTokensGroup(this);2948 this.tokens = new TokensGroup(this);2949 }29502951 getSudo<T extends AcalaHelper>() {2952 // eslint-disable-next-line @typescript-eslint/naming-convention2953 const SudoHelperType = SudoHelper(this.helperBase);2954 return this.clone(SudoHelperType) as T;2955 }2956}29572958// eslint-disable-next-line @typescript-eslint/naming-convention2959function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {2960 return class extends Base {2961 scheduleFn: 'schedule' | 'scheduleAfter';2962 blocksNum: number;2963 options: ISchedulerOptions;29642965 constructor(...args: any[]) {2966 const logger = args[0] as ILogger;2967 const options = args[1] as {2968 scheduleFn: 'schedule' | 'scheduleAfter',2969 blocksNum: number,2970 options: ISchedulerOptions2971 };29722973 super(logger);29742975 this.scheduleFn = options.scheduleFn;2976 this.blocksNum = options.blocksNum;2977 this.options = options.options;2978 }29792980 executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {2981 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);2982 2983 const mandatorySchedArgs = [2984 this.blocksNum,2985 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,2986 this.options.priority ?? null,2987 scheduledTx,2988 ];2989 2990 let schedArgs;2991 let scheduleFn;29922993 if (this.options.scheduledId) {2994 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];29952996 if (this.scheduleFn == 'schedule') {2997 scheduleFn = 'scheduleNamed';2998 } else if (this.scheduleFn == 'scheduleAfter') {2999 scheduleFn = 'scheduleNamedAfter';3000 }3001 } else {3002 schedArgs = mandatorySchedArgs;3003 scheduleFn = this.scheduleFn;3004 }30053006 const extrinsic = 'api.tx.scheduler.' + scheduleFn;30073008 return super.executeExtrinsic(3009 sender,3010 extrinsic,3011 schedArgs,3012 expectSuccess,3013 );3014 }3015 };3016}30173018// eslint-disable-next-line @typescript-eslint/naming-convention3019function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {3020 return class extends Base {3021 constructor(...args: any[]) {3022 super(...args);3023 }30243025 async executeExtrinsic(3026 sender: IKeyringPair,3027 extrinsic: string,3028 params: any[],3029 expectSuccess?: boolean,3030 options: Partial<SignerOptions>|null = null,3031 ): Promise<ITransactionResult> {3032 const call = this.constructApiCall(extrinsic, params);3033 const result = await super.executeExtrinsic(3034 sender,3035 'api.tx.sudo.sudo',3036 [call],3037 expectSuccess,3038 options,3039 );30403041 if (result.status === 'Fail') return result;30423043 const data = this.eventHelper.extractEvents(result.result.events).find(x => x.section == 'sudo')?.data[0];3044 if (data.err) {3045 const error = data.err.module;3046 // todo:collator3047 const metaError = super.getApi()?.registry.findMetaError({index: new BN(error.index), error: new BN(9)});3048 throw new Error(`${data.err.module.error} ${metaError.section}.${metaError.name}`);3049 }3050 return result;3051 }3052 };3053}30543055export class UniqueBaseCollection {3056 helper: UniqueHelper;3057 collectionId: number;30583059 constructor(collectionId: number, uniqueHelper: UniqueHelper) {3060 this.collectionId = collectionId;3061 this.helper = uniqueHelper;3062 }30633064 async getData() {3065 return await this.helper.collection.getData(this.collectionId);3066 }30673068 async getLastTokenId() {3069 return await this.helper.collection.getLastTokenId(this.collectionId);3070 }30713072 async doesTokenExist(tokenId: number) {3073 return await this.helper.collection.doesTokenExist(this.collectionId, tokenId);3074 }30753076 async getAdmins() {3077 return await this.helper.collection.getAdmins(this.collectionId);3078 }30793080 async getAllowList() {3081 return await this.helper.collection.getAllowList(this.collectionId);3082 }30833084 async getEffectiveLimits() {3085 return await this.helper.collection.getEffectiveLimits(this.collectionId);3086 }30873088 async getProperties(propertyKeys?: string[] | null) {3089 return await this.helper.collection.getProperties(this.collectionId, propertyKeys);3090 }30913092 async getPropertiesConsumedSpace() {3093 return await this.helper.collection.getPropertiesConsumedSpace(this.collectionId);3094 }30953096 async getTokenNextSponsored(tokenId: number, addressObj: ICrossAccountId) {3097 return await this.helper.collection.getTokenNextSponsored(this.collectionId, tokenId, addressObj);3098 }30993100 async getOptions() {3101 return await this.helper.collection.getCollectionOptions(this.collectionId);3102 }31033104 async setSponsor(signer: TSigner, sponsorAddress: TSubstrateAccount) {3105 return await this.helper.collection.setSponsor(signer, this.collectionId, sponsorAddress);3106 }31073108 async confirmSponsorship(signer: TSigner) {3109 return await this.helper.collection.confirmSponsorship(signer, this.collectionId);3110 }31113112 async removeSponsor(signer: TSigner) {3113 return await this.helper.collection.removeSponsor(signer, this.collectionId);3114 }31153116 async setLimits(signer: TSigner, limits: ICollectionLimits) {3117 return await this.helper.collection.setLimits(signer, this.collectionId, limits);3118 }31193120 async changeOwner(signer: TSigner, ownerAddress: TSubstrateAccount) {3121 return await this.helper.collection.changeOwner(signer, this.collectionId, ownerAddress);3122 }31233124 async addAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {3125 return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj);3126 }31273128 async addToAllowList(signer: TSigner, addressObj: ICrossAccountId) {3129 return await this.helper.collection.addToAllowList(signer, this.collectionId, addressObj);3130 }31313132 async removeFromAllowList(signer: TSigner, addressObj: ICrossAccountId) {3133 return await this.helper.collection.removeFromAllowList(signer, this.collectionId, addressObj);3134 }31353136 async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId) {3137 return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj);3138 }31393140 async setProperties(signer: TSigner, properties: IProperty[]) {3141 return await this.helper.collection.setProperties(signer, this.collectionId, properties);3142 }31433144 async deleteProperties(signer: TSigner, propertyKeys: string[]) {3145 return await this.helper.collection.deleteProperties(signer, this.collectionId, propertyKeys);3146 }31473148 async setPermissions(signer: TSigner, permissions: ICollectionPermissions) {3149 return await this.helper.collection.setPermissions(signer, this.collectionId, permissions);3150 }31513152 async enableNesting(signer: TSigner, permissions: INestingPermissions) {3153 return await this.helper.collection.enableNesting(signer, this.collectionId, permissions);3154 }31553156 async disableNesting(signer: TSigner) {3157 return await this.helper.collection.disableNesting(signer, this.collectionId);3158 }31593160 async burn(signer: TSigner) {3161 return await this.helper.collection.burn(signer, this.collectionId);3162 }31633164 scheduleAt<T extends UniqueHelper>(3165 executionBlockNumber: number,3166 options: ISchedulerOptions = {},3167 ) {3168 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);3169 return new UniqueBaseCollection(this.collectionId, scheduledHelper);3170 }31713172 scheduleAfter<T extends UniqueHelper>(3173 blocksBeforeExecution: number,3174 options: ISchedulerOptions = {},3175 ) {3176 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);3177 return new UniqueBaseCollection(this.collectionId, scheduledHelper);3178 }31793180 getSudo<T extends UniqueHelper>() {3181 return new UniqueBaseCollection(this.collectionId, this.helper.getSudo<T>());3182 }3183}318431853186export class UniqueNFTCollection extends UniqueBaseCollection {3187 getTokenObject(tokenId: number) {3188 return new UniqueNFToken(tokenId, this);3189 }31903191 async getTokensByAddress(addressObj: ICrossAccountId) {3192 return await this.helper.nft.getTokensByAddress(this.collectionId, addressObj);3193 }31943195 async getToken(tokenId: number, blockHashAt?: string) {3196 return await this.helper.nft.getToken(this.collectionId, tokenId, [], blockHashAt);3197 }31983199 async getTokenOwner(tokenId: number, blockHashAt?: string) {3200 return await this.helper.nft.getTokenOwner(this.collectionId, tokenId, blockHashAt);3201 }32023203 async getTokenTopmostOwner(tokenId: number, blockHashAt?: string) {3204 return await this.helper.nft.getTokenTopmostOwner(this.collectionId, tokenId, blockHashAt);3205 }32063207 async getTokenChildren(tokenId: number, blockHashAt?: string) {3208 return await this.helper.nft.getTokenChildren(this.collectionId, tokenId, blockHashAt);3209 }32103211 async getPropertyPermissions(propertyKeys: string[] | null = null) {3212 return await this.helper.nft.getPropertyPermissions(this.collectionId, propertyKeys);3213 }32143215 async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {3216 return await this.helper.nft.getTokenProperties(this.collectionId, tokenId, propertyKeys);3217 }32183219 async getTokenPropertiesConsumedSpace(tokenId: number): Promise<number> {3220 const api = this.helper.getApi();3221 const props = (await api.query.nonfungible.tokenProperties(this.collectionId, tokenId)).toJSON();3222 3223 return (props! as any).consumedSpace;3224 }32253226 async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId) {3227 return await this.helper.nft.transferToken(signer, this.collectionId, tokenId, addressObj);3228 }32293230 async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {3231 return await this.helper.nft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj);3232 }32333234 async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId) {3235 return await this.helper.nft.approveToken(signer, this.collectionId, tokenId, toAddressObj);3236 }32373238 async isTokenApproved(tokenId: number, toAddressObj: ICrossAccountId) {3239 return await this.helper.nft.isTokenApproved(this.collectionId, tokenId, toAddressObj);3240 }32413242 async mintToken(signer: TSigner, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {3243 return await this.helper.nft.mintToken(signer, {collectionId: this.collectionId, owner, properties});3244 }32453246 async mintMultipleTokens(signer: TSigner, tokens: {owner: ICrossAccountId, properties?: IProperty[]}[]) {3247 return await this.helper.nft.mintMultipleTokens(signer, this.collectionId, tokens);3248 }32493250 async burnToken(signer: TSigner, tokenId: number) {3251 return await this.helper.nft.burnToken(signer, this.collectionId, tokenId);3252 }32533254 async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId) {3255 return await this.helper.nft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj);3256 }32573258 async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {3259 return await this.helper.nft.setTokenProperties(signer, this.collectionId, tokenId, properties);3260 }32613262 async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {3263 return await this.helper.nft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);3264 }32653266 async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {3267 return await this.helper.nft.setTokenPropertyPermissions(signer, this.collectionId, permissions);3268 }32693270 async nestToken(signer: TSigner, tokenId: number, toTokenObj: IToken) {3271 return await this.helper.nft.nestToken(signer, {collectionId: this.collectionId, tokenId}, toTokenObj);3272 }32733274 async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {3275 return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);3276 }32773278 scheduleAt<T extends UniqueHelper>(3279 executionBlockNumber: number,3280 options: ISchedulerOptions = {},3281 ) {3282 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);3283 return new UniqueNFTCollection(this.collectionId, scheduledHelper);3284 }32853286 scheduleAfter<T extends UniqueHelper>(3287 blocksBeforeExecution: number,3288 options: ISchedulerOptions = {},3289 ) {3290 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);3291 return new UniqueNFTCollection(this.collectionId, scheduledHelper);3292 }32933294 getSudo<T extends UniqueHelper>() {3295 return new UniqueNFTCollection(this.collectionId, this.helper.getSudo<T>());3296 }3297}329832993300export class UniqueRFTCollection extends UniqueBaseCollection {3301 getTokenObject(tokenId: number) {3302 return new UniqueRFToken(tokenId, this);3303 }33043305 async getToken(tokenId: number, blockHashAt?: string) {3306 return await this.helper.rft.getToken(this.collectionId, tokenId, [], blockHashAt);3307 }33083309 async getTokensByAddress(addressObj: ICrossAccountId) {3310 return await this.helper.rft.getTokensByAddress(this.collectionId, addressObj);3311 }33123313 async getTop10TokenOwners(tokenId: number) {3314 return await this.helper.rft.getTokenTop10Owners(this.collectionId, tokenId);3315 }33163317 async getTokenBalance(tokenId: number, addressObj: ICrossAccountId) {3318 return await this.helper.rft.getTokenBalance(this.collectionId, tokenId, addressObj);3319 }33203321 async getTokenTotalPieces(tokenId: number) {3322 return await this.helper.rft.getTokenTotalPieces(this.collectionId, tokenId);3323 }33243325 async getTokenApprovedPieces(tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {3326 return await this.helper.rft.getTokenApprovedPieces(this.collectionId, tokenId, toAddressObj, fromAddressObj);3327 }33283329 async getPropertyPermissions(propertyKeys: string[] | null = null) {3330 return await this.helper.rft.getPropertyPermissions(this.collectionId, propertyKeys);3331 }33323333 async getTokenProperties(tokenId: number, propertyKeys?: string[] | null) {3334 return await this.helper.rft.getTokenProperties(this.collectionId, tokenId, propertyKeys);3335 }33363337 async getTokenPropertiesConsumedSpace(tokenId: number): Promise<number> {3338 const api = this.helper.getApi();3339 const props = (await api.query.refungible.tokenProperties(this.collectionId, tokenId)).toJSON();3340 3341 return (props! as any).consumedSpace;3342 }33433344 async transferToken(signer: TSigner, tokenId: number, addressObj: ICrossAccountId, amount=1n) {3345 return await this.helper.rft.transferToken(signer, this.collectionId, tokenId, addressObj, amount);3346 }33473348 async transferTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {3349 return await this.helper.rft.transferTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, toAddressObj, amount);3350 }33513352 async approveToken(signer: TSigner, tokenId: number, toAddressObj: ICrossAccountId, amount=1n) {3353 return await this.helper.rft.approveToken(signer, this.collectionId, tokenId, toAddressObj, amount);3354 }33553356 async repartitionToken(signer: TSigner, tokenId: number, amount: bigint) {3357 return await this.helper.rft.repartitionToken(signer, this.collectionId, tokenId, amount);3358 }33593360 async mintToken(signer: TSigner, pieces=1n, owner: ICrossAccountId = {Substrate: signer.address}, properties?: IProperty[]) {3361 return await this.helper.rft.mintToken(signer, {collectionId: this.collectionId, owner, pieces, properties});3362 }33633364 async mintMultipleTokens(signer: TSigner, tokens: {pieces: bigint, owner: ICrossAccountId, properties?: IProperty[]}[]) {3365 return await this.helper.rft.mintMultipleTokens(signer, this.collectionId, tokens);3366 }33673368 async burnToken(signer: TSigner, tokenId: number, amount=1n) {3369 return await this.helper.rft.burnToken(signer, this.collectionId, tokenId, amount);3370 }33713372 async burnTokenFrom(signer: TSigner, tokenId: number, fromAddressObj: ICrossAccountId, amount=1n) {3373 return await this.helper.rft.burnTokenFrom(signer, this.collectionId, tokenId, fromAddressObj, amount);3374 }33753376 async setTokenProperties(signer: TSigner, tokenId: number, properties: IProperty[]) {3377 return await this.helper.rft.setTokenProperties(signer, this.collectionId, tokenId, properties);3378 }33793380 async deleteTokenProperties(signer: TSigner, tokenId: number, propertyKeys: string[]) {3381 return await this.helper.rft.deleteTokenProperties(signer, this.collectionId, tokenId, propertyKeys);3382 }33833384 async setTokenPropertyPermissions(signer: TSigner, permissions: ITokenPropertyPermission[]) {3385 return await this.helper.rft.setTokenPropertyPermissions(signer, this.collectionId, permissions);3386 }33873388 scheduleAt<T extends UniqueHelper>(3389 executionBlockNumber: number,3390 options: ISchedulerOptions = {},3391 ) {3392 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);3393 return new UniqueRFTCollection(this.collectionId, scheduledHelper);3394 }33953396 scheduleAfter<T extends UniqueHelper>(3397 blocksBeforeExecution: number,3398 options: ISchedulerOptions = {},3399 ) {3400 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);3401 return new UniqueRFTCollection(this.collectionId, scheduledHelper);3402 }34033404 getSudo<T extends UniqueHelper>() {3405 return new UniqueRFTCollection(this.collectionId, this.helper.getSudo<T>());3406 }3407}340834093410export class UniqueFTCollection extends UniqueBaseCollection {3411 async getBalance(addressObj: ICrossAccountId) {3412 return await this.helper.ft.getBalance(this.collectionId, addressObj);3413 }34143415 async getTotalPieces() {3416 return await this.helper.ft.getTotalPieces(this.collectionId);3417 }34183419 async getApprovedTokens(fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {3420 return await this.helper.ft.getApprovedTokens(this.collectionId, fromAddressObj, toAddressObj);3421 }34223423 async getTop10Owners() {3424 return await this.helper.ft.getTop10Owners(this.collectionId);3425 }34263427 async mint(signer: TSigner, amount=1n, owner: ICrossAccountId = {Substrate: signer.address}) {3428 return await this.helper.ft.mintTokens(signer, this.collectionId, amount, owner);3429 }34303431 async mintWithOneOwner(signer: TSigner, tokens: {value: bigint}[], owner: ICrossAccountId = {Substrate: signer.address}) {3432 return await this.helper.ft.mintMultipleTokensWithOneOwner(signer, this.collectionId, tokens, owner);3433 }34343435 async transfer(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {3436 return await this.helper.ft.transfer(signer, this.collectionId, toAddressObj, amount);3437 }34383439 async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {3440 return await this.helper.ft.transferFrom(signer, this.collectionId, fromAddressObj, toAddressObj, amount);3441 }34423443 async burnTokens(signer: TSigner, amount=1n) {3444 return await this.helper.ft.burnTokens(signer, this.collectionId, amount);3445 }34463447 async burnTokensFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {3448 return await this.helper.ft.burnTokensFrom(signer, this.collectionId, fromAddressObj, amount);3449 }34503451 async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {3452 return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);3453 }34543455 scheduleAt<T extends UniqueHelper>(3456 executionBlockNumber: number,3457 options: ISchedulerOptions = {},3458 ) {3459 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);3460 return new UniqueFTCollection(this.collectionId, scheduledHelper);3461 }34623463 scheduleAfter<T extends UniqueHelper>(3464 blocksBeforeExecution: number,3465 options: ISchedulerOptions = {},3466 ) {3467 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);3468 return new UniqueFTCollection(this.collectionId, scheduledHelper);3469 }34703471 getSudo<T extends UniqueHelper>() {3472 return new UniqueFTCollection(this.collectionId, this.helper.getSudo<T>());3473 }3474}347534763477export class UniqueBaseToken {3478 collection: UniqueNFTCollection | UniqueRFTCollection;3479 collectionId: number;3480 tokenId: number;34813482 constructor(tokenId: number, collection: UniqueNFTCollection | UniqueRFTCollection) {3483 this.collection = collection;3484 this.collectionId = collection.collectionId;3485 this.tokenId = tokenId;3486 }34873488 async getNextSponsored(addressObj: ICrossAccountId) {3489 return await this.collection.getTokenNextSponsored(this.tokenId, addressObj);3490 }34913492 async getProperties(propertyKeys?: string[] | null) {3493 return await this.collection.getTokenProperties(this.tokenId, propertyKeys);3494 }34953496 async getTokenPropertiesConsumedSpace() {3497 return await this.collection.getTokenPropertiesConsumedSpace(this.tokenId);3498 }34993500 async setProperties(signer: TSigner, properties: IProperty[]) {3501 return await this.collection.setTokenProperties(signer, this.tokenId, properties);3502 }35033504 async deleteProperties(signer: TSigner, propertyKeys: string[]) {3505 return await this.collection.deleteTokenProperties(signer, this.tokenId, propertyKeys);3506 }35073508 async doesExist() {3509 return await this.collection.doesTokenExist(this.tokenId);3510 }35113512 nestingAccount() {3513 return this.collection.helper.util.getTokenAccount(this);3514 }35153516 scheduleAt<T extends UniqueHelper>(3517 executionBlockNumber: number,3518 options: ISchedulerOptions = {},3519 ) {3520 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);3521 return new UniqueBaseToken(this.tokenId, scheduledCollection);3522 }35233524 scheduleAfter<T extends UniqueHelper>(3525 blocksBeforeExecution: number,3526 options: ISchedulerOptions = {},3527 ) {3528 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);3529 return new UniqueBaseToken(this.tokenId, scheduledCollection);3530 }35313532 getSudo<T extends UniqueHelper>() {3533 return new UniqueBaseToken(this.tokenId, this.collection.getSudo<T>());3534 }3535}353635373538export class UniqueNFToken extends UniqueBaseToken {3539 collection: UniqueNFTCollection;35403541 constructor(tokenId: number, collection: UniqueNFTCollection) {3542 super(tokenId, collection);3543 this.collection = collection;3544 }35453546 async getData(blockHashAt?: string) {3547 return await this.collection.getToken(this.tokenId, blockHashAt);3548 }35493550 async getOwner(blockHashAt?: string) {3551 return await this.collection.getTokenOwner(this.tokenId, blockHashAt);3552 }35533554 async getTopmostOwner(blockHashAt?: string) {3555 return await this.collection.getTokenTopmostOwner(this.tokenId, blockHashAt);3556 }35573558 async getChildren(blockHashAt?: string) {3559 return await this.collection.getTokenChildren(this.tokenId, blockHashAt);3560 }35613562 async nest(signer: TSigner, toTokenObj: IToken) {3563 return await this.collection.nestToken(signer, this.tokenId, toTokenObj);3564 }35653566 async unnest(signer: TSigner, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {3567 return await this.collection.unnestToken(signer, this.tokenId, fromTokenObj, toAddressObj);3568 }35693570 async transfer(signer: TSigner, addressObj: ICrossAccountId) {3571 return await this.collection.transferToken(signer, this.tokenId, addressObj);3572 }35733574 async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId) {3575 return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj);3576 }35773578 async approve(signer: TSigner, toAddressObj: ICrossAccountId) {3579 return await this.collection.approveToken(signer, this.tokenId, toAddressObj);3580 }35813582 async isApproved(toAddressObj: ICrossAccountId) {3583 return await this.collection.isTokenApproved(this.tokenId, toAddressObj);3584 }35853586 async burn(signer: TSigner) {3587 return await this.collection.burnToken(signer, this.tokenId);3588 }35893590 async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) {3591 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);3592 }35933594 scheduleAt<T extends UniqueHelper>(3595 executionBlockNumber: number,3596 options: ISchedulerOptions = {},3597 ) {3598 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);3599 return new UniqueNFToken(this.tokenId, scheduledCollection);3600 }36013602 scheduleAfter<T extends UniqueHelper>(3603 blocksBeforeExecution: number,3604 options: ISchedulerOptions = {},3605 ) {3606 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);3607 return new UniqueNFToken(this.tokenId, scheduledCollection);3608 }36093610 getSudo<T extends UniqueHelper>() {3611 return new UniqueNFToken(this.tokenId, this.collection.getSudo<T>());3612 }3613}36143615export class UniqueRFToken extends UniqueBaseToken {3616 collection: UniqueRFTCollection;36173618 constructor(tokenId: number, collection: UniqueRFTCollection) {3619 super(tokenId, collection);3620 this.collection = collection;3621 }36223623 async getData(blockHashAt?: string) {3624 return await this.collection.getToken(this.tokenId, blockHashAt);3625 }36263627 async getTop10Owners() {3628 return await this.collection.getTop10TokenOwners(this.tokenId);3629 }36303631 async getBalance(addressObj: ICrossAccountId) {3632 return await this.collection.getTokenBalance(this.tokenId, addressObj);3633 }36343635 async getTotalPieces() {3636 return await this.collection.getTokenTotalPieces(this.tokenId);3637 }36383639 async getApprovedPieces(fromAddressObj: ICrossAccountId, toAccountObj: ICrossAccountId) {3640 return await this.collection.getTokenApprovedPieces(this.tokenId, fromAddressObj, toAccountObj);3641 }36423643 async transfer(signer: TSigner, addressObj: ICrossAccountId, amount=1n) {3644 return await this.collection.transferToken(signer, this.tokenId, addressObj, amount);3645 }36463647 async transferFrom(signer: TSigner, fromAddressObj: ICrossAccountId, toAddressObj: ICrossAccountId, amount=1n) {3648 return await this.collection.transferTokenFrom(signer, this.tokenId, fromAddressObj, toAddressObj, amount);3649 }36503651 async approve(signer: TSigner, toAddressObj: ICrossAccountId, amount=1n) {3652 return await this.collection.approveToken(signer, this.tokenId, toAddressObj, amount);3653 }36543655 async repartition(signer: TSigner, amount: bigint) {3656 return await this.collection.repartitionToken(signer, this.tokenId, amount);3657 }36583659 async burn(signer: TSigner, amount=1n) {3660 return await this.collection.burnToken(signer, this.tokenId, amount);3661 }36623663 async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount=1n) {3664 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);3665 }36663667 scheduleAt<T extends UniqueHelper>(3668 executionBlockNumber: number,3669 options: ISchedulerOptions = {},3670 ) {3671 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);3672 return new UniqueRFToken(this.tokenId, scheduledCollection);3673 }36743675 scheduleAfter<T extends UniqueHelper>(3676 blocksBeforeExecution: number,3677 options: ISchedulerOptions = {},3678 ) {3679 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);3680 return new UniqueRFToken(this.tokenId, scheduledCollection);3681 }36823683 getSudo<T extends UniqueHelper>() {3684 return new UniqueRFToken(this.tokenId, this.collection.getSudo<T>());3685 }3686}