diff --git a/pallets/creator-staking/src/functions.rs b/pallets/creator-staking/src/functions.rs index e605caa5..6dc936e6 100644 --- a/pallets/creator-staking/src/functions.rs +++ b/pallets/creator-staking/src/functions.rs @@ -39,6 +39,11 @@ impl Pallet { .map_or(false, |info| info.status == CreatorStatus::Active) } + pub(crate) fn ensure_creator_is_active(creator_id: CreatorId) -> DispatchResult { + ensure!(Self::is_creator_active(creator_id), Error::::InactiveCreator); + Ok(()) + } + pub(crate) fn ensure_creator_active_in_era( creator_info: &CreatorInfo, era: EraIndex, @@ -145,7 +150,7 @@ impl Pallet { /// If the unstake operation was successful, the given structs are properly modified and the total /// unstaked value is returned. If not, an error is returned and the structs are left in /// an undefined state. - pub(crate) fn calculate_final_unstaking_amount( + pub(crate) fn calculate_and_apply_stake_decrease( backer_stakes: &mut StakesInfoOf, stake_info: &mut CreatorStakeInfo>, desired_amount: BalanceOf, @@ -154,11 +159,10 @@ impl Pallet { let staked_value = backer_stakes.current_stake(); ensure!(staked_value > Zero::zero(), Error::::NotStakedCreator); - // Calculate the value which will be unstaked. let remaining = staked_value.saturating_sub(desired_amount); // If the remaining amount is less than the minimum staking amount, unstake the entire amount. - let amount_to_unstake = if remaining < T::MinimumStake::get() { + let amount_to_decrease = if remaining < T::MinimumStake::get() { stake_info.backers_count = stake_info.backers_count.saturating_sub(1); staked_value } else { @@ -166,17 +170,18 @@ impl Pallet { }; // Sanity check - ensure!(amount_to_unstake > Zero::zero(), Error::::CannotUnstakeZero); + ensure!(amount_to_decrease > Zero::zero(), Error::::CannotUnstakeZero); - stake_info.total_staked = stake_info.total_staked.saturating_sub(amount_to_unstake); + // Modify data + stake_info.total_staked = stake_info.total_staked.saturating_sub(amount_to_decrease); backer_stakes - .decrease_stake(current_era, amount_to_unstake) + .decrease_stake(current_era, amount_to_decrease) .map_err(|_| Error::::CannotChangeStakeInPastEra)?; Self::ensure_can_add_stake_item(backer_stakes)?; - Ok(amount_to_unstake) + Ok(amount_to_decrease) } /// Update the locks for a backer. This will also update the stash lock. diff --git a/pallets/creator-staking/src/lib.rs b/pallets/creator-staking/src/lib.rs index cb96438c..a6da365d 100644 --- a/pallets/creator-staking/src/lib.rs +++ b/pallets/creator-staking/src/lib.rs @@ -234,6 +234,7 @@ pub mod pallet { pub enum Event { Staked { who: T::AccountId, creator_id: CreatorId, era: EraIndex, amount: BalanceOf }, Unstaked { who: T::AccountId, creator_id: CreatorId, era: EraIndex, amount: BalanceOf }, + StakeMoved { who: T::AccountId, from_creator_id: CreatorId, to_creator_id: CreatorId, amount: BalanceOf }, BackerRewardsClaimed { who: T::AccountId, creator_id: CreatorId, amount: BalanceOf }, CreatorRewardsClaimed { who: T::AccountId, amount: BalanceOf }, StakeWithdrawn { who: T::AccountId, amount: BalanceOf }, @@ -273,6 +274,8 @@ pub mod pallet { MaintenanceModeNotChanged, InvalidSumOfRewardDistributionConfig, StakeHasExpired, + CannotMoveStakeToSameCreator, + CannotMoveZeroStake, } #[pallet::hooks] @@ -392,7 +395,7 @@ pub mod pallet { let backer = ensure_signed(origin)?; // Check that a creator is ready for staking. - ensure!(Self::is_creator_active(creator_id), Error::::InactiveCreator); + Self::ensure_creator_is_active(creator_id)?; // Retrieve the backer's locks, or create an entry if it doesn't exist. let mut backer_locks = Self::backer_locks(&backer); @@ -435,16 +438,6 @@ pub mod pallet { Ok(().into()) } - // #[weight = 10_000] - // fn increase_stake(origin, creator_id, additional_amount) { - // todo!() - // } - // - // #[weight = 10_000] - // fn move_stake(origin, from_creator_id, to_creator_id, amount) { - // todo!() - // } - /// Start unbonding process and unstake balance from the creator. /// /// The unstaked amount will no longer be eligible for rewards but still won't be unlocked. @@ -464,7 +457,7 @@ pub mod pallet { let backer = ensure_signed(origin)?; ensure!(amount > Zero::zero(), Error::::CannotUnstakeZero); - ensure!(Self::is_creator_active(creator_id), Error::::InactiveCreator); + Self::ensure_creator_is_active(creator_id)?; let current_era = Self::current_era(); let mut backer_stakes = Self::backer_stakes(&backer, creator_id); @@ -472,7 +465,7 @@ pub mod pallet { Self::creator_stake_info(creator_id, current_era).unwrap_or_default(); let amount_to_unstake = - Self::calculate_final_unstaking_amount(&mut backer_stakes, &mut stake_info, amount, current_era)?; + Self::calculate_and_apply_stake_decrease(&mut backer_stakes, &mut stake_info, amount, current_era)?; // Update the chunks and write them to storage let mut backer_locks = Self::backer_locks(&backer); @@ -589,11 +582,84 @@ pub mod pallet { Ok(().into()) } + /// Move stake from one creator to another. + /// + /// It follows the same rules as the `stake` and `unstake` functions, with one notable + /// difference: there is no unbonding period. + /// + /// # Parameters + /// - `from_creator_id`: The ID of the source creator. + /// - `to_creator_id`: The ID of the target creator. + /// - `amount`: The amount of stake to be moved. + /// + #[pallet::call_index(7)] + #[pallet::weight(Weight::from_parts(10_000, 0))] + pub fn move_stake( + origin: OriginFor, + from_creator_id: CreatorId, + to_creator_id: CreatorId, + #[pallet::compact] amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + Self::ensure_pallet_enabled()?; + let backer = ensure_signed(origin)?; + + // Creators must differ and both must be active + ensure!( + from_creator_id != to_creator_id, + Error::::CannotMoveStakeToSameCreator + ); + + ensure!(amount > Zero::zero(), Error::::CannotMoveZeroStake); + + Self::ensure_creator_is_active(to_creator_id)?; + + // Validate and update previous creator related data + let current_era = Self::current_era(); + let mut backer_stakes_by_source_creator = Self::backer_stakes(&backer, from_creator_id); + let mut source_creator_info = + Self::creator_stake_info(from_creator_id, current_era).unwrap_or_default(); + + // Backer stake is decreased in `calculate_and_apply_stake_decrease` + let stake_amount_to_move = Self::calculate_and_apply_stake_decrease( + &mut backer_stakes_by_source_creator, + &mut source_creator_info, + amount, + current_era, + )?; + + // Validate and update target creator related data + let mut backer_stakes_by_target_creator = Self::backer_stakes(&backer, to_creator_id); + let mut target_creator_info = + Self::creator_stake_info(to_creator_id, current_era).unwrap_or_default(); + + Self::stake_to_creator( + &mut backer_stakes_by_target_creator, + &mut target_creator_info, + stake_amount_to_move, + current_era, + )?; + + CreatorStakeInfoByEra::::insert(from_creator_id, current_era, source_creator_info); + Self::update_backer_stakes(&backer, from_creator_id, backer_stakes_by_source_creator); + + CreatorStakeInfoByEra::::insert(to_creator_id, current_era, target_creator_info); + Self::update_backer_stakes(&backer, to_creator_id, backer_stakes_by_target_creator); + + Self::deposit_event(Event::::StakeMoved { + who: backer, + from_creator_id, + to_creator_id, + amount: stake_amount_to_move, + }); + + Ok(().into()) + } + // Claim rewards for the backer on the oldest unclaimed era where they have a stake // and optionally restake the rewards to the same creator. // Not sure here whether to calculate total rewards for all creators // or to withdraw per-creator rewards (preferably) - #[pallet::call_index(7)] + #[pallet::call_index(8)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn claim_backer_reward(origin: OriginFor, creator_id: CreatorId, restake: bool) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; @@ -654,7 +720,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(8)] + #[pallet::call_index(9)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn claim_creator_reward(origin: OriginFor, creator_id: CreatorId, era: EraIndex) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; @@ -711,7 +777,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(9)] + #[pallet::call_index(10)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn set_maintenance_mode( origin: OriginFor, @@ -732,7 +798,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(10)] + #[pallet::call_index(11)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn force_new_era(origin: OriginFor) -> DispatchResultWithPostInfo { ensure_root(origin)?; @@ -740,7 +806,7 @@ pub mod pallet { Ok(Pays::No.into()) } - #[pallet::call_index(11)] + #[pallet::call_index(12)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn set_reward_distribution_config( origin: OriginFor, @@ -756,7 +822,7 @@ pub mod pallet { Ok(()) } - #[pallet::call_index(12)] + #[pallet::call_index(13)] #[pallet::weight(T::DbWeight::get().writes(1) + Weight::from_parts(10_000, 0))] pub fn set_per_block_reward(origin: OriginFor, new_reward: BalanceOf) -> DispatchResult { ensure_root(origin)?; diff --git a/pallets/creator-staking/src/tests/testing_utils.rs b/pallets/creator-staking/src/tests/testing_utils.rs index 1679c8aa..272263b4 100644 --- a/pallets/creator-staking/src/tests/testing_utils.rs +++ b/pallets/creator-staking/src/tests/testing_utils.rs @@ -417,8 +417,7 @@ pub(super) fn assert_claim_backer(claimer: AccountId, creator_id: SpaceId, resta let mut init_state_current_era = MemorySnapshot::all(current_era, creator_id, claimer); // Calculate backer's portion of the reward - let stakes = init_state_claim_era.backer_stakes.stakes.clone(); - let (claim_era, staked) = StakesInfo { stakes }.claim(); + let (claim_era, staked) = init_state_claim_era.backer_stakes.clone().claim(); assert!(claim_era > 0); // Sanity check - if this fails, method is being used incorrectly // Cannot claim rewards post unregister era, this indicates a bug! @@ -479,8 +478,7 @@ pub(super) fn assert_claim_backer(claimer: AccountId, creator_id: SpaceId, resta amount: calculated_reward, })); - let stakes = final_state_current_era.backer_stakes.stakes.clone(); - let (new_era, _) = StakesInfo { stakes }.claim(); + let (new_era, _) = final_state_current_era.backer_stakes.clone().claim(); if final_state_current_era.backer_stakes.is_empty() { assert!(new_era.is_zero()); assert!(!BackerStakesByCreator::::contains_key( @@ -513,7 +511,7 @@ fn assert_restake_reward( final_state_current_era: &MemorySnapshot, reward: Balance, ) { - let mut init_backer_stakes = StakesInfo { stakes: init_state_current_era.backer_stakes.stakes.clone() }; + let mut init_backer_stakes = init_state_current_era.backer_stakes.clone(); if CreatorStaking::ensure_can_restake_reward( restake, init_state_current_era.clone().creator_info.status, @@ -599,3 +597,93 @@ pub(super) fn assert_claim_creator(creator_id: SpaceId, claim_era: EraIndex) { assert_eq!(init_state.backer_locks.total_locked, final_state.backer_locks.total_locked); assert_eq!(init_state.backer_locks.unbonding_info.vec(), final_state.backer_locks.unbonding_info.vec()); } + +/// Used to perform move stake with success and storage assertions. +pub(crate) fn assert_move_stake( + backer: AccountId, + from_creator_id: CreatorId, + to_creator_id: CreatorId, + amount: Balance, +) { + // Get latest staking info + let current_era = CreatorStaking::current_era(); + let source_creator_init_state = MemorySnapshot::all(current_era, from_creator_id, backer); + let target_creator_init_state = MemorySnapshot::all(current_era, to_creator_id, backer); + + // Calculate value which will actually be transfered + let source_creator_init_stake_amount = source_creator_init_state.backer_stakes.current_stake(); + let expected_amount_to_move = if source_creator_init_stake_amount - amount >= MINIMUM_STAKING_AMOUNT { + amount + } else { + source_creator_init_stake_amount + }; + + // Ensure op is successful and event is emitted + assert_ok!(CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + from_creator_id, + to_creator_id, + amount, + )); + System::assert_last_event(RuntimeEvent::CreatorStaking(Event::StakeMoved { + who: backer.clone(), + from_creator_id, + to_creator_id, + amount: expected_amount_to_move, + })); + + let source_creator_final_state = MemorySnapshot::all(current_era, from_creator_id, backer); + let target_creator_final_state = MemorySnapshot::all(current_era, to_creator_id, backer); + + // Ensure backer stakes has increased/decreased staked amount + assert_eq!( + source_creator_final_state.backer_stakes.current_stake(), + source_creator_init_stake_amount - expected_amount_to_move + ); + assert_eq!( + target_creator_final_state.backer_stakes.current_stake(), + target_creator_init_state.backer_stakes.current_stake() + expected_amount_to_move + ); + + // Ensure total value staked on creators has appropriately increased/decreased + assert_eq!( + source_creator_final_state.creator_stakes_info.total_staked, + source_creator_init_state.creator_stakes_info.total_staked - expected_amount_to_move + ); + assert_eq!( + target_creator_final_state.creator_stakes_info.total_staked, + target_creator_init_state.creator_stakes_info.total_staked + expected_amount_to_move + ); + + // Ensure number of backers has been reduced on source creator if it is fully unstaked + let is_source_creator_fully_unstaked = source_creator_init_stake_amount == expected_amount_to_move; + if is_source_creator_fully_unstaked { + assert_eq!( + source_creator_final_state.creator_stakes_info.backers_count, + source_creator_init_state.creator_stakes_info.backers_count - 1, + ); + } + + // Ensure the number of backers has been increased on the target creator + // if it is first stake by the backer + let no_init_stake_on_target_creator = target_creator_init_state + .backer_stakes + .current_stake() + .is_zero(); + if no_init_stake_on_target_creator { + assert_eq!( + target_creator_final_state.creator_stakes_info.backers_count, + target_creator_init_state.creator_stakes_info.backers_count + 1 + ); + } + + // Ensure DB entry has been removed if era stake vector is empty + let fully_unstaked_and_nothing_to_claim = + is_source_creator_fully_unstaked && source_creator_final_state.backer_stakes.clone().claim() == (0, 0); + if fully_unstaked_and_nothing_to_claim { + assert!(!BackerStakesByCreator::::contains_key( + &backer, + &from_creator_id + )); + } +} diff --git a/pallets/creator-staking/src/tests/tests.rs b/pallets/creator-staking/src/tests/tests.rs index b29479f9..bb885db2 100644 --- a/pallets/creator-staking/src/tests/tests.rs +++ b/pallets/creator-staking/src/tests/tests.rs @@ -1831,6 +1831,307 @@ pub fn tvl_util_test() { }) } +// Move stake tests +// ---------------- + +#[test] +fn move_stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let from_creator_id = 1; + let to_creator_id = 2; + + assert_register(stakeholder, from_creator_id); + assert_register(stakeholder, to_creator_id); + assert_stake(backer, from_creator_id, MINIMUM_STAKING_AMOUNT * 2); + + // The first transfer will ensure that both creators are staked after operation is complete + assert_move_stake( + backer, + from_creator_id, + to_creator_id, + MINIMUM_STAKING_AMOUNT, + ); + assert!( + !BackerStakesByCreator::::get(&backer, &from_creator_id) + .current_stake() + .is_zero() + ); + + // The second operation should fully unstake source creator since it takes it below minimum staking amount + assert_move_stake( + backer, + from_creator_id, + to_creator_id, + MINIMUM_STAKING_AMOUNT, + ); + assert!( + BackerStakesByCreator::::get(&backer, &from_creator_id) + .current_stake() + .is_zero() + ); + }) +} + +#[test] +fn move_stake_to_same_creator_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + let stakeholder = 1; + let backer = 2; + let creator_id = 1; + + assert_register(stakeholder, creator_id); + + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + creator_id, + creator_id, + 100, + ), + Error::::CannotMoveStakeToSameCreator + ); + }) +} + +#[test] +fn move_stake_should_work_from_inactive_creator() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + let stake_amount = MINIMUM_STAKING_AMOUNT * 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + assert_stake(backer, source_creator_id, stake_amount); + assert_move_stake(backer, source_creator_id, target_creator_id, stake_amount / 2); + + assert_unregister(stakeholder, source_creator_id); + assert_move_stake(backer, source_creator_id, target_creator_id, stake_amount / 2); + }) +} + +#[test] +fn move_stake_to_inactive_creator_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + let stake_amount = MINIMUM_STAKING_AMOUNT * 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + assert_stake(backer, source_creator_id, stake_amount); + assert_move_stake(backer, source_creator_id, target_creator_id, stake_amount / 2); + + assert_unregister(stakeholder, target_creator_id); + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + 100, + ), + Error::::InactiveCreator + ); + }) +} + +#[test] +fn move_stake_from_not_staked_creator_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + 20, + ), + Error::::NotStakedCreator + ); + }) +} + +#[test] +fn move_zero_stake_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + assert_stake(backer, source_creator_id, 100); + + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + Zero::zero(), + ), + Error::::CannotMoveZeroStake + ); + }) +} + +#[test] +// When target_creator wasn't staked by a backer before, +// moving amount should be more than MinimumStakingAmount +fn move_stake_should_fail_with_insufficient_staking_amount() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + assert_stake(backer, source_creator_id, 100); + + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + MINIMUM_STAKING_AMOUNT - 1, + ), + Error::::InsufficientStakingAmount + ); + }) +} + +#[test] +fn move_stake_with_max_era_stake_items_exceeded_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + let backer = 3; + let source_creator_id = 1; + let target_creator_id = 2; + + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + + // Ensure we fill up era stakes vector + for _ in 1..MAX_ERA_STAKE_ITEMS { + // We use stake since its only limiting factor is max era stake items + assert_stake(backer, source_creator_id, 15); + advance_to_era(CreatorStaking::current_era() + 1); + } + assert_noop!( + CreatorStaking::stake( + RuntimeOrigin::signed(backer), + source_creator_id, + 15 + ), + Error::::TooManyEraStakeValues + ); + + // Ensure it's not possible to transfer from source creator since it's era stake items are at max + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + 15, + ), + Error::::TooManyEraStakeValues + ); + + // Swap source and target to verify that same is true if target creator era stake imtes is maxed out + let (source_creator_id, target_creator_id) = (target_creator_id, source_creator_id); + assert_stake(backer, source_creator_id, 15); + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(backer), + source_creator_id, + target_creator_id, + 15, + ), + Error::::TooManyEraStakeValues + ); + }) +} + +#[test] +fn move_stake_with_max_number_of_backers_exceeded_should_fail() { + ExternalityBuilder::build().execute_with(|| { + initialize_first_block(); + + let stakeholder = 1; + // This one will only stake on source creator + let first_backer = 3; + // This one will stake on both origin and target contracts + let second_backer = 4; + let source_creator_id = 1; + let target_creator_id = 2; + + // Register creators and stake them with both backers + assert_register(stakeholder, source_creator_id); + assert_register(stakeholder, target_creator_id); + + assert_stake(first_backer, source_creator_id, 23); + assert_stake(second_backer, target_creator_id, 37); + assert_stake(second_backer, target_creator_id, 41); + + // Fill up the second creator with backers until max number of backers limit has been reached + for temp_backer in (second_backer + 1)..(MAX_NUMBER_OF_BACKERS as u64 + second_backer) { + Balances::resolve_creating(&temp_backer, Balances::issue(100)); + assert_stake(temp_backer, target_creator_id, 13); + } + // Sanity check + assurance that first_backer isn't staking on target creator + assert_noop!( + CreatorStaking::stake( + RuntimeOrigin::signed(first_backer), + target_creator_id, + 19 + ), + Error::::MaxNumberOfBackersExceeded + ); + + // Now attempt move stake and expect an error + assert_noop!( + CreatorStaking::move_stake( + RuntimeOrigin::signed(first_backer), + source_creator_id, + target_creator_id, + 19, + ), + Error::::MaxNumberOfBackersExceeded + ); + }) +} + // Inflation tests // --------------- diff --git a/pallets/creator-staking/src/types/impls.rs b/pallets/creator-staking/src/types/impls.rs index 5e056b23..6b75500c 100644 --- a/pallets/creator-staking/src/types/impls.rs +++ b/pallets/creator-staking/src/types/impls.rs @@ -72,6 +72,13 @@ impl StakesInfo self.stakes.len() as u32 } + #[cfg(test)] + pub(crate) fn clone(&self) -> Self { + Self { + stakes: self.stakes.clone(), + } + } + fn change_stake( mut stakes: BoundedVec, MaxEraStakeItems>, current_era: EraIndex, diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fcd9fd20..8bf458e8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -176,10 +176,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("subsocial-parachain"), impl_name: create_runtime_str!("subsocial-parachain"), authoring_version: 1, - spec_version: 33, + spec_version: 34, impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 6, + transaction_version: 7, state_version: 0, };