From f19f4f265b356bbaf18749a7284b72f3df9bfbdf Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 8 Oct 2024 13:56:54 -0700 Subject: [PATCH 01/17] WIP: postition voting rewards contract --- Anchor.toml | 2 + Cargo.lock | 12 + programs/position-voting-rewards/Cargo.toml | 29 ++ programs/position-voting-rewards/Xargo.toml | 2 + .../src/create_account.rs | 145 ++++++++++ programs/position-voting-rewards/src/error.rs | 18 ++ .../src/instructions/claim_rewards_v0.rs | 141 +++++++++ .../src/instructions/enroll_v0.rs | 264 +++++++++++++++++ .../src/instructions/mod.rs | 11 + .../src/instructions/reset_lockup_v0.rs | 106 +++++++ .../src/instructions/transfer_v0.rs | 110 +++++++ .../src/instructions/unenroll_v0.rs | 268 +++++++++++++++++ programs/position-voting-rewards/src/lib.rs | 35 +++ programs/position-voting-rewards/src/state.rs | 273 ++++++++++++++++++ programs/position-voting-rewards/src/util.rs | 196 +++++++++++++ .../src/instructions/deposit_v0.rs | 9 +- .../instructions/initialize_position_v0.rs | 30 +- .../instructions/initialize_registrar_v0.rs | 18 +- .../src/instructions/proxied_vote_v0.rs | 5 +- .../src/instructions/relinquish_vote_v1.rs | 3 +- .../src/instructions/vote_v0.rs | 5 +- .../src/state/position.rs | 6 +- .../src/state/registrar.rs | 5 +- 23 files changed, 1654 insertions(+), 39 deletions(-) create mode 100644 programs/position-voting-rewards/Cargo.toml create mode 100644 programs/position-voting-rewards/Xargo.toml create mode 100644 programs/position-voting-rewards/src/create_account.rs create mode 100644 programs/position-voting-rewards/src/error.rs create mode 100644 programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/enroll_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/mod.rs create mode 100644 programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/transfer_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/unenroll_v0.rs create mode 100644 programs/position-voting-rewards/src/lib.rs create mode 100644 programs/position-voting-rewards/src/state.rs create mode 100644 programs/position-voting-rewards/src/util.rs diff --git a/Anchor.toml b/Anchor.toml index 349ce0771..bbf70be50 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -17,6 +17,7 @@ fanout = "fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6" mobile_entity_manager = "memMa1HG4odAFmUbGWfPwS1WWfK95k99F2YTkGvyxZr" hexboosting = "hexbnKYoA2GercNNhHUCCfrTRWrHjT6ujKPXTa5NPqJ" no_emit = "noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv" +position_voting_rewards = "pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM" [workspace] members = [ @@ -34,6 +35,7 @@ members = [ "programs/mobile-entity-manager", "programs/hexboosting", "programs/no-emit", + "programs/position-voting-rewards", ] [registry] diff --git a/Cargo.lock b/Cargo.lock index d4a3596b6..52eefaaa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3013,6 +3013,18 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" +[[package]] +name = "position-voting-rewards" +version = "0.0.1" +dependencies = [ + "anchor-lang", + "anchor-spl", + "default-env", + "shared-utils", + "solana-security-txt", + "voter-stake-registry", +] + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/programs/position-voting-rewards/Cargo.toml b/programs/position-voting-rewards/Cargo.toml new file mode 100644 index 000000000..de24595c6 --- /dev/null +++ b/programs/position-voting-rewards/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "position-voting-rewards" +version = "0.0.1" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "position_voting_rewards" + +[features] +devnet = [] +no-genesis = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[profile.release] +overflow-checks = true + +[dependencies] +anchor-lang = { workspace = true } +anchor-spl = { workspace = true } +shared-utils = { workspace = true } +solana-security-txt = { workspace = true } +voter-stake-registry = { path = "../voter-stake-registry" } +default-env = { workspace = true } diff --git a/programs/position-voting-rewards/Xargo.toml b/programs/position-voting-rewards/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/position-voting-rewards/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/position-voting-rewards/src/create_account.rs b/programs/position-voting-rewards/src/create_account.rs new file mode 100644 index 000000000..794d15a52 --- /dev/null +++ b/programs/position-voting-rewards/src/create_account.rs @@ -0,0 +1,145 @@ +use anchor_lang::{ + prelude::borsh::BorshSerialize, + solana_program::{ + account_info::AccountInfo, + msg, + program::{invoke, invoke_signed}, + program_error::ProgramError, + pubkey::Pubkey, + rent::Rent, + system_instruction::{self, create_account}, + }, +}; + +pub trait AccountMaxSize { + fn get_max_size(&self) -> Option; +} + +/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call +/// The owner of the account is set to the PDA program +/// Note: This functions also checks the provided account PDA matches the supplied seeds +#[allow(clippy::too_many_arguments)] +pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>( + payer_info: &AccountInfo<'a>, + account_info: &AccountInfo<'a>, + account_data: &T, + account_address_seeds: &[&[u8]], + program_id: &Pubkey, + system_info: &AccountInfo<'a>, + rent: &Rent, + extra_lamports: u64, // Extra lamports added on top of the rent exempt amount +) -> std::result::Result<(), ProgramError> { + create_and_serialize_account_with_owner_signed( + payer_info, + account_info, + account_data, + account_address_seeds, + program_id, + program_id, // By default use PDA program_id as the owner of the account + system_info, + rent, + extra_lamports, + ) +} + +/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call +/// Note: This functions also checks the provided account PDA matches the supplied seeds +#[allow(clippy::too_many_arguments)] +pub fn create_and_serialize_account_with_owner_signed<'a, T: BorshSerialize + AccountMaxSize>( + payer_info: &AccountInfo<'a>, + account_info: &AccountInfo<'a>, + account_data: &T, + account_address_seeds: &[&[u8]], + program_id: &Pubkey, + owner_program_id: &Pubkey, + system_info: &AccountInfo<'a>, + rent: &Rent, + extra_lamports: u64, // Extra lamports added on top of the rent exempt amount +) -> std::result::Result<(), ProgramError> { + // Get PDA and assert it's the same as the requested account address + let (account_address, bump_seed) = + Pubkey::find_program_address(account_address_seeds, program_id); + + if account_address != *account_info.key { + msg!( + "Create account with PDA: {:?} was requested while PDA: {:?} was expected", + account_info.key, + account_address + ); + return Err(ProgramError::InvalidSeeds); + } + + let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() { + (None, max_size) + } else { + let serialized_data = account_data.try_to_vec()?; + let account_size = serialized_data.len(); + (Some(serialized_data), account_size) + }; + + let mut signers_seeds = account_address_seeds.to_vec(); + let bump = &[bump_seed]; + signers_seeds.push(bump); + + let rent_exempt_lamports = rent.minimum_balance(account_size); + let total_lamports = rent_exempt_lamports.checked_add(extra_lamports).unwrap(); + + // If the account has some lamports already it can't be created using create_account instruction + // Anybody can send lamports to a PDA and by doing so create the account and perform DoS attack by blocking create_account + if account_info.lamports() > 0 { + let top_up_lamports = total_lamports.saturating_sub(account_info.lamports()); + + if top_up_lamports > 0 { + invoke( + &system_instruction::transfer(payer_info.key, account_info.key, top_up_lamports), + &[ + payer_info.clone(), + account_info.clone(), + system_info.clone(), + ], + )?; + } + + invoke_signed( + &system_instruction::allocate(account_info.key, account_size as u64), + &[account_info.clone(), system_info.clone()], + &[&signers_seeds[..]], + )?; + + invoke_signed( + &system_instruction::assign(account_info.key, owner_program_id), + &[account_info.clone(), system_info.clone()], + &[&signers_seeds[..]], + )?; + } else { + // If the PDA doesn't exist use create_account to use lower compute budget + let create_account_instruction = create_account( + payer_info.key, + account_info.key, + total_lamports, + account_size as u64, + owner_program_id, + ); + + invoke_signed( + &create_account_instruction, + &[ + payer_info.clone(), + account_info.clone(), + system_info.clone(), + ], + &[&signers_seeds[..]], + )?; + } + + if let Some(serialized_data) = serialized_data { + account_info + .data + .borrow_mut() + .copy_from_slice(&serialized_data); + } else if account_size > 0 { + anchor_lang::prelude::borsh::to_writer(&mut account_info.data.borrow_mut()[..], account_data)?; + } + + Ok(()) +} diff --git a/programs/position-voting-rewards/src/error.rs b/programs/position-voting-rewards/src/error.rs new file mode 100644 index 000000000..9f1d0ebdb --- /dev/null +++ b/programs/position-voting-rewards/src/error.rs @@ -0,0 +1,18 @@ +use anchor_lang::prelude::*; + +#[error_code] +pub enum ErrorCode { + MustCalculateVehntLinearly, + #[msg("Rewards enrollment must be disabled to make changes to a position")] + DisableRewardsEnrollment, + #[msg("Epoch must be closed to claim rewards")] + EpochNotClosed, + #[msg("Epoch must be over to claim rewards")] + EpochNotOver, + #[msg("Rewards need to be claimed in the correct epoch order")] + InvalidClaimEpoch, + #[msg("Cannot enroll on a position ending this epoch")] + NoEnrollEndingPosition, + #[msg("Cannot change position while enrolled")] + PositionChangeWhileEnrolled, +} diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs new file mode 100644 index 000000000..a42505403 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -0,0 +1,141 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{transfer, Mint, Token, TokenAccount, Transfer}, +}; +use voter_stake_registry::{ + state::{PositionV0, Registrar}, + VoterStakeRegistry, +}; + +use crate::{error::ErrorCode, state::*, util::current_epoch, vsr_epoch_info_seeds}; +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ClaimRewardsArgsV0 { + pub epoch: u64, +} + +#[derive(Accounts)] +#[instruction(args: ClaimRewardsArgsV0)] +pub struct ClaimRewardsV0<'info> { + #[account( + has_one = rewards_mint, + has_one = registrar, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + #[account( + seeds = [b"position".as_ref(), mint.key().as_ref()], + seeds::program = vsr_program.key(), + bump = position.bump_seed, + has_one = mint, + has_one = registrar, + )] + pub position: Box>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + #[account(mut)] + pub position_authority: Signer<'info>, + pub registrar: Box>, + #[account( + mut, + has_one = vetoken_tracker, + seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], + bump, + )] + pub enrolled_position: Account<'info, EnrolledPositionV0>, + pub rewards_mint: Box>, + + #[account( + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), &args.epoch.to_le_bytes()], + bump, + constraint = vsr_epoch_info.rewards_issued_at.is_some() @ ErrorCode::EpochNotClosed + )] + pub vsr_epoch_info: Box>, + #[account( + mut, + associated_token::mint = rewards_mint, + associated_token::authority = vsr_epoch_info, + )] + pub rewards_pool: Box>, + #[account( + init_if_needed, + payer = position_authority, + associated_token::mint = rewards_mint, + associated_token::authority = position_authority, + )] + pub enrolled_ata: Box>, + + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub token_program: Program<'info, Token>, +} + +impl<'info> ClaimRewardsV0<'info> { + fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> { + let cpi_accounts = Transfer { + from: self.rewards_pool.to_account_info(), + to: self.enrolled_ata.to_account_info(), + authority: self.vsr_epoch_info.to_account_info(), + }; + + CpiContext::new(self.token_program.to_account_info(), cpi_accounts) + } +} + +pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result<()> { + // load the vetokens information + let position = &mut ctx.accounts.position; + let registrar = &ctx.accounts.registrar; + let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; + + let enrolled_position = &mut ctx.accounts.enrolled_position; + + // check epoch that's being claimed is over + let epoch = current_epoch(registrar.clock_unix_timestamp()); + if !TESTING { + require_gt!(epoch, args.epoch, ErrorCode::EpochNotOver); + if enrolled_position.is_claimed(args.epoch)? { + return Err(error!(ErrorCode::InvalidClaimEpoch)); + } + } + + let enrolled_vetokens_at_epoch = + position.voting_power(voting_mint_config, ctx.accounts.vsr_epoch_info.start_ts())?; + + msg!( + "Staked {} veHNT at start of epoch with {} total veTokens and {} total rewards", + enrolled_vetokens_at_epoch, + ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start, + ctx.accounts.vsr_epoch_info.rewards_amount + ); + + // calculate the position's share of that epoch's rewards + // rewards = staking_rewards_issued * staked_vetokens_at_epoch / total_vetokens + let rewards = u64::try_from( + enrolled_vetokens_at_epoch + .checked_mul(ctx.accounts.vsr_epoch_info.rewards_amount as u128) + .unwrap() + .checked_div(ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start as u128) + .unwrap(), + ) + .unwrap(); + + enrolled_position.set_claimed(args.epoch)?; + + let amount_left = ctx.accounts.rewards_pool.amount; + transfer( + ctx + .accounts + .transfer_ctx() + .with_signer(&[vsr_epoch_info_seeds!(ctx.accounts.vsr_epoch_info)]), + // Due to rounding down of vetokens fall rates it's possible the vetokens on the dao does not exactly match the + // vetokens remaining. It could be off by a little bit of dust. + std::cmp::min(rewards, amount_left), + )?; + Ok(()) +} diff --git a/programs/position-voting-rewards/src/instructions/enroll_v0.rs b/programs/position-voting-rewards/src/instructions/enroll_v0.rs new file mode 100644 index 000000000..79e3d5509 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/enroll_v0.rs @@ -0,0 +1,264 @@ +use anchor_lang::{prelude::*, Discriminator}; +use anchor_spl::token::{Mint, TokenAccount}; +use voter_stake_registry::{ + state::{LockupKind, PositionV0, Registrar}, + VoterStakeRegistry, +}; + +use self::borsh::BorshSerialize; +use crate::{ + create_account::{create_and_serialize_account_signed, AccountMaxSize}, + error::ErrorCode, + id, + state::{EnrolledPositionV0, VeTokenTrackerV0, VsrEpochInfoV0}, + util::*, +}; + +#[derive(Accounts)] +pub struct EnrollV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + seeds = [b"position".as_ref(), mint.key().as_ref()], + seeds::program = vsr_program.key(), + bump = position.bump_seed, + has_one = mint, + has_one = registrar, + constraint = position.lockup.kind == LockupKind::Constant || position.lockup.end_ts > registrar.clock_unix_timestamp() + )] + pub position: Box>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + #[account(mut)] + pub position_authority: Signer<'info>, + pub registrar: Box>, + #[account( + mut, + has_one = registrar, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + #[account( + init_if_needed, + payer = payer, + space = 60 + VsrEpochInfoV0::INIT_SPACE, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), ¤t_epoch(registrar.clock_unix_timestamp()).to_le_bytes()], + bump, + constraint = vsr_epoch_info.key() != closing_time_vsr_epoch_info.key() @ ErrorCode::NoEnrollEndingPosition + )] + pub vsr_epoch_info: Box>, + #[account( + init_if_needed, + payer = payer, + space = 60 + VsrEpochInfoV0::INIT_SPACE, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), ¤t_epoch(position.lockup.end_ts).to_le_bytes()], + bump, + )] + pub closing_time_vsr_epoch_info: Box>, + #[account( + mut, + seeds = [ + "vsr_epoch_info".as_bytes(), + vetoken_tracker.key().as_ref(), + ¤t_epoch( + // Avoid passing an extra account if the end is 0 (no genesis on this position). + // Pass instead closing time epoch info, txn account deduplication will reduce the overall tx size + if position.genesis_end <= registrar.clock_unix_timestamp() { + position.lockup.end_ts + } else { + position.genesis_end + } + ).to_le_bytes() + ], + bump, + )] + /// CHECK: Verified when needed in the inner instr + pub genesis_end_vsr_epoch_info: UncheckedAccount<'info>, + + #[account( + init, + space = 60 + 8 + std::mem::size_of::(), + payer = position_authority, + seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], + bump, + )] + pub enrolled_position: Box>, + + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, +} + +pub struct VsrEpochInfoV0WithDescriminator { + pub vsr_epoch_info: VsrEpochInfoV0, +} + +impl BorshSerialize for VsrEpochInfoV0WithDescriminator { + fn serialize(&self, writer: &mut W) -> std::io::Result<()> { + VsrEpochInfoV0::DISCRIMINATOR.serialize(writer)?; + self.vsr_epoch_info.serialize(writer) + } +} + +impl AccountMaxSize for VsrEpochInfoV0WithDescriminator { + fn get_max_size(&self) -> Option { + Some(60 + VsrEpochInfoV0::INIT_SPACE) + } +} + +pub fn handler(ctx: Context) -> Result<()> { + // load the vetokens information + let position = &mut ctx.accounts.position; + let registrar = &ctx.accounts.registrar; + let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; + let curr_ts = registrar.clock_unix_timestamp(); + + let vetokens_info = calculate_vetoken_info(curr_ts, position, voting_mint_config)?; + let VetokenInfo { + has_genesis, + vetokens_at_curr_ts, + pre_genesis_end_fall_rate, + post_genesis_end_fall_rate, + genesis_end_fall_rate_correction, + genesis_end_vetoken_correction, + end_fall_rate_correction, + end_vetoken_correction, + } = vetokens_info; + + msg!("Vehnt calculations: {:?}", vetokens_info); + + let curr_epoch = current_epoch(curr_ts); + + let vetoken_tracker = &mut ctx.accounts.vetoken_tracker; + let enrolled_position = &mut ctx.accounts.enrolled_position; + + // Update the veHnt at start of epoch + ctx.accounts.vsr_epoch_info.epoch = current_epoch(curr_ts); + vetoken_tracker.update_vetokens(&mut ctx.accounts.vsr_epoch_info, curr_ts)?; + + vetoken_tracker.total_vetokens = vetoken_tracker + .total_vetokens + .checked_add(vetokens_at_curr_ts) + .unwrap(); + vetoken_tracker.vetoken_fall_rate = if has_genesis { + vetoken_tracker + .vetoken_fall_rate + .checked_add(pre_genesis_end_fall_rate) + .unwrap() + } else { + vetoken_tracker + .vetoken_fall_rate + .checked_add(post_genesis_end_fall_rate) + .unwrap() + }; + + ctx + .accounts + .closing_time_vsr_epoch_info + .fall_rates_from_closing_positions = ctx + .accounts + .closing_time_vsr_epoch_info + .fall_rates_from_closing_positions + .checked_add(end_fall_rate_correction) + .unwrap(); + + ctx + .accounts + .closing_time_vsr_epoch_info + .vetokens_in_closing_positions = ctx + .accounts + .closing_time_vsr_epoch_info + .vetokens_in_closing_positions + .checked_add(end_vetoken_correction) + .unwrap(); + ctx.accounts.closing_time_vsr_epoch_info.vetoken_tracker = vetoken_tracker.key(); + ctx.accounts.closing_time_vsr_epoch_info.epoch = current_epoch(position.lockup.end_ts); + ctx.accounts.closing_time_vsr_epoch_info.bump_seed = ctx.bumps["closing_time_vsr_epoch_info"]; + + let genesis_end_is_closing = + ctx.accounts.genesis_end_vsr_epoch_info.key() == ctx.accounts.closing_time_vsr_epoch_info.key(); + if position.genesis_end > curr_ts + && (genesis_end_fall_rate_correction > 0 || genesis_end_vetoken_correction > 0) + { + // If the end account doesn't exist, init it. Otherwise just set the correcitons + if !genesis_end_is_closing && ctx.accounts.genesis_end_vsr_epoch_info.data_len() == 0 { + msg!("Genesis end doesn't exist, initting"); + let genesis_end_epoch = current_epoch(position.genesis_end); + // Anchor doesn't natively support dynamic account creation using remaining_accounts + // and we have to take it on the manual drive + create_and_serialize_account_signed( + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.genesis_end_vsr_epoch_info.to_account_info(), + &VsrEpochInfoV0WithDescriminator { + vsr_epoch_info: VsrEpochInfoV0 { + epoch: genesis_end_epoch, + bump_seed: ctx.bumps["genesis_end_vsr_epoch_info"], + vetoken_tracker: vetoken_tracker.key(), + vetokens_at_epoch_start: 0, + vetokens_in_closing_positions: genesis_end_vetoken_correction, + fall_rates_from_closing_positions: genesis_end_fall_rate_correction, + rewards_issued_at: None, + initialized: false, + registrar: ctx.accounts.registrar.key(), + rewards_amount: 0, + }, + }, + &[ + "vsr_epoch_info".as_bytes(), + vetoken_tracker.key().as_ref(), + &genesis_end_epoch.to_le_bytes(), + ], + &id(), + &ctx.accounts.system_program.to_account_info(), + &Rent::get()?, + 0, + )?; + } else { + // closing can be the same account as genesis end. Make sure to use the proper account + let mut parsed: Account; + let genesis_end_vsr_epoch_info: &mut Account = if genesis_end_is_closing { + &mut ctx.accounts.closing_time_vsr_epoch_info + } else { + parsed = Account::try_from(&ctx.accounts.genesis_end_vsr_epoch_info.to_account_info())?; + &mut parsed + }; + + // EDGE CASE: The genesis end could be this epoch. Do not override what was done with update_subdao_vetokens + if genesis_end_vsr_epoch_info.key() == ctx.accounts.vsr_epoch_info.key() { + genesis_end_vsr_epoch_info.fall_rates_from_closing_positions = ctx + .accounts + .vsr_epoch_info + .fall_rates_from_closing_positions; + genesis_end_vsr_epoch_info.vetokens_in_closing_positions = + ctx.accounts.vsr_epoch_info.vetokens_in_closing_positions; + } else { + genesis_end_vsr_epoch_info.fall_rates_from_closing_positions = genesis_end_vsr_epoch_info + .fall_rates_from_closing_positions + .checked_add(genesis_end_fall_rate_correction) + .unwrap(); + + genesis_end_vsr_epoch_info.vetokens_in_closing_positions = genesis_end_vsr_epoch_info + .vetokens_in_closing_positions + .checked_add(genesis_end_vetoken_correction) + .unwrap(); + } + + genesis_end_vsr_epoch_info.exit(&id())?; + } + } + + enrolled_position.start_ts = curr_ts; + enrolled_position.last_claimed_epoch = curr_epoch; + enrolled_position.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + enrolled_position.position = ctx.accounts.position.key(); + enrolled_position.bump_seed = ctx.bumps["enrolled_position"]; + + ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); + ctx.accounts.vsr_epoch_info.initialized = true; + + Ok(()) +} diff --git a/programs/position-voting-rewards/src/instructions/mod.rs b/programs/position-voting-rewards/src/instructions/mod.rs new file mode 100644 index 000000000..e8bbe6cfb --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/mod.rs @@ -0,0 +1,11 @@ +pub mod claim_rewards_v0; +pub mod enroll_v0; +pub mod reset_lockup_v0; +pub mod transfer_v0; +pub mod unenroll_v0; + +pub use claim_rewards_v0::*; +pub use enroll_v0::*; +pub use reset_lockup_v0::*; +pub use transfer_v0::*; +pub use unenroll_v0::*; diff --git a/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs b/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs new file mode 100644 index 000000000..170983441 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs @@ -0,0 +1,106 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, TokenAccount}; +use voter_stake_registry::{ + cpi::{accounts::ResetLockupV0 as VsrResetLockupV0, reset_lockup_v0}, + state::{LockupKind as VsrLockupKind, PositionV0, Registrar}, + ResetLockupArgsV0 as VsrResetLockupArgsV0, VoterStakeRegistry, +}; + +use crate::{error::ErrorCode, state::*}; + +#[derive(Accounts)] +pub struct ResetLockupV0<'info> { + pub registrar: Box>, + #[account( + has_one = registrar, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + #[account( + mut, + seeds = [b"position".as_ref(), mint.key().as_ref()], + bump = position.bump_seed, + seeds::program = vsr_program.key(), + has_one = registrar, + has_one = mint + )] + pub position: Box>, + /// CHECK: This account needs to be closed. You can't reset_lockup while enrolled + #[account( + seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], + bump, + constraint = enrolled_position.data_is_empty() && enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled + )] + pub enrolled_position: UncheckedAccount<'info>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + pub position_authority: Signer<'info>, + pub vsr_program: Program<'info, VoterStakeRegistry>, +} + +#[repr(u8)] +#[derive(Default, AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum LockupKind { + /// No lockup, tokens can be withdrawn as long as not engaged in a proposal. + #[default] + None, + + /// Lock up for a number of days + Cliff, + + /// Lock up permanently. The number of days specified becomes the minimum + /// unlock period when the deposit (or a part of it) is changed to Cliff. + Constant, +} + +impl From for VsrLockupKind { + fn from(kind: LockupKind) -> Self { + match kind { + LockupKind::None => VsrLockupKind::None, + LockupKind::Cliff => VsrLockupKind::Cliff, + LockupKind::Constant => VsrLockupKind::Constant, + } + } +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ResetLockupArgsV0 { + pub kind: LockupKind, + pub periods: u32, +} + +/// Resets a lockup to start at the current slot timestamp and to last for +/// `periods`, which must be >= the number of periods left on the lockup. +/// This will re-lock any non-withdrawn vested funds. +pub fn handler(ctx: Context, args: ResetLockupArgsV0) -> Result<()> { + let ResetLockupArgsV0 { kind, periods } = args; + + reset_lockup_v0( + CpiContext::new_with_signer( + ctx.accounts.vsr_program.to_account_info(), + VsrResetLockupV0 { + registrar: ctx.accounts.registrar.to_account_info(), + position_update_authority: ctx.accounts.vetoken_tracker.to_account_info(), + position: ctx.accounts.position.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + position_token_account: ctx.accounts.position_token_account.to_account_info(), + position_authority: ctx.accounts.position_authority.to_account_info(), + }, + &[&[ + b"vetoken_tracker", + ctx.accounts.vetoken_tracker.registrar.key().as_ref(), + &[ctx.accounts.vetoken_tracker.bump_seed], + ]], + ), + VsrResetLockupArgsV0 { + kind: kind.into(), + periods, + }, + )?; + + Ok(()) +} diff --git a/programs/position-voting-rewards/src/instructions/transfer_v0.rs b/programs/position-voting-rewards/src/instructions/transfer_v0.rs new file mode 100644 index 000000000..d01ff4fb2 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/transfer_v0.rs @@ -0,0 +1,110 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use voter_stake_registry::{ + cpi::{accounts::TransferV0 as VsrTransferV0, transfer_v0}, + state::{PositionV0, Registrar}, + TransferArgsV0 as VsrTransferArgsV0, VoterStakeRegistry, +}; + +use crate::{error::ErrorCode, state::*}; + +#[derive(Accounts)] +pub struct TransferV0<'info> { + pub registrar: Box>, + #[account( + has_one = registrar, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + #[account( + mut, + seeds = [b"position".as_ref(), mint.key().as_ref()], + seeds::program = vsr_program.key(), + bump = source_position.bump_seed, + has_one = registrar, + has_one = mint + )] + pub source_position: Box>, + /// CHECK: This account needs to be closed. You can't transfer while enrolled + #[account( + seeds = ["enrolled_position".as_bytes(), source_position.key().as_ref()], + bump, + constraint = source_enrolled_position.data_is_empty() && source_enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled + )] + pub source_enrolled_position: UncheckedAccount<'info>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + pub position_authority: Signer<'info>, + #[account( + mut, + has_one = registrar, + )] + pub target_position: Box>, + /// CHECK: This account needs to be closed. You can't transfer while enrolled + #[account( + seeds = ["enrolled_position".as_bytes(), target_position.key().as_ref()], + bump, + constraint = target_enrolled_position.data_is_empty() && target_enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled + )] + pub target_enrolled_position: UncheckedAccount<'info>, + pub deposit_mint: Box>, + #[account( + mut, + associated_token::authority = source_position, + associated_token::mint = deposit_mint, + )] + pub source_vault: Box>, + #[account( + mut, + associated_token::authority = target_position, + associated_token::mint = deposit_mint, + )] + pub target_vault: Box>, + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub token_program: Program<'info, Token>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct TransferArgsV0 { + pub amount: u64, +} + +pub fn handler(ctx: Context, args: TransferArgsV0) -> Result<()> { + transfer_v0( + CpiContext::new_with_signer( + ctx.accounts.vsr_program.to_account_info(), + VsrTransferV0 { + registrar: ctx.accounts.registrar.to_account_info(), + position_update_authority: ctx.accounts.vetoken_tracker.to_account_info(), + source_position: ctx.accounts.source_position.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + position_token_account: ctx.accounts.position_token_account.to_account_info(), + position_authority: ctx.accounts.position_authority.to_account_info(), + target_position: ctx.accounts.target_position.to_account_info(), + deposit_mint: ctx.accounts.deposit_mint.to_account_info(), + source_vault: ctx.accounts.source_vault.to_account_info(), + target_vault: ctx.accounts.target_vault.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + }, + &[&[ + b"vetoken_tracker", + ctx.accounts.vetoken_tracker.registrar.key().as_ref(), + &[ctx.accounts.vetoken_tracker.bump_seed], + ]], + ), + VsrTransferArgsV0 { + amount: args.amount, + }, + )?; + + Ok(()) +} diff --git a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs new file mode 100644 index 000000000..7076408ce --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs @@ -0,0 +1,268 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, TokenAccount}; +use voter_stake_registry::{ + state::{LockupKind, PositionV0, Registrar}, + VoterStakeRegistry, +}; + +use crate::{ + id, + state::*, + util::{calculate_vetoken_info, current_epoch, VetokenInfo}, +}; + +#[derive(Accounts)] +pub struct UnenrollV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + seeds = [b"position".as_ref(), mint.key().as_ref()], + seeds::program = vsr_program.key(), + bump = position.bump_seed, + has_one = mint, + has_one = registrar, + )] + pub position: Box>, + pub mint: Box>, + #[account( + token::mint = mint, + token::authority = position_authority, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + #[account(mut)] + pub position_authority: Signer<'info>, + pub registrar: Box>, + #[account( + mut, + has_one = registrar, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + + #[account( + mut, + has_one = vetoken_tracker, + seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], + bump = enrolled_position.bump_seed, + has_one = position, + )] + pub enrolled_position: Account<'info, EnrolledPositionV0>, + #[account( + init_if_needed, + payer = payer, + space = 60 + VsrEpochInfoV0::INIT_SPACE, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), ¤t_epoch(registrar.clock_unix_timestamp()).to_le_bytes()], + bump, + )] + pub vsr_epoch_info: Box>, + // We know these two accounts are initialized because + // They were used when delegate_v0 was called + #[account( + mut, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), ¤t_epoch(position.lockup.end_ts).to_le_bytes()], + bump = closing_time_vsr_epoch_info.bump_seed, + )] + pub closing_time_vsr_epoch_info: Box>, + #[account( + mut, + seeds = [ + "vsr_epoch_info".as_bytes(), + vetoken_tracker.key().as_ref(), + ¤t_epoch( + // If the genesis piece is no longer in effect (has been purged), + // no need to pass an extra account here. Just pass the closing time sdei and + // do not change it. + if position.genesis_end <= registrar.clock_unix_timestamp() { + position.lockup.end_ts + } else { + position.genesis_end + } + ).to_le_bytes() + ], + bump = genesis_end_vsr_epoch_info.bump_seed, + )] + pub genesis_end_vsr_epoch_info: Box>, + + pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context) -> Result<()> { + // load the vetokens information + let position = &mut ctx.accounts.position; + let registrar = &ctx.accounts.registrar; + let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; + let curr_ts = registrar.clock_unix_timestamp(); + let vetokens_at_curr_ts = position.voting_power(voting_mint_config, curr_ts)?; + let vetokens_info = calculate_vetoken_info( + ctx.accounts.enrolled_position.start_ts, + position, + voting_mint_config, + )?; + ctx.accounts.enrolled_position.is_rewards_enrolled = false; + + let VetokenInfo { + pre_genesis_end_fall_rate, + post_genesis_end_fall_rate, + genesis_end_fall_rate_correction, + genesis_end_vetoken_correction: genesis_end_vetokens_correction, + end_fall_rate_correction, + end_vetoken_correction: end_vetokens_correction, + .. + } = vetokens_info; + + msg!("Vehnt calculations: {:?}", vetokens_info); + + // don't allow unstake without claiming available rewards + // make sure to account for when the position ends + // unless we're testing, in which case we don't care + let curr_epoch = current_epoch(curr_ts); + let to_claim_to_epoch = + if position.lockup.end_ts < curr_ts && position.lockup.kind == LockupKind::Cliff { + current_epoch(position.lockup.end_ts) - 1 + } else { + curr_epoch - 1 + }; + assert!((ctx.accounts.enrolled_position.last_claimed_epoch >= to_claim_to_epoch) || TESTING); + + let enrolled_position = &mut ctx.accounts.enrolled_position; + let vetoken_tracker = &mut ctx.accounts.vetoken_tracker; + + ctx.accounts.vsr_epoch_info.epoch = current_epoch(curr_ts); + vetoken_tracker.update_vetokens(&mut ctx.accounts.vsr_epoch_info, curr_ts)?; + + // Update the ending epochs with this new info + if ctx.accounts.closing_time_vsr_epoch_info.epoch > curr_epoch { + ctx + .accounts + .closing_time_vsr_epoch_info + .fall_rates_from_closing_positions = ctx + .accounts + .closing_time_vsr_epoch_info + .fall_rates_from_closing_positions + .checked_sub(end_fall_rate_correction) + .unwrap(); + + ctx + .accounts + .closing_time_vsr_epoch_info + .vetokens_in_closing_positions = ctx + .accounts + .closing_time_vsr_epoch_info + .vetokens_in_closing_positions + .saturating_sub(end_vetokens_correction); + } + + // Closing time and genesis end can be the same account + let mut parsed: Account; + let end_and_genesis_same = + ctx.accounts.genesis_end_vsr_epoch_info.key() == ctx.accounts.closing_time_vsr_epoch_info.key(); + let genesis_end_vsr_epoch_info: &mut Account = if end_and_genesis_same { + &mut ctx.accounts.closing_time_vsr_epoch_info + } else { + parsed = Account::try_from(&ctx.accounts.genesis_end_vsr_epoch_info.to_account_info())?; + &mut parsed + }; + + // Once start ts passes, everything gets purged. We only + // need this correction when the epoch has not passed + if position.genesis_end > curr_ts && ctx.accounts.genesis_end_vsr_epoch_info.start_ts() > curr_ts + { + genesis_end_vsr_epoch_info.fall_rates_from_closing_positions = genesis_end_vsr_epoch_info + .fall_rates_from_closing_positions + .checked_sub(genesis_end_fall_rate_correction) + .unwrap(); + + genesis_end_vsr_epoch_info.vetokens_in_closing_positions = genesis_end_vsr_epoch_info + .vetokens_in_closing_positions + .saturating_sub(genesis_end_vetokens_correction); + + genesis_end_vsr_epoch_info.exit(&id())?; + ctx.accounts.genesis_end_vsr_epoch_info.reload()?; + } + + // Only subtract from the stake if the position ends after the end of this epoch. Otherwise, + // the position was already purged due to the vsr_epoch_info closing info logic. + if position.lockup.end_ts >= ctx.accounts.vsr_epoch_info.end_ts() + || position.lockup.kind == LockupKind::Constant + { + msg!( + "Current vetokens {}, removing {} from the subdao", + vetoken_tracker.total_vetokens, + vetokens_at_curr_ts + ); + // remove this stake information from the subdao + vetoken_tracker.total_vetokens = vetoken_tracker + .total_vetokens + .saturating_sub(vetokens_at_curr_ts); + + vetoken_tracker.vetoken_fall_rate = vetoken_tracker + .vetoken_fall_rate + .checked_sub(if curr_ts >= position.genesis_end { + post_genesis_end_fall_rate + } else { + pre_genesis_end_fall_rate + }) + .unwrap(); + } + // If the position was staked before this epoch, remove it. + if current_epoch(enrolled_position.start_ts) < curr_epoch { + let vetokens_at_start = + position.voting_power(voting_mint_config, ctx.accounts.vsr_epoch_info.start_ts())?; + msg!( + "Removing {} vetokens from this epoch for this subdao, which currently has {} vetokens", + vetokens_at_start, + ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start + ); + ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start = ctx + .accounts + .vsr_epoch_info + .vetokens_at_epoch_start + .saturating_sub(vetokens_at_start); + } + + ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); + ctx.accounts.vsr_epoch_info.initialized = true; + + // EDGE CASE: When the closing time epoch infos are the same as the current epoch info, + // update_subdao_vetokens will have already removed the fall rates and vetokens from the sub dao. + // Unfortunately, these changes aren't persisted across the various clones of the account, only + // on the main vsr_epoch_info. When the accounts are exited after this call, they will save + // with non-zero fall rates and vetokens in closing positions, causing a double-count. + // Example txs here: + // https://explorer.solana.com/tx/2Mcj4y7K5rE5ioFLKGBynNyX6S56NkfhQscdB3tB9M7wBsWFxWFg6R7vLGRnohsCyLt1U2ba166GUwd9DhU9Af9H + // https://explorer.solana.com/tx/T1TLfyfZyE6iJE9BhjMXkMVRtEUsS1jP3Q9AbNKvvtDpe5HxmVmqp9yT4H7HjdLKt6Q553Vrc7JcQCJeqpqZkK3 + if ctx.accounts.closing_time_vsr_epoch_info.key() == ctx.accounts.vsr_epoch_info.key() { + ctx + .accounts + .closing_time_vsr_epoch_info + .vetokens_in_closing_positions = 0; + ctx + .accounts + .closing_time_vsr_epoch_info + .fall_rates_from_closing_positions = 0; + } + + if ctx.accounts.genesis_end_vsr_epoch_info.key() == ctx.accounts.vsr_epoch_info.key() { + ctx + .accounts + .genesis_end_vsr_epoch_info + .vetokens_in_closing_positions = 0; + ctx + .accounts + .genesis_end_vsr_epoch_info + .fall_rates_from_closing_positions = 0; + } + + // If the lockup is expired or no tokens staked, close the enrollment. + if ctx.accounts.position.lockup.expired(curr_ts) + || ctx.accounts.position.amount_deposited_native == 0 + { + ctx + .accounts + .enrolled_position + .close(ctx.accounts.position_authority.to_account_info())?; + } + Ok(()) +} diff --git a/programs/position-voting-rewards/src/lib.rs b/programs/position-voting-rewards/src/lib.rs new file mode 100644 index 000000000..369092abe --- /dev/null +++ b/programs/position-voting-rewards/src/lib.rs @@ -0,0 +1,35 @@ +use anchor_lang::prelude::*; +#[cfg(not(feature = "no-entrypoint"))] +use {default_env::default_env, solana_security_txt::security_txt}; + +declare_id!("pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM"); + +use anchor_spl::token::{Mint, Token, TokenAccount}; + +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + name: "Position Voting Rewards", + project_url: "http://helium.com", + contacts: "email:hello@helium.foundation", + policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md", + + + // Optional Fields + preferred_languages: "en", + source_code: "https://github.com/helium/helium-program-library/tree/master/programs/position-voting-rewards", + source_revision: default_env!("GITHUB_SHA", ""), + source_release: default_env!("GITHUB_REF_NAME", ""), + auditors: "Sec3" +} + +pub mod create_account; +pub mod error; +pub mod instructions; +pub mod state; +pub mod util; + +#[program] +pub mod position_voting_rewards { + + use super::*; +} diff --git a/programs/position-voting-rewards/src/state.rs b/programs/position-voting-rewards/src/state.rs new file mode 100644 index 000000000..814dfafe1 --- /dev/null +++ b/programs/position-voting-rewards/src/state.rs @@ -0,0 +1,273 @@ +use std::cmp::Ordering; + +use anchor_lang::prelude::*; + +use crate::{ + error::ErrorCode, + util::{apply_fall_rate_factor, current_epoch, EPOCH_LENGTH}, +}; + +#[account] +#[derive(Default)] +pub struct EnrolledPositionV0 { + pub vetoken_tracker: Pubkey, + pub registrar: Pubkey, + pub position: Pubkey, + pub start_ts: i64, + pub is_rewards_enrolled: bool, + pub recent_proposals: [Pubkey; 4], + pub last_claimed_epoch: u64, // the latest epoch not included claimed_epochs_bitmap + // A bitmap of epochs past last_claimed_epoch (exclusive) that have been claimed. + // This bitmap gets rotated as last_claimed_epoch increases. + // This allows for claiming ~128 epochs worth of rewards in parallel. + pub claimed_epochs_bitmap: u128, + pub bump_seed: u8, +} + +pub const TESTING: bool = std::option_env!("TESTING").is_some(); + +impl EnrolledPositionV0 { + pub fn is_claimed(&self, epoch: u64) -> Result { + if epoch <= self.last_claimed_epoch { + Ok(true) + } else if epoch > self.last_claimed_epoch + 128 { + Err(error!(ErrorCode::InvalidClaimEpoch)) + } else { + let bit_index = (epoch - self.last_claimed_epoch - 1) as u128; + Ok(self.claimed_epochs_bitmap >> (127_u128 - bit_index) & 1 == 1) + } + } + + pub fn set_claimed(&mut self, epoch: u64) -> Result<()> { + if epoch <= self.last_claimed_epoch { + Err(error!(ErrorCode::InvalidClaimEpoch)) + } else if epoch > self.last_claimed_epoch + 128 { + Err(error!(ErrorCode::InvalidClaimEpoch)) + } else { + let bit_index = (epoch - self.last_claimed_epoch - 1) as u128; + // Set the bit at bit_index to 1 + self.claimed_epochs_bitmap |= 1_u128 << (127_u128 - bit_index); + + // Shift claimed_epochs_bitmap to the left until the first bit is 0 + while self.claimed_epochs_bitmap & (1_u128 << 127) != 0 { + self.claimed_epochs_bitmap <<= 1; + self.last_claimed_epoch += 1; + } + + Ok(()) + } + } + + // Add a proposal to the recent proposals list + pub fn add_recent_proposal(&mut self, proposal_key: Pubkey, valid_proposals: &[Pubkey]) { + self.clean_recent_proposals(valid_proposals); + + let recent_proposals = &mut self.recent_proposals; + if !recent_proposals.contains(&proposal_key) && valid_proposals.contains(&proposal_key) { + if let Some(index) = recent_proposals + .iter() + .position(|&x| x == Pubkey::default()) + { + recent_proposals[index] = proposal_key; + } else { + // Shift array to the left + for i in 0..recent_proposals.len() - 1 { + recent_proposals[i] = recent_proposals[i + 1]; + } + recent_proposals[recent_proposals.len() - 1] = proposal_key; + } + } + } + + // Remove a proposal from the recent proposals list + pub fn remove_recent_proposal(&mut self, proposal_key: Pubkey, valid_proposals: &[Pubkey]) { + self.clean_recent_proposals(valid_proposals); + + let recent_proposals = &mut self.recent_proposals; + if let Some(index) = recent_proposals.iter().position(|&x| x == proposal_key) { + // Shift elements to the left, overwriting the removed proposal + for i in index..recent_proposals.len() - 1 { + recent_proposals[i] = recent_proposals[i + 1]; + } + // Set the last element to Pubkey::default() + recent_proposals[recent_proposals.len() - 1] = Pubkey::default(); + } + } + + // Remove all proposals that are not in the valid_proposals list + fn clean_recent_proposals(&mut self, valid_proposals: &[Pubkey]) { + for proposal in self.recent_proposals.iter_mut() { + if *proposal != Pubkey::default() && !valid_proposals.contains(proposal) { + *proposal = Pubkey::default(); + } + } + self.compact_recent_proposals(); + } + + // Remove all Pubkey defaults and place them at the end of the array + fn compact_recent_proposals(&mut self) { + let non_default = self + .recent_proposals + .iter() + .filter(|&&pubkey| pubkey != Pubkey::default()) + .cloned() + .collect::>(); + + self.recent_proposals.fill(Pubkey::default()); + self.recent_proposals[..non_default.len()].copy_from_slice(&non_default); + } +} + +#[account] +#[derive(InitSpace)] +pub struct VsrEpochInfoV0 { + pub epoch: u64, + pub vetoken_tracker: Pubkey, + pub registrar: Pubkey, + pub initialized: bool, + pub vetokens_at_epoch_start: u128, + /// The number of enrollment rewards issued this epoch, so that enrollies can claim their share of the rewards + pub rewards_amount: u64, + /// The vetokens amount associated with positions that are closing this epoch. This is the amount that will be subtracted from the tracker + /// total vetokens after the epoch passes. Typically these positions close somewhere between the epoch start and end time, so we cannot rely + /// on fall rate calculations alone without knowing the exact end date of each position. Instead, just keep track of what needs to be + /// removed. + pub vetokens_in_closing_positions: u128, + /// The vetokens amount that is decaying per second, with 12 decimals of extra precision. Associated with positions that are closing this epoch, + /// which means they must be subtracted from the total fall rate on the tracker after this epoch passes + pub fall_rates_from_closing_positions: u128, + pub bump_seed: u8, + /// The program only needs to know whether or not rewards were issued, however having a history of when they were issued could prove + /// useful in the future, or at least for debugging purposes + pub rewards_issued_at: Option, +} + +#[macro_export] +macro_rules! vsr_epoch_info_seeds { + ( $ei:expr ) => { + &[ + b"vsr_epoch_info".as_ref(), + $ei.vetoken_tracker.as_ref(), + &$ei.epoch.to_le_bytes(), + &[$ei.bump_seed], + ] + }; +} + +impl VsrEpochInfoV0 { + pub fn start_ts(&self) -> i64 { + i64::try_from(self.epoch).unwrap() * EPOCH_LENGTH + } + + pub fn end_ts(&self) -> i64 { + i64::try_from(self.epoch + 1).unwrap() * EPOCH_LENGTH + } +} + +#[account] +pub struct VeTokenTrackerV0 { + pub registrar: Pubkey, + pub rewards_mint: Pubkey, + pub vetoken_last_calculated_ts: i64, + pub vetoken_fall_rate: u128, // the vetoken amount that the position decays by per second, with 12 decimals of extra precision + pub total_vetokens: u128, // the total amount of vetoken staked to this subdao, with 12 decimals of extra precision + pub recent_proposals: [Pubkey; 4], + pub bump_seed: u8, +} + +impl VeTokenTrackerV0 { + pub fn update_vetokens( + &mut self, + curr_epoch_info: &mut VsrEpochInfoV0, + curr_ts: i64, + ) -> Result<()> { + if curr_ts < self.vetoken_last_calculated_ts { + return Ok(()); + } + + msg!( + "Current vetoken is {} with last updated of {}. Fast forwarding to {} at fall rate {}", + self.total_vetokens, + self.vetoken_last_calculated_ts, + curr_ts, + self.vetoken_fall_rate + ); + + // If last calculated was more than an epoch ago + let epoch_start = curr_epoch_info.start_ts(); + if epoch_start + .checked_sub(self.vetoken_last_calculated_ts) + .unwrap() + > EPOCH_LENGTH + && !TESTING + // Allow this check to be bypassed when testing so we can run + // checks against this method without having to update _every_ epoch + { + return Err(error!(ErrorCode::MustCalculateVehntLinearly)); + } + + // Step 1. Update veHNT up to the point that this epoch starts + if epoch_start > self.vetoken_last_calculated_ts { + let fall = self + .vetoken_fall_rate + .checked_mul( + u128::try_from(epoch_start) + .unwrap() + .checked_sub(u128::try_from(self.vetoken_last_calculated_ts).unwrap()) + .unwrap(), + ) + .unwrap(); + + self.total_vetokens = self.total_vetokens.checked_sub(fall).unwrap(); + self.vetoken_last_calculated_ts = epoch_start; + } + + // If sub dao epoch info account was just created, log the vetoken + if !curr_epoch_info.initialized { + msg!("Setting vetoken_at_epoch_start to {}", self.total_vetokens,); + curr_epoch_info.vetokens_at_epoch_start = + apply_fall_rate_factor(self.total_vetokens).unwrap(); + } + // Step 2. Update fall rate according to this epoch's closed position corrections + if curr_epoch_info.fall_rates_from_closing_positions > 0 + || curr_epoch_info.vetokens_in_closing_positions > 0 + { + msg!( + "Correcting fall rate by {} and vetoken by {} due to closed positions", + curr_epoch_info.fall_rates_from_closing_positions, + curr_epoch_info.vetokens_in_closing_positions + ); + self.vetoken_fall_rate = self + .vetoken_fall_rate + .checked_sub(curr_epoch_info.fall_rates_from_closing_positions) + .unwrap(); + + self.total_vetokens = self + .total_vetokens + .saturating_sub(curr_epoch_info.vetokens_in_closing_positions); + // Since this has already been applied, set to 0 + curr_epoch_info.fall_rates_from_closing_positions = 0; + curr_epoch_info.vetokens_in_closing_positions = 0; + } + + // Step 3. Update veHNT up to now (from start of epoch) using the current fall rate. At this point, closing positions are effectively ignored. + if current_epoch(curr_ts) == curr_epoch_info.epoch { + let fall = self + .vetoken_fall_rate + .checked_mul( + u128::try_from(curr_ts) + .unwrap() + .checked_sub( + u128::try_from(std::cmp::max(self.vetoken_last_calculated_ts, epoch_start)).unwrap(), + ) + .unwrap(), + ) + .unwrap(); + + self.total_vetokens = self.total_vetokens.saturating_sub(fall); + self.vetoken_last_calculated_ts = curr_ts; + } + + Ok(()) + } +} diff --git a/programs/position-voting-rewards/src/util.rs b/programs/position-voting-rewards/src/util.rs new file mode 100644 index 000000000..33197b295 --- /dev/null +++ b/programs/position-voting-rewards/src/util.rs @@ -0,0 +1,196 @@ +use std::{cmp::Ordering, convert::TryInto}; + +use anchor_lang::prelude::*; +use voter_stake_registry::state::{LockupKind, PositionV0, VotingMintConfigV0}; + +pub const EPOCH_LENGTH: i64 = 24 * 60 * 60; + +pub fn current_epoch(unix_timestamp: i64) -> u64 { + (unix_timestamp / (EPOCH_LENGTH)).try_into().unwrap() +} + +pub fn next_epoch_ts(unix_timestamp: i64) -> u64 { + (current_epoch(unix_timestamp) + 1) * u64::try_from(EPOCH_LENGTH).unwrap() +} + +pub const FALL_RATE_FACTOR: u128 = 1_000_000_000_000; + +pub fn calculate_fall_rate(curr_vp: u128, future_vp: u128, num_seconds: u64) -> Option { + if num_seconds == 0 { + return Some(0); + } + + let diff: u128 = curr_vp.checked_sub(future_vp).unwrap(); + + diff.checked_div(num_seconds.into()) +} + +#[derive(Debug)] +pub struct VetokenInfo { + pub has_genesis: bool, + pub vetokens_at_curr_ts: u128, + pub pre_genesis_end_fall_rate: u128, + pub post_genesis_end_fall_rate: u128, + pub genesis_end_vetoken_correction: u128, + pub genesis_end_fall_rate_correction: u128, + pub end_vetoken_correction: u128, + pub end_fall_rate_correction: u128, +} +pub fn calculate_vetoken_info( + curr_ts: i64, + position: &PositionV0, + voting_mint_config: &VotingMintConfigV0, +) -> Result { + let vetokens_at_curr_ts = position.voting_power(voting_mint_config, curr_ts)?; + + let has_genesis = position.genesis_end > curr_ts; + let seconds_to_genesis = if has_genesis { + u64::try_from( + position + .genesis_end + .checked_sub(curr_ts) + .unwrap() + // Genesis end is inclusive (the genesis will go away at exactly genesis end), so subtract 1 second + // We want to calculate the fall rates before genesis ends + .checked_sub(1) + .unwrap(), + ) + .unwrap() + } else { + 0 + }; + let seconds_from_genesis_to_end = if has_genesis { + u64::try_from( + position + .lockup + .end_ts + .checked_sub(position.genesis_end) + .unwrap(), + ) + .unwrap() + } else { + position.lockup.seconds_left(curr_ts) + }; + // One second before genesis end, the last moment we have the multiplier + let vetokens_at_genesis_end = position.voting_power( + voting_mint_config, + curr_ts + .checked_add(i64::try_from(seconds_to_genesis).unwrap()) + .unwrap(), + )?; + let vetokens_at_genesis_end_exact = if has_genesis { + position.voting_power(voting_mint_config, position.genesis_end)? + } else { + position.voting_power(voting_mint_config, curr_ts)? + }; + let vetokens_at_position_end = + position.voting_power(voting_mint_config, position.lockup.end_ts)?; + + let pre_genesis_end_fall_rate = calculate_fall_rate( + vetokens_at_curr_ts, + vetokens_at_genesis_end, + seconds_to_genesis, + ) + .unwrap(); + let post_genesis_end_fall_rate = calculate_fall_rate( + vetokens_at_genesis_end_exact, + vetokens_at_position_end, + seconds_from_genesis_to_end, + ) + .unwrap(); + + let mut genesis_end_vetokens_correction = 0; + let mut genesis_end_fall_rate_correction = 0; + if has_genesis { + let genesis_end_epoch_start_ts = + i64::try_from(current_epoch(position.genesis_end)).unwrap() * EPOCH_LENGTH; + + if position.lockup.kind == LockupKind::Cliff { + genesis_end_fall_rate_correction = pre_genesis_end_fall_rate + .checked_sub(post_genesis_end_fall_rate) + .unwrap(); + } + + // Subtract the genesis bonus from the vetokens. + // When we do this, we're overcorrecting because the fall rate (corrected to post-genesis) + // is also taking off vetokens for the time period between closing info start and genesis end. + // So add that fall rate back in. + // Only do this if the genesis end epoch isn't the same as the position end epoch. + // If these are the same, then the full vetokens at epoch start is already being taken off. + if position.lockup.kind == LockupKind::Constant + || current_epoch(position.genesis_end) != current_epoch(position.lockup.end_ts) + { + // edge case, if the genesis end is _exactly_ the start of the epoch, getting the voting power at the epoch start + // will not include the genesis. When this happens, we'll miss a vetokens correction + if genesis_end_epoch_start_ts == position.genesis_end { + genesis_end_vetokens_correction = position + .voting_power(voting_mint_config, genesis_end_epoch_start_ts - 1)? + .checked_sub(vetokens_at_genesis_end_exact) + .unwrap(); + } else { + genesis_end_vetokens_correction = position + .voting_power(voting_mint_config, genesis_end_epoch_start_ts)? + .checked_sub(vetokens_at_genesis_end_exact) + .unwrap() + // Correction factor + .checked_sub( + post_genesis_end_fall_rate + .checked_mul( + u128::try_from( + position + .genesis_end + .checked_sub(genesis_end_epoch_start_ts) + .unwrap(), + ) + .unwrap(), + ) + .unwrap(), + ) + .unwrap(); + } + } + } + + let mut end_fall_rate_correction = 0; + let mut end_vetokens_correction = 0; + if position.lockup.kind == LockupKind::Cliff { + let end_epoch_start_ts = + i64::try_from(current_epoch(position.lockup.end_ts)).unwrap() * EPOCH_LENGTH; + let vetokens_at_closing_epoch_start = + position.voting_power(voting_mint_config, end_epoch_start_ts)?; + + end_vetokens_correction = vetokens_at_closing_epoch_start; + end_fall_rate_correction = post_genesis_end_fall_rate; + } + + Ok(VetokenInfo { + has_genesis, + pre_genesis_end_fall_rate, + post_genesis_end_fall_rate, + vetokens_at_curr_ts, + genesis_end_fall_rate_correction, + genesis_end_vetoken_correction: genesis_end_vetokens_correction, + end_fall_rate_correction, + end_vetoken_correction: end_vetokens_correction, + }) +} + +// Use bankers rounding +pub fn apply_fall_rate_factor(item: u128) -> Option { + let fall_rate_sub_one = FALL_RATE_FACTOR / 10; + let lsb = item.checked_div(fall_rate_sub_one).unwrap() % 10; + let round_divide = item.checked_div(FALL_RATE_FACTOR).unwrap(); + let last_seen_bit = round_divide % 10; + match lsb.cmp(&5) { + Ordering::Equal => { + // bankers round + if last_seen_bit % 2 == 0 { + Some(round_divide) + } else { + round_divide.checked_add(1) + } + } + Ordering::Less => Some(round_divide), + Ordering::Greater => round_divide.checked_add(1), + } +} diff --git a/programs/voter-stake-registry/src/instructions/deposit_v0.rs b/programs/voter-stake-registry/src/instructions/deposit_v0.rs index bb45801ea..ec2c26f9e 100644 --- a/programs/voter-stake-registry/src/instructions/deposit_v0.rs +++ b/programs/voter-stake-registry/src/instructions/deposit_v0.rs @@ -1,8 +1,7 @@ -use crate::error::VsrError; -use crate::state::*; use anchor_lang::prelude::*; -use anchor_spl::token::Mint; -use anchor_spl::token::{self, Token, TokenAccount}; +use anchor_spl::token::{self, Mint, Token, TokenAccount}; + +use crate::{error::VsrError, state::*}; #[derive(Accounts)] pub struct DepositV0<'info> { @@ -10,7 +9,7 @@ pub struct DepositV0<'info> { #[account( mut, - has_one = registrar + has_one = registrar, )] pub position: Box>, diff --git a/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs b/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs index e49345a72..7d7d867a2 100644 --- a/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs +++ b/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs @@ -1,20 +1,20 @@ -use crate::error::VsrError; -use crate::position_seeds; -use crate::registrar_seeds; -use crate::state::*; +use std::{convert::TryFrom, mem::size_of}; + use anchor_lang::prelude::*; -use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token; -use anchor_spl::token::FreezeAccount; -use anchor_spl::token::{Mint, MintTo, Token, TokenAccount}; -use mpl_token_metadata::types::Collection; -use mpl_token_metadata::types::DataV2; -use shared_utils::create_metadata_accounts_v3; -use shared_utils::token_metadata::{ - verify_sized_collection_item, CreateMetadataAccountsV3, Metadata, VerifyCollectionItem, +use anchor_spl::{ + associated_token::AssociatedToken, + token, + token::{FreezeAccount, Mint, MintTo, Token, TokenAccount}, +}; +use mpl_token_metadata::types::{Collection, DataV2}; +use shared_utils::{ + create_metadata_accounts_v3, + token_metadata::{ + verify_sized_collection_item, CreateMetadataAccountsV3, Metadata, VerifyCollectionItem, + }, }; -use std::convert::TryFrom; -use std::mem::size_of; + +use crate::{error::VsrError, position_seeds, registrar_seeds, state::*}; #[cfg(feature = "devnet")] const URL: &str = "https://positions.nft.test-helium.com"; diff --git a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs index a83bd1fd1..1fc95de9e 100644 --- a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs +++ b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs @@ -1,17 +1,20 @@ -use crate::state::*; +use std::mem::size_of; + use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, token::{self, Mint, MintTo, Token, TokenAccount}, }; -use mpl_token_metadata::types::CollectionDetails; -use mpl_token_metadata::types::DataV2; +use mpl_token_metadata::types::{CollectionDetails, DataV2}; use nft_proxy::ProxyConfigV0; -use shared_utils::create_metadata_accounts_v3; -use shared_utils::token_metadata::{ - create_master_edition_v3, CreateMasterEditionV3, CreateMetadataAccountsV3, Metadata, +use shared_utils::{ + create_metadata_accounts_v3, + token_metadata::{ + create_master_edition_v3, CreateMasterEditionV3, CreateMetadataAccountsV3, Metadata, + }, }; -use std::mem::size_of; + +use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)] pub struct InitializeRegistrarArgsV0 { @@ -189,6 +192,7 @@ pub fn handler(ctx: Context, args: InitializeRegistrarArg collection_bump_seed: ctx.bumps["collection"], reserved1: [0; 4], reserved2: [0; 3], + recent_proposals: [Pubkey::default(); 4], voting_mints: Vec::new(), proxy_config: ctx .accounts diff --git a/programs/voter-stake-registry/src/instructions/proxied_vote_v0.rs b/programs/voter-stake-registry/src/instructions/proxied_vote_v0.rs index 67cb567ee..4622fdbb6 100644 --- a/programs/voter-stake-registry/src/instructions/proxied_vote_v0.rs +++ b/programs/voter-stake-registry/src/instructions/proxied_vote_v0.rs @@ -1,10 +1,9 @@ -use crate::{error::VsrError, VoteArgsV0}; use anchor_lang::prelude::*; use nft_proxy::ProxyAssignmentV0; - -use crate::{registrar_seeds, state::*}; use proposal::{ProposalConfigV0, ProposalV0}; +use crate::{error::VsrError, registrar_seeds, state::*, VoteArgsV0}; + #[derive(Accounts)] pub struct ProxiedVoteV0<'info> { #[account(mut)] diff --git a/programs/voter-stake-registry/src/instructions/relinquish_vote_v1.rs b/programs/voter-stake-registry/src/instructions/relinquish_vote_v1.rs index 18c1c43d3..128cb8b8e 100644 --- a/programs/voter-stake-registry/src/instructions/relinquish_vote_v1.rs +++ b/programs/voter-stake-registry/src/instructions/relinquish_vote_v1.rs @@ -1,9 +1,8 @@ -use crate::error::VsrError; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; use proposal::{ProposalConfigV0, ProposalV0}; -use crate::{registrar_seeds, state::*}; +use crate::{error::VsrError, registrar_seeds, state::*}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct RelinquishVoteArgsV1 { diff --git a/programs/voter-stake-registry/src/instructions/vote_v0.rs b/programs/voter-stake-registry/src/instructions/vote_v0.rs index dee7ae527..d3b5e3066 100644 --- a/programs/voter-stake-registry/src/instructions/vote_v0.rs +++ b/programs/voter-stake-registry/src/instructions/vote_v0.rs @@ -1,10 +1,9 @@ -use crate::error::VsrError; use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; - -use crate::{registrar_seeds, state::*}; use proposal::{ProposalConfigV0, ProposalV0}; +use crate::{error::VsrError, registrar_seeds, state::*}; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct VoteArgsV0 { pub choice: u16, diff --git a/programs/voter-stake-registry/src/state/position.rs b/programs/voter-stake-registry/src/state/position.rs index 9ace63d54..7dea634c6 100644 --- a/programs/voter-stake-registry/src/state/position.rs +++ b/programs/voter-stake-registry/src/state/position.rs @@ -1,7 +1,9 @@ +use std::cmp::min; + +use anchor_lang::prelude::*; + use super::{Lockup, LockupKind, VotingMintConfigV0}; use crate::error::*; -use anchor_lang::prelude::*; -use std::cmp::min; pub const PRECISION_FACTOR: u128 = 1_000_000_000_000; diff --git a/programs/voter-stake-registry/src/state/registrar.rs b/programs/voter-stake-registry/src/state/registrar.rs index 91fcfbb4e..451449207 100644 --- a/programs/voter-stake-registry/src/state/registrar.rs +++ b/programs/voter-stake-registry/src/state/registrar.rs @@ -1,7 +1,7 @@ -use crate::error::*; -use crate::state::voting_mint_config::VotingMintConfigV0; use anchor_lang::prelude::*; +use crate::{error::*, state::voting_mint_config::VotingMintConfigV0}; + // Instance of a voting rights distributor. #[account] #[derive(Default)] @@ -24,6 +24,7 @@ pub struct Registrar { pub reserved2: [u64; 3], // split because `Default` does not support [u8; 60] pub proxy_config: Pubkey, pub voting_mints: Vec, + pub recent_proposals: [Pubkey; 4], } impl Registrar { From cbab5c86f2890b067ff39ef10284bf419e3cdf86 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Wed, 9 Oct 2024 15:30:26 -0700 Subject: [PATCH 02/17] Getting more wip... --- Cargo.lock | 1 + .../position-voting-rewards-sdk/package.json | 49 + .../src/constants.ts | 9 + .../position-voting-rewards-sdk/src/index.ts | 30 + .../position-voting-rewards-sdk/src/pdas.ts | 36 + .../src/resolvers.ts | 142 + .../tsconfig.cjs.json | 7 + .../tsconfig.esm.json | 8 + .../position-voting-rewards-sdk/tsconfig.json | 20 + .../yarn.deploy.lock | 2288 +++++++++++++++++ programs/position-voting-rewards/Cargo.toml | 3 +- .../src/instructions/enroll_v0.rs | 1 + .../initialize_vetoken_tracker_v0.rs | 42 + .../src/instructions/mod.rs | 6 + .../src/instructions/reward_for_epoch_v0.rs | 13 + .../src/instructions/track_vote_v0.rs | 53 + programs/position-voting-rewards/src/lib.rs | 33 +- programs/position-voting-rewards/src/state.rs | 89 +- tsconfig.json | 3 + yarn.lock | 18 + 20 files changed, 2794 insertions(+), 57 deletions(-) create mode 100644 packages/position-voting-rewards-sdk/package.json create mode 100644 packages/position-voting-rewards-sdk/src/constants.ts create mode 100644 packages/position-voting-rewards-sdk/src/index.ts create mode 100644 packages/position-voting-rewards-sdk/src/pdas.ts create mode 100644 packages/position-voting-rewards-sdk/src/resolvers.ts create mode 100644 packages/position-voting-rewards-sdk/tsconfig.cjs.json create mode 100644 packages/position-voting-rewards-sdk/tsconfig.esm.json create mode 100644 packages/position-voting-rewards-sdk/tsconfig.json create mode 100644 packages/position-voting-rewards-sdk/yarn.deploy.lock create mode 100644 programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs create mode 100644 programs/position-voting-rewards/src/instructions/track_vote_v0.rs diff --git a/Cargo.lock b/Cargo.lock index 52eefaaa2..56c36a50c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3020,6 +3020,7 @@ dependencies = [ "anchor-lang", "anchor-spl", "default-env", + "proposal", "shared-utils", "solana-security-txt", "voter-stake-registry", diff --git a/packages/position-voting-rewards-sdk/package.json b/packages/position-voting-rewards-sdk/package.json new file mode 100644 index 000000000..521d5f447 --- /dev/null +++ b/packages/position-voting-rewards-sdk/package.json @@ -0,0 +1,49 @@ +{ + "name": "@helium/position-voting-rewards-sdk", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "license": "Apache-2.0", + "version": "0.9.7", + "description": "Interface to the position-voting-rewards smart contract", + "repository": { + "type": "git", + "url": "https://github.com/helium/helium-program-libary" + }, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/src/index.js", + "types": "./lib/types/src/index.d.ts", + "sideEffects": false, + "files": [ + "lib" + ], + "exports": { + "import": "./lib/esm/src/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/types/src/index.d.ts" + }, + "scripts": { + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "precommit": "npx git-format-staged -f 'prettier --ignore-unknown --stdin --stdin-filepath \"{}\"' .", + "clean": "npx shx mkdir -p lib && npx shx rm -rf lib", + "package": "npx shx mkdir -p lib/cjs lib/esm", + "prebuild": "npm run clean && npm run package" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.28.0", + "@helium/anchor-resolvers": "^0.9.7", + "@helium/idls": "^0.9.7", + "@helium/spl-utils": "^0.9.7", + "@solana/spl-token": "^0.3.8", + "bn.js": "^5.2.0", + "bs58": "^4.0.1" + }, + "devDependencies": { + "git-format-staged": "^2.1.3", + "ts-loader": "^9.2.3", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "gitHead": "5a8bf0b7b88e5934ef8d774e686f7c95804fbb8d" +} diff --git a/packages/position-voting-rewards-sdk/src/constants.ts b/packages/position-voting-rewards-sdk/src/constants.ts new file mode 100644 index 000000000..180d02ddb --- /dev/null +++ b/packages/position-voting-rewards-sdk/src/constants.ts @@ -0,0 +1,9 @@ +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +export const PROGRAM_ID = new PublicKey("pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM"); +export const EPOCH_LENGTH = 60 * 60 * 24; + +export function currentEpoch(unixTime: BN): BN { + return new BN(Math.floor(unixTime.toNumber() / EPOCH_LENGTH)); +} \ No newline at end of file diff --git a/packages/position-voting-rewards-sdk/src/index.ts b/packages/position-voting-rewards-sdk/src/index.ts new file mode 100644 index 000000000..66b8f2d82 --- /dev/null +++ b/packages/position-voting-rewards-sdk/src/index.ts @@ -0,0 +1,30 @@ +import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor"; +import { PositionVotingRewards } from "@helium/idls/lib/types/position_voting_rewards"; +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID } from "./constants"; +import { positionVotingRewardsResolvers } from "./resolvers"; +export * from "./constants"; +export * from "./pdas"; +export * from "./resolvers"; + +export const init = async ( + provider: AnchorProvider, + programId: PublicKey = PROGRAM_ID, + idl?: Idl | null +): Promise> => { + if (!idl) { + idl = await Program.fetchIdl(programId, provider); + } + + const positionVotingRewards = new Program( + idl as PositionVotingRewards, + programId, + provider, + undefined, + () => { + return positionVotingRewardsResolvers; + } + ) as Program; + + return positionVotingRewards; +}; diff --git a/packages/position-voting-rewards-sdk/src/pdas.ts b/packages/position-voting-rewards-sdk/src/pdas.ts new file mode 100644 index 000000000..75f99b3c2 --- /dev/null +++ b/packages/position-voting-rewards-sdk/src/pdas.ts @@ -0,0 +1,36 @@ +import { PublicKey } from "@solana/web3.js"; +import { PROGRAM_ID, currentEpoch } from "./constants"; +import BN from "bn.js"; + +export const vsrTrackerKey = ( + registrar: PublicKey, + programId: PublicKey = PROGRAM_ID +) => + PublicKey.findProgramAddressSync( + [Buffer.from("vsr_tracker", "utf-8"), registrar.toBuffer()], + programId + ); + +export function vsrEpochInfoKey( + vsrTracker: PublicKey, + unixTime: number | BN, + programId: PublicKey = PROGRAM_ID +): [PublicKey, number] { + let bU64 = Buffer.alloc(8); + const epoch = currentEpoch(new BN(unixTime)).toNumber(); + bU64.writeBigUInt64LE(BigInt(epoch)); + return PublicKey.findProgramAddressSync( + [Buffer.from("vsr_epoch_info", "utf-8"), vsrTracker.toBuffer(), bU64], + programId + ); +} + +export function enrolledPositionKey( + position: PublicKey, + programId: PublicKey = PROGRAM_ID +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("enrolled_position", "utf-8"), position.toBuffer()], + programId + ); +} diff --git a/packages/position-voting-rewards-sdk/src/resolvers.ts b/packages/position-voting-rewards-sdk/src/resolvers.ts new file mode 100644 index 000000000..cbf56accb --- /dev/null +++ b/packages/position-voting-rewards-sdk/src/resolvers.ts @@ -0,0 +1,142 @@ +import { + ataResolver, + combineResolvers, + get, + heliumCommonResolver, + resolveIndividual, +} from "@helium/anchor-resolvers"; +import { + init, + PROGRAM_ID as VSR_PROGRAM_ID, + vsrResolvers, +} from "@helium/voter-stake-registry-sdk"; +import { AnchorProvider, Provider } from "@coral-xyz/anchor"; +import { PublicKey, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js"; +import { EPOCH_LENGTH, PROGRAM_ID } from "./constants"; +import { vsrEpochInfoKey } from "./pdas"; + +export const vsrEpochInfoResolver = resolveIndividual( + async ({ provider, path, accounts }) => { + if (path[path.length - 1] === "vsrEpochInfo" && accounts.registrar) { + const vsr = await init(provider as AnchorProvider, VSR_PROGRAM_ID); + let registrar; + try { + registrar = await vsr.account.registrar.fetch( + accounts.registrar as PublicKey + ); + } catch (e: any) { + // ignore. It's fine, we just won't use time offset which is only used in testing cases + console.error(e); + } + const clock = await provider.connection.getAccountInfo( + SYSVAR_CLOCK_PUBKEY + ); + const unixTime = + Number(clock!.data.readBigInt64LE(8 * 4)) + + (registrar?.timeOffset.toNumber() || 0); + const vsrTracker = get(accounts, [ + ...path.slice(0, path.length - 1), + "vsr_tracker", + ]) as PublicKey; + if (vsrTracker) { + const [key] = await vsrEpochInfoKey(vsrTracker, unixTime, PROGRAM_ID); + + return key; + } + } + } +); + +export const closingTimeEpochInfoResolver = resolveIndividual( + async ({ provider, path, accounts }) => { + if (path[path.length - 1] === "closingTimeVsrEpochInfo") { + const program = await init(provider as AnchorProvider, VSR_PROGRAM_ID); + + const vsrTracker = get(accounts, [ + ...path.slice(0, path.length - 1), + "vsr_tracker", + ]) as PublicKey; + const position = get(accounts, [ + ...path.slice(0, path.length - 1), + "position", + ]) as PublicKey; + const positionAcc = + position && (await program.account.positionV0.fetch(position)); + if (positionAcc) { + const [key] = await vsrEpochInfoKey( + vsrTracker, + positionAcc.lockup.endTs + ); + + return key; + } + } + } +); + +async function getSolanaUnixTimestamp(provider: Provider): Promise { + const clock = await provider.connection.getAccountInfo(SYSVAR_CLOCK_PUBKEY); + const unixTime = clock!.data.readBigInt64LE(8 * 4); + return unixTime; +} + +export const genesisEndEpochInfoResolver = resolveIndividual( + async ({ provider, path, accounts }) => { + if (path[path.length - 1] === "genesisEndVsrEpochInfo") { + const program = await init(provider as AnchorProvider, VSR_PROGRAM_ID); + + const vsrTracker = get(accounts, [ + ...path.slice(0, path.length - 1), + "vsr_tracker", + ]) as PublicKey; + const position = get(accounts, [ + ...path.slice(0, path.length - 1), + "position", + ]) as PublicKey; + const registrar = get(accounts, [ + ...path.slice(0, path.length - 1), + "registrar", + ]) as PublicKey; + const positionAcc = + position && (await program.account.positionV0.fetch(position)); + const registrarAcc = + registrar && (await program.account.registrar.fetch(registrar)); + if (positionAcc && registrarAcc) { + const currTs = + Number(await getSolanaUnixTimestamp(provider)) + + registrarAcc.timeOffset.toNumber(); + const ts = + positionAcc.genesisEnd.toNumber() < currTs + ? positionAcc.lockup.endTs.toNumber() + : positionAcc.genesisEnd; + const [key] = await vsrEpochInfoKey(vsrTracker, ts); + + return key; + } + } + } +); + +export const positionVotingRewardsResolvers = combineResolvers( + heliumCommonResolver, + vsrEpochInfoResolver, + genesisEndEpochInfoResolver, + closingTimeEpochInfoResolver, + ataResolver({ + instruction: "claimRewardsV0", + account: "enrolledAta", + mint: "rewardsMint", + owner: "positionAuthority", + }), + ataResolver({ + account: "positionTokenAccount", + mint: "mint", + owner: "positionAuthority", + }), + ataResolver({ + account: "rewardsPool", + mint: "rewardsMint", + owner: "vsrEpochInfo", + }), + vsrResolvers +); diff --git a/packages/position-voting-rewards-sdk/tsconfig.cjs.json b/packages/position-voting-rewards-sdk/tsconfig.cjs.json new file mode 100644 index 000000000..5445b9909 --- /dev/null +++ b/packages/position-voting-rewards-sdk/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs" + } +} diff --git a/packages/position-voting-rewards-sdk/tsconfig.esm.json b/packages/position-voting-rewards-sdk/tsconfig.esm.json new file mode 100644 index 000000000..7232d1e38 --- /dev/null +++ b/packages/position-voting-rewards-sdk/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types" + } +} \ No newline at end of file diff --git a/packages/position-voting-rewards-sdk/tsconfig.json b/packages/position-voting-rewards-sdk/tsconfig.json new file mode 100644 index 000000000..baa4e028a --- /dev/null +++ b/packages/position-voting-rewards-sdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.root.json", + "references": [ + { + "path": "../idls" + }, + { + "path": "../spl-utils" + }, + { + "path": "../anchor-resolvers" + }, + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/position-voting-rewards-sdk/yarn.deploy.lock b/packages/position-voting-rewards-sdk/yarn.deploy.lock new file mode 100644 index 000000000..c65b0a5d6 --- /dev/null +++ b/packages/position-voting-rewards-sdk/yarn.deploy.lock @@ -0,0 +1,2288 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"@babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.22.6": + version: 7.22.11 + resolution: "@babel/runtime@npm:7.22.11" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: a5cd6683a8fcdb8065cb1677f221e22f6c67ec8f15ad1d273b180b93ab3bd86c66da2c48f500d4e72d8d2cfa85ff4872a3f350e5aa3855630036af5da765c001 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.23.4": + version: 7.24.1 + resolution: "@babel/runtime@npm:7.24.1" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 5c8f3b912ba949865f03b3cf8395c60e1f4ebd1033fbd835bdfe81b6cac8a87d85bc3c7aded5fcdf07be044c9ab8c818f467abe0deca50020c72496782639572 + languageName: node + linkType: hard + +"@coral-xyz/anchor@npm:^0.28.0": + version: 0.28.0 + resolution: "@coral-xyz/anchor@npm:0.28.0" + dependencies: + "@coral-xyz/borsh": "npm:^0.28.0" + "@solana/web3.js": "npm:^1.68.0" + base64-js: "npm:^1.5.1" + bn.js: "npm:^5.1.2" + bs58: "npm:^4.0.1" + buffer-layout: "npm:^1.2.2" + camelcase: "npm:^6.3.0" + cross-fetch: "npm:^3.1.5" + crypto-hash: "npm:^1.3.0" + eventemitter3: "npm:^4.0.7" + js-sha256: "npm:^0.9.0" + pako: "npm:^2.0.3" + snake-case: "npm:^3.0.4" + superstruct: "npm:^0.15.4" + toml: "npm:^3.0.0" + checksum: 58b3677b5b2ce2c779045184ce4a0ef696966a6a58f41c1c56f6f178db0491acecb6ec677ce0502f0b382a2c724f9c2860d82cc88601784d556d95fbeda415e5 + languageName: node + linkType: hard + +"@coral-xyz/borsh@npm:^0.28.0": + version: 0.28.0 + resolution: "@coral-xyz/borsh@npm:0.28.0" + dependencies: + bn.js: "npm:^5.1.2" + buffer-layout: "npm:^1.2.0" + peerDependencies: + "@solana/web3.js": ^1.68.0 + checksum: bc2b06b777f9ed43d3b886d2350826bd44d5b932c4fd4471af5956e8755236f5854938c890ee4986d88f61d1439e1d84e715c43dcb6dca4e76198c1ce8dc7a58 + languageName: node + linkType: hard + +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 5718f267085ed8edb3e7ef210137241775e607ee18b77d95aa5bd7514f47f5019aa2d82d96b3bf342ef7aa890a346fa1044532ff7cc3009e7d24fce3ce6200fa + languageName: node + linkType: hard + +"@helium/account-fetch-cache@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/account-fetch-cache@workspace:packages/account-fetch-cache" + dependencies: + "@solana/web3.js": ^1.78.8 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@helium/address@npm:^4.10.2": + version: 4.10.2 + resolution: "@helium/address@npm:4.10.2" + dependencies: + bs58: "npm:^5.0.0" + js-sha256: "npm:^0.9.0" + multiformats: "npm:^9.6.4" + checksum: 4d83df57c638ed1496a92241a2f45da671e11b6397f02ab65c5281cbd1a435548e7ffd5be969d8d214b5af045e9be4bdd2d89f41744289c17e6410d458b03bcc + languageName: node + linkType: hard + +"@helium/anchor-resolvers@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/anchor-resolvers@workspace:packages/anchor-resolvers" + dependencies: + "@solana/spl-token": ^0.3.8 + "@solana/web3.js": ^1.78.8 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + peerDependencies: + "@coral-xyz/anchor": ^0.28.0 + languageName: unknown + linkType: soft + +"@helium/idls@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/idls@workspace:packages/idls" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@solana/web3.js": ^1.78.8 + bn.js: ^5.2.0 + borsh: ^0.7.0 + bs58: ^4.0.1 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@helium/position-voting-rewards-sdk@workspace:.": + version: 0.0.0-use.local + resolution: "@helium/position-voting-rewards-sdk@workspace:." + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.9.7 + "@helium/idls": ^0.9.7 + "@helium/spl-utils": ^0.9.7 + "@solana/spl-token": ^0.3.8 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@helium/spl-utils@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/spl-utils@workspace:packages/spl-utils" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/account-fetch-cache": ^0.9.7 + "@helium/address": ^4.10.2 + "@helium/anchor-resolvers": ^0.9.7 + "@metaplex-foundation/mpl-token-metadata": ^2.10.0 + "@solana/spl-account-compression": ^0.1.7 + "@solana/spl-token": ^0.3.8 + "@solana/web3.js": ^1.78.8 + axios: ^1.5.0 + bn.js: ^5.2.0 + borsh: ^0.7.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 4a473b9b32a7d4d3cfb7a614226e555091ff0c5a29a1734c28c72a182c2f6699b26fc6b5c2131dfd841e86b185aea714c72201d7c98c2fba5f17709333a67aeb + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.0.3": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: f5b441fe7900eab4f9155b3b93f9800a916257f4e8563afbcd3b5a5337b55e52bd8ae6735453b1b745457d9f6cdb16d74cd6220bbdd98cf153239e13f6cbb653 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 + languageName: node + linkType: hard + +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: d89597752fd88d3f3480845691a05a44bd21faac18e2185b6f436c3b0fd0c5a859fbbd9aaa92050c4052caf325ad3e10e2e1d1b64327517471b7d51babc0ddef + languageName: node + linkType: hard + +"@metaplex-foundation/beet-solana@npm:^0.4.0": + version: 0.4.0 + resolution: "@metaplex-foundation/beet-solana@npm:0.4.0" + dependencies: + "@metaplex-foundation/beet": "npm:>=0.1.0" + "@solana/web3.js": "npm:^1.56.2" + bs58: "npm:^5.0.0" + debug: "npm:^4.3.4" + checksum: ee746c2d15f985c31d133d4ee29efbda445877473cc32aafa4b684ce3fa9a916ddff30d0e3cfef7654ff5725adff59a62a635c76bc781a6e1362c5b5d3137ed0 + languageName: node + linkType: hard + +"@metaplex-foundation/beet@npm:>=0.1.0, @metaplex-foundation/beet@npm:^0.7.1": + version: 0.7.1 + resolution: "@metaplex-foundation/beet@npm:0.7.1" + dependencies: + ansicolors: "npm:^0.3.2" + bn.js: "npm:^5.2.0" + debug: "npm:^4.3.3" + checksum: f8a330073ab1a0976478e9847c0e63e32f7bee67ea6306e1f89784e8275e30daaecba7cbc5f3424e5d96c411aa3bfbc2b638c105a90067a985acdfbd33a1a287 + languageName: node + linkType: hard + +"@metaplex-foundation/cusper@npm:^0.0.2": + version: 0.0.2 + resolution: "@metaplex-foundation/cusper@npm:0.0.2" + checksum: d157953baf42a2a012cdeb809c1785f29a44d80a3b5a3841c930baeb12ac6ddcf37f1a15eded4dce20d66f7bc8f23bedb87e905758df721e274bfcd816e70ba1 + languageName: node + linkType: hard + +"@metaplex-foundation/mpl-token-metadata@npm:^2.10.0": + version: 2.13.0 + resolution: "@metaplex-foundation/mpl-token-metadata@npm:2.13.0" + dependencies: + "@metaplex-foundation/beet": "npm:^0.7.1" + "@metaplex-foundation/beet-solana": "npm:^0.4.0" + "@metaplex-foundation/cusper": "npm:^0.0.2" + "@solana/spl-token": "npm:^0.3.6" + "@solana/web3.js": "npm:^1.66.2" + bn.js: "npm:^5.2.0" + debug: "npm:^4.3.4" + checksum: 89f82980f435ac45d961e6ab03859c7e4ab9809ad2d9f84aca91bcf50fdc60e210d0596d8856adf63e474ca69d971c51a4c27d0fe4dab115fcef64d57f3be611 + languageName: node + linkType: hard + +"@noble/curves@npm:^1.0.0": + version: 1.2.0 + resolution: "@noble/curves@npm:1.2.0" + dependencies: + "@noble/hashes": "npm:1.3.2" + checksum: bb798d7a66d8e43789e93bc3c2ddff91a1e19fdb79a99b86cd98f1e5eff0ee2024a2672902c2576ef3577b6f282f3b5c778bebd55761ddbb30e36bf275e83dd0 + languageName: node + linkType: hard + +"@noble/curves@npm:^1.2.0": + version: 1.4.0 + resolution: "@noble/curves@npm:1.4.0" + dependencies: + "@noble/hashes": 1.4.0 + checksum: 0014ff561d16e98da4a57e2310a4015e4bdab3b1e1eafcd18d3f9b955c29c3501452ca5d702fddf8ca92d570bbeadfbe53fe16ebbd81a319c414f739154bb26b + languageName: node + linkType: hard + +"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.1": + version: 1.3.2 + resolution: "@noble/hashes@npm:1.3.2" + checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 + languageName: node + linkType: hard + +"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.3": + version: 1.4.0 + resolution: "@noble/hashes@npm:1.4.0" + checksum: 8ba816ae26c90764b8c42493eea383716396096c5f7ba6bea559993194f49d80a73c081f315f4c367e51bd2d5891700bcdfa816b421d24ab45b41cb03e4f3342 + languageName: node + linkType: hard + +"@npmcli/fs@npm:^3.1.0": + version: 3.1.0 + resolution: "@npmcli/fs@npm:3.1.0" + dependencies: + semver: "npm:^7.3.5" + checksum: a50a6818de5fc557d0b0e6f50ec780a7a02ab8ad07e5ac8b16bf519e0ad60a144ac64f97d05c443c3367235d337182e1d012bbac0eb8dbae8dc7b40b193efd0e + languageName: node + linkType: hard + +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 6ad6a00fc4f2f2cfc6bff76fb1d88b8ee20bc0601e18ebb01b6d4be583733a860239a521a7fbca73b612e66705078809483549d2b18f370eb346c5155c8e4a0f + languageName: node + linkType: hard + +"@solana/buffer-layout-utils@npm:^0.2.0": + version: 0.2.0 + resolution: "@solana/buffer-layout-utils@npm:0.2.0" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/web3.js": "npm:^1.32.0" + bigint-buffer: "npm:^1.1.5" + bignumber.js: "npm:^9.0.1" + checksum: 9284242245b18b49577195ba7548263850be865a4a2d183944fa01bb76382039db589aab8473698e9bb734b515ada9b4d70db0a72e341c5d567c59b83d6d0840 + languageName: node + linkType: hard + +"@solana/buffer-layout@npm:^4.0.0, @solana/buffer-layout@npm:^4.0.1": + version: 4.0.1 + resolution: "@solana/buffer-layout@npm:4.0.1" + dependencies: + buffer: "npm:~6.0.3" + checksum: bf846888e813187243d4008a7a9f58b49d16cbd995b9d7f1b72898aa510ed77b1ce5e8468e7b2fd26dd81e557a4e74a666e21fccb95f123c1f740d41138bbacd + languageName: node + linkType: hard + +"@solana/spl-account-compression@npm:^0.1.7": + version: 0.1.10 + resolution: "@solana/spl-account-compression@npm:0.1.10" + dependencies: + "@metaplex-foundation/beet": "npm:^0.7.1" + "@metaplex-foundation/beet-solana": "npm:^0.4.0" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + js-sha3: "npm:^0.8.0" + typescript-collections: "npm:^1.3.3" + peerDependencies: + "@solana/web3.js": ^1.50.1 + checksum: 99bd851933c46a068dbd13484770edd7ae12488b1474ee2e9d7dfd114087f3f9f813745795f91278142fbeb09aec1024f74ef2d2c8be2b47f1df319d37f0af11 + languageName: node + linkType: hard + +"@solana/spl-token@npm:^0.3.6, @solana/spl-token@npm:^0.3.8": + version: 0.3.8 + resolution: "@solana/spl-token@npm:0.3.8" + dependencies: + "@solana/buffer-layout": "npm:^4.0.0" + "@solana/buffer-layout-utils": "npm:^0.2.0" + buffer: "npm:^6.0.3" + peerDependencies: + "@solana/web3.js": ^1.47.4 + checksum: 01f4f87112b0ad277701a3bcb8e03069b69449b92724b17959107686731082bfd3475b5f105e1e8f04badd2e810a43d5ef811744ced5178eea1232de8fd75147 + languageName: node + linkType: hard + +"@solana/web3.js@npm:^1.32.0, @solana/web3.js@npm:^1.56.2, @solana/web3.js@npm:^1.66.2, @solana/web3.js@npm:^1.68.0": + version: 1.78.4 + resolution: "@solana/web3.js@npm:1.78.4" + dependencies: + "@babel/runtime": "npm:^7.22.6" + "@noble/curves": "npm:^1.0.0" + "@noble/hashes": "npm:^1.3.1" + "@solana/buffer-layout": "npm:^4.0.0" + agentkeepalive: "npm:^4.3.0" + bigint-buffer: "npm:^1.1.5" + bn.js: "npm:^5.2.1" + borsh: "npm:^0.7.0" + bs58: "npm:^4.0.1" + buffer: "npm:6.0.3" + fast-stable-stringify: "npm:^1.0.0" + jayson: "npm:^4.1.0" + node-fetch: "npm:^2.6.12" + rpc-websockets: "npm:^7.5.1" + superstruct: "npm:^0.14.2" + checksum: e1c44c6cbec87cdfd4d6d23b4241b746e14ed3a9ca73d596693758d91ac825cecf579345da3b0b7bb5e54b6794791bc0eac02cadf11f1ec79e859b6536f26f11 + languageName: node + linkType: hard + +"@solana/web3.js@npm:^1.78.8": + version: 1.91.3 + resolution: "@solana/web3.js@npm:1.91.3" + dependencies: + "@babel/runtime": ^7.23.4 + "@noble/curves": ^1.2.0 + "@noble/hashes": ^1.3.3 + "@solana/buffer-layout": ^4.0.1 + agentkeepalive: ^4.5.0 + bigint-buffer: ^1.1.5 + bn.js: ^5.2.1 + borsh: ^0.7.0 + bs58: ^4.0.1 + buffer: 6.0.3 + fast-stable-stringify: ^1.0.0 + jayson: ^4.1.0 + node-fetch: ^2.7.0 + rpc-websockets: ^7.5.1 + superstruct: ^0.14.2 + checksum: 5b251915057368d0615d6a6f0de7b4d4129c3d0643599551c0a342125601418634851b633a47d19da481973406f0f2928fa4bf1a2c57fb19914e69fd5b81c302 + languageName: node + linkType: hard + +"@tootallnate/once@npm:2": + version: 2.0.0 + resolution: "@tootallnate/once@npm:2.0.0" + checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 + languageName: node + linkType: hard + +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.9 + resolution: "@tsconfig/node10@npm:1.0.9" + checksum: a33ae4dc2a621c0678ac8ac4bceb8e512ae75dac65417a2ad9b022d9b5411e863c4c198b6ba9ef659e14b9fb609bbec680841a2e84c1172df7a5ffcf076539df + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + +"@types/connect@npm:^3.4.33": + version: 3.4.35 + resolution: "@types/connect@npm:3.4.35" + dependencies: + "@types/node": "npm:*" + checksum: fe81351470f2d3165e8b12ce33542eef89ea893e36dd62e8f7d72566dfb7e448376ae962f9f3ea888547ce8b55a40020ca0e01d637fab5d99567673084542641 + languageName: node + linkType: hard + +"@types/node@npm:*": + version: 20.5.7 + resolution: "@types/node@npm:20.5.7" + checksum: fc284c8e16ddc04569730d58e87eae349eb1c3dd9020cb79a1862d9d9add6f04e7367a236f3252db8db2572f90278e250f4cd43d27d264972b54394eaba1ed76 + languageName: node + linkType: hard + +"@types/node@npm:^12.12.54": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: e4f86785f4092706e0d3b0edff8dca5a13b45627e4b36700acd8dfe6ad53db71928c8dee914d4276c7fd3b6ccd829aa919811c9eb708a2c8e4c6eb3701178c37 + languageName: node + linkType: hard + +"@types/ws@npm:^7.4.4": + version: 7.4.7 + resolution: "@types/ws@npm:7.4.7" + dependencies: + "@types/node": "npm:*" + checksum: b4c9b8ad209620c9b21e78314ce4ff07515c0cadab9af101c1651e7bfb992d7fd933bd8b9c99d110738fd6db523ed15f82f29f50b45510288da72e964dedb1a3 + languageName: node + linkType: hard + +"JSONStream@npm:^1.3.5": + version: 1.3.5 + resolution: "JSONStream@npm:1.3.5" + dependencies: + jsonparse: "npm:^1.2.0" + through: "npm:>=2.2.7 <3" + bin: + JSONStream: ./bin.js + checksum: 2605fa124260c61bad38bb65eba30d2f72216a78e94d0ab19b11b4e0327d572b8d530c0c9cc3b0764f727ad26d39e00bf7ebad57781ca6368394d73169c59e46 + languageName: node + linkType: hard + +"abbrev@npm:^1.0.0": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 + languageName: node + linkType: hard + +"acorn-walk@npm:^8.1.1": + version: 8.2.0 + resolution: "acorn-walk@npm:8.2.0" + checksum: 1715e76c01dd7b2d4ca472f9c58968516a4899378a63ad5b6c2d668bba8da21a71976c14ec5f5b75f887b6317c4ae0b897ab141c831d741dc76024d8745f1ad1 + languageName: node + linkType: hard + +"acorn@npm:^8.4.1": + version: 8.10.0 + resolution: "acorn@npm:8.10.0" + bin: + acorn: bin/acorn + checksum: 538ba38af0cc9e5ef983aee196c4b8b4d87c0c94532334fa7e065b2c8a1f85863467bb774231aae91613fcda5e68740c15d97b1967ae3394d20faddddd8af61d + languageName: node + linkType: hard + +"agent-base@npm:6, agent-base@npm:^6.0.2": + version: 6.0.2 + resolution: "agent-base@npm:6.0.2" + dependencies: + debug: "npm:4" + checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d + languageName: node + linkType: hard + +"agentkeepalive@npm:^4.2.1, agentkeepalive@npm:^4.3.0, agentkeepalive@npm:^4.5.0": + version: 4.5.0 + resolution: "agentkeepalive@npm:4.5.0" + dependencies: + humanize-ms: "npm:^1.2.1" + checksum: 13278cd5b125e51eddd5079f04d6fe0914ac1b8b91c1f3db2c1822f99ac1a7457869068997784342fe455d59daaff22e14fb7b8c3da4e741896e7e31faf92481 + languageName: node + linkType: hard + +"aggregate-error@npm:^3.0.0": + version: 3.1.0 + resolution: "aggregate-error@npm:3.1.0" + dependencies: + clean-stack: "npm:^2.0.0" + indent-string: "npm:^4.0.0" + checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 + languageName: node + linkType: hard + +"ansi-regex@npm:^5.0.1": + version: 5.0.1 + resolution: "ansi-regex@npm:5.0.1" + checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b + languageName: node + linkType: hard + +"ansi-regex@npm:^6.0.1": + version: 6.0.1 + resolution: "ansi-regex@npm:6.0.1" + checksum: 1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": + version: 4.3.0 + resolution: "ansi-styles@npm:4.3.0" + dependencies: + color-convert: "npm:^2.0.1" + checksum: 513b44c3b2105dd14cc42a19271e80f386466c4be574bccf60b627432f9198571ebf4ab1e4c3ba17347658f4ee1711c163d574248c0c1cdc2d5917a0ad582ec4 + languageName: node + linkType: hard + +"ansi-styles@npm:^6.1.0": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + +"ansicolors@npm:^0.3.2": + version: 0.3.2 + resolution: "ansicolors@npm:0.3.2" + checksum: e84fae7ebc27ac96d9dbb57f35f078cd6dde1b7046b0f03f73dcefc9fbb1f2e82e3685d083466aded8faf038f9fa9ebb408d215282bcd7aaa301d5ac3c486815 + languageName: node + linkType: hard + +"aproba@npm:^1.0.3 || ^2.0.0": + version: 2.0.0 + resolution: "aproba@npm:2.0.0" + checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 + languageName: node + linkType: hard + +"are-we-there-yet@npm:^3.0.0": + version: 3.0.1 + resolution: "are-we-there-yet@npm:3.0.1" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: 52590c24860fa7173bedeb69a4c05fb573473e860197f618b9a28432ee4379049336727ae3a1f9c4cb083114601c1140cee578376164d0e651217a9843f9fe83 + languageName: node + linkType: hard + +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 544af8dd3f60546d3e4aff084d451b96961d2267d668670199692f8d054f0415d86fc5497d0e641e91546f0aa920e7c29e5250e99fc89f5552a34b5d93b77f43 + languageName: node + linkType: hard + +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + +"axios@npm:^1.5.0": + version: 1.5.0 + resolution: "axios@npm:1.5.0" + dependencies: + follow-redirects: "npm:^1.15.0" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: e7405a5dbbea97760d0e6cd58fecba311b0401ddb4a8efbc4108f5537da9b3f278bde566deb777935a960beec4fa18e7b8353881f2f465e4f2c0e949fead35be + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"base-x@npm:^3.0.2": + version: 3.0.9 + resolution: "base-x@npm:3.0.9" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 957101d6fd09e1903e846fd8f69fd7e5e3e50254383e61ab667c725866bec54e5ece5ba49ce385128ae48f9ec93a26567d1d5ebb91f4d56ef4a9cc0d5a5481e8 + languageName: node + linkType: hard + +"base-x@npm:^4.0.0": + version: 4.0.0 + resolution: "base-x@npm:4.0.0" + checksum: b25db9e07eb1998472a20557c7f00c797dc0595f79df95155ab74274e7fa98b9f2659b3ee547ac8773666b7f69540656793aeb97ad2b1ceccdb6fa5faaf69ac0 + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 + languageName: node + linkType: hard + +"bigint-buffer@npm:^1.1.5": + version: 1.1.5 + resolution: "bigint-buffer@npm:1.1.5" + dependencies: + bindings: "npm:^1.3.0" + node-gyp: "npm:latest" + checksum: d010c9f57758bcdaccb435d88b483ffcc95fe8bbc6e7fb3a44fb5221f29c894ffaf4a3c5a4a530e0e7d6608203c2cde9b79ee4f2386cd6d4462d1070bc8c9f4e + languageName: node + linkType: hard + +"bignumber.js@npm:^9.0.1": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + +"bindings@npm:^1.3.0": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7 + languageName: node + linkType: hard + +"bn.js@npm:^5.1.2, bn.js@npm:^5.2.0, bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + +"borsh@npm:^0.7.0": + version: 0.7.0 + resolution: "borsh@npm:0.7.0" + dependencies: + bn.js: "npm:^5.2.0" + bs58: "npm:^4.0.0" + text-encoding-utf-8: "npm:^1.0.2" + checksum: e98bfb5f7cfb820819c2870b884dac58dd4b4ce6a86c286c8fbf5c9ca582e73a8c6094df67e81a28c418ff07a309c6b118b2e27fdfea83fd92b8100c741da0b5 + languageName: node + linkType: hard + +"brace-expansion@npm:^1.1.7": + version: 1.1.11 + resolution: "brace-expansion@npm:1.1.11" + dependencies: + balanced-match: "npm:^1.0.0" + concat-map: "npm:0.0.1" + checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 + languageName: node + linkType: hard + +"brace-expansion@npm:^2.0.1": + version: 2.0.1 + resolution: "brace-expansion@npm:2.0.1" + dependencies: + balanced-match: "npm:^1.0.0" + checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 + languageName: node + linkType: hard + +"braces@npm:^3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: e2a8e769a863f3d4ee887b5fe21f63193a891c68b612ddb4b68d82d1b5f3ff9073af066c343e9867a393fe4c2555dcb33e89b937195feb9c1613d259edfcd459 + languageName: node + linkType: hard + +"bs58@npm:^4.0.0, bs58@npm:^4.0.1": + version: 4.0.1 + resolution: "bs58@npm:4.0.1" + dependencies: + base-x: "npm:^3.0.2" + checksum: b3c5365bb9e0c561e1a82f1a2d809a1a692059fae016be233a6127ad2f50a6b986467c3a50669ce4c18929dcccb297c5909314dd347a25a68c21b68eb3e95ac2 + languageName: node + linkType: hard + +"bs58@npm:^5.0.0": + version: 5.0.0 + resolution: "bs58@npm:5.0.0" + dependencies: + base-x: "npm:^4.0.0" + checksum: 2475cb0684e07077521aac718e604a13e0f891d58cff923d437a2f7e9e28703ab39fce9f84c7c703ab369815a675f11e3bd394d38643bfe8969fbe42e6833d45 + languageName: node + linkType: hard + +"buffer-layout@npm:^1.2.0, buffer-layout@npm:^1.2.2": + version: 1.2.2 + resolution: "buffer-layout@npm:1.2.2" + checksum: e5809ba275530bf4e52fd09558b7c2111fbda5b405124f581acf364261d9c154e271800271898cd40473f9bcbb42c31584efb04219bde549d3460ca4bafeaa07 + languageName: node + linkType: hard + +"buffer@npm:6.0.3, buffer@npm:^6.0.3, buffer@npm:~6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 + languageName: node + linkType: hard + +"bufferutil@npm:^4.0.1": + version: 4.0.7 + resolution: "bufferutil@npm:4.0.7" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: f75aa87e3d1b99b87a95f60a855e63f70af07b57fb8443e75a2ddfef2e47788d130fdd46e3a78fd7e0c10176082b26dfbed970c5b8632e1cc299cafa0e93ce45 + languageName: node + linkType: hard + +"cacache@npm:^17.0.0": + version: 17.1.4 + resolution: "cacache@npm:17.1.4" + dependencies: + "@npmcli/fs": "npm:^3.1.0" + fs-minipass: "npm:^3.0.0" + glob: "npm:^10.2.2" + lru-cache: "npm:^7.7.1" + minipass: "npm:^7.0.3" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: b7751df756656954a51201335addced8f63fc53266fa56392c9f5ae83c8d27debffb4458ac2d168a744a4517ec3f2163af05c20097f93d17bdc2dc8a385e14a6 + languageName: node + linkType: hard + +"camelcase@npm:^6.3.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + +"chalk@npm:^4.1.0": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" + dependencies: + ansi-styles: "npm:^4.1.0" + supports-color: "npm:^7.1.0" + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + languageName: node + linkType: hard + +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"clean-stack@npm:^2.0.0": + version: 2.2.0 + resolution: "clean-stack@npm:2.2.0" + checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: "npm:~1.1.4" + checksum: 79e6bdb9fd479a205c71d89574fccfb22bd9053bd98c6c4d870d65c132e5e904e6034978e55b43d69fcaa7433af2016ee203ce76eeba9cfa554b373e7f7db336 + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 + languageName: node + linkType: hard + +"color-support@npm:^1.1.3": + version: 1.1.3 + resolution: "color-support@npm:1.1.3" + bin: + color-support: bin.js + checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b + languageName: node + linkType: hard + +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: "npm:~1.0.0" + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + +"commander@npm:^2.20.3": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e + languageName: node + linkType: hard + +"concat-map@npm:0.0.1": + version: 0.0.1 + resolution: "concat-map@npm:0.0.1" + checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af + languageName: node + linkType: hard + +"console-control-strings@npm:^1.1.0": + version: 1.1.0 + resolution: "console-control-strings@npm:1.1.0" + checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed + languageName: node + linkType: hard + +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + +"cross-fetch@npm:^3.1.5": + version: 3.1.8 + resolution: "cross-fetch@npm:3.1.8" + dependencies: + node-fetch: "npm:^2.6.12" + checksum: 78f993fa099eaaa041122ab037fe9503ecbbcb9daef234d1d2e0b9230a983f64d645d088c464e21a247b825a08dc444a6e7064adfa93536d3a9454b4745b3632 + languageName: node + linkType: hard + +"cross-spawn@npm:^7.0.0": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: "npm:^3.1.0" + shebang-command: "npm:^2.0.0" + which: "npm:^2.0.1" + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + +"crypto-hash@npm:^1.3.0": + version: 1.3.0 + resolution: "crypto-hash@npm:1.3.0" + checksum: a3a507e0d2b18fbd2da8088a1c62d0c53c009a99bbfa6d851cac069734ffa546922fa51bdd776d006459701cdda873463e5059ece3431aca048fd99e7573d138 + languageName: node + linkType: hard + +"debug@npm:4, debug@npm:^4.3.3, debug@npm:^4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 + languageName: node + linkType: hard + +"delay@npm:^5.0.0": + version: 5.0.0 + resolution: "delay@npm:5.0.0" + checksum: 62f151151ecfde0d9afbb8a6be37a6d103c4cb24f35a20ef3fe56f920b0d0d0bb02bc9c0a3084d0179ef669ca332b91155f2ee4d9854622cd2cdba5fc95285f9 + languageName: node + linkType: hard + +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + +"delegates@npm:^1.0.0": + version: 1.0.0 + resolution: "delegates@npm:1.0.0" + checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd + languageName: node + linkType: hard + +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: f2c09b0ce4e6b301c221addd83bf3f454c0bc00caa3dd837cf6c127d6edf7223aa2bbe3b688feea110b7f262adbfc845b757c44c8a9f8c0c5b15d8fa9ce9d20d + languageName: node + linkType: hard + +"dot-case@npm:^3.0.4": + version: 3.0.4 + resolution: "dot-case@npm:3.0.4" + dependencies: + no-case: "npm:^3.0.4" + tslib: "npm:^2.0.3" + checksum: a65e3519414856df0228b9f645332f974f2bf5433370f544a681122eab59e66038fc3349b4be1cdc47152779dac71a5864f1ccda2f745e767c46e9c6543b1169 + languageName: node + linkType: hard + +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 7d00d7cd8e49b9afa762a813faac332dee781932d6f2c848dc348939c4253f1d4564341b7af1d041853bc3f32c2ef141b58e0a4d9862c17a7f08f68df1e0f1ed + languageName: node + linkType: hard + +"emoji-regex@npm:^8.0.0": + version: 8.0.0 + resolution: "emoji-regex@npm:8.0.0" + checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 + languageName: node + linkType: hard + +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 8487182da74aabd810ac6d6f1994111dfc0e331b01271ae01ec1eb0ad7b5ecc2bbbbd2f053c05cb55a1ac30449527d819bbfbf0e3de1023db308cbcb47f86601 + languageName: node + linkType: hard + +"encoding@npm:^0.1.13": + version: 0.1.13 + resolution: "encoding@npm:0.1.13" + dependencies: + iconv-lite: "npm:^0.6.2" + checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f + languageName: node + linkType: hard + +"enhanced-resolve@npm:^5.0.0": + version: 5.15.0 + resolution: "enhanced-resolve@npm:5.15.0" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: fbd8cdc9263be71cc737aa8a7d6c57b43d6aa38f6cc75dde6fcd3598a130cc465f979d2f4d01bb3bf475acb43817749c79f8eef9be048683602ca91ab52e4f11 + languageName: node + linkType: hard + +"env-paths@npm:^2.2.0": + version: 2.2.1 + resolution: "env-paths@npm:2.2.1" + checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e + languageName: node + linkType: hard + +"err-code@npm:^2.0.2": + version: 2.0.3 + resolution: "err-code@npm:2.0.3" + checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 + languageName: node + linkType: hard + +"es6-promise@npm:^4.0.3": + version: 4.2.8 + resolution: "es6-promise@npm:4.2.8" + checksum: 95614a88873611cb9165a85d36afa7268af5c03a378b35ca7bda9508e1d4f1f6f19a788d4bc755b3fd37c8ebba40782018e02034564ff24c9d6fa37e959ad57d + languageName: node + linkType: hard + +"es6-promisify@npm:^5.0.0": + version: 5.0.0 + resolution: "es6-promisify@npm:5.0.0" + dependencies: + es6-promise: "npm:^4.0.3" + checksum: fbed9d791598831413be84a5374eca8c24800ec71a16c1c528c43a98e2dadfb99331483d83ae6094ddb9b87e6f799a15d1553cebf756047e0865c753bc346b92 + languageName: node + linkType: hard + +"eventemitter3@npm:^4.0.7": + version: 4.0.7 + resolution: "eventemitter3@npm:4.0.7" + checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374 + languageName: node + linkType: hard + +"exponential-backoff@npm:^3.1.1": + version: 3.1.1 + resolution: "exponential-backoff@npm:3.1.1" + checksum: 3d21519a4f8207c99f7457287291316306255a328770d320b401114ec8481986e4e467e854cb9914dd965e0a1ca810a23ccb559c642c88f4c7f55c55778a9b48 + languageName: node + linkType: hard + +"eyes@npm:^0.1.8": + version: 0.1.8 + resolution: "eyes@npm:0.1.8" + checksum: c31703a92bf36ba75ee8d379ee7985c24ee6149f3a6175f44cec7a05b178c38bce9836d3ca48c9acb0329a960ac2c4b2ead4e60cdd4fe6e8c92cad7cd6913687 + languageName: node + linkType: hard + +"fast-stable-stringify@npm:^1.0.0": + version: 1.0.0 + resolution: "fast-stable-stringify@npm:1.0.0" + checksum: ef1203d246a7e8ac15e2bfbda0a89fa375947bccf9f7910be0ea759856dbe8ea5024a0d8cc2cceabe18a9cb67e95927b78bb6173a3ae37ec55a518cf36e5244b + languageName: node + linkType: hard + +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: cc283f4e65b504259e64fd969bcf4def4eb08d85565e906b7d36516e87819db52029a76b6363d0f02d0d532f0033c9603b9e2d943d56ee3b0d4f7ad3328ff917 + languageName: node + linkType: hard + +"follow-redirects@npm:^1.15.0": + version: 1.15.2 + resolution: "follow-redirects@npm:1.15.2" + peerDependenciesMeta: + debug: + optional: true + checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190 + languageName: node + linkType: hard + +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: "npm:^7.0.0" + signal-exit: "npm:^4.0.1" + checksum: 139d270bc82dc9e6f8bc045fe2aae4001dc2472157044fdfad376d0a3457f77857fa883c1c8b21b491c6caade9a926a4bed3d3d2e8d3c9202b151a4cbbd0bcd5 + languageName: node + linkType: hard + +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 + languageName: node + linkType: hard + +"fs-minipass@npm:^3.0.0": + version: 3.0.3 + resolution: "fs-minipass@npm:3.0.3" + dependencies: + minipass: "npm:^7.0.3" + checksum: 8722a41109130851d979222d3ec88aabaceeaaf8f57b2a8f744ef8bd2d1ce95453b04a61daa0078822bc5cd21e008814f06fe6586f56fef511e71b8d2394d802 + languageName: node + linkType: hard + +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 + languageName: node + linkType: hard + +"gauge@npm:^4.0.3": + version: 4.0.4 + resolution: "gauge@npm:4.0.4" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.3" + console-control-strings: "npm:^1.1.0" + has-unicode: "npm:^2.0.1" + signal-exit: "npm:^3.0.7" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.5" + checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d + languageName: node + linkType: hard + +"git-format-staged@npm:^2.1.3": + version: 2.1.3 + resolution: "git-format-staged@npm:2.1.3" + bin: + git-format-staged: git-format-staged + checksum: 749da68f0d9bf24db53b87a5f1613fc1a8790801d7c3ccb31d02b94d99f4cf2450126ef565f16adcc0649fbbf90dc44b4f009d4f99ff8a26921ba754bdb09b31 + languageName: node + linkType: hard + +"glob@npm:^10.2.2": + version: 10.3.3 + resolution: "glob@npm:10.3.3" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^2.0.3" + minimatch: "npm:^9.0.1" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry: "npm:^1.10.1" + bin: + glob: dist/cjs/src/bin.js + checksum: 29190d3291f422da0cb40b77a72fc8d2c51a36524e99b8bf412548b7676a6627489528b57250429612b6eec2e6fe7826d328451d3e694a9d15e575389308ec53 + languageName: node + linkType: hard + +"glob@npm:^7.1.3, glob@npm:^7.1.4": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + +"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + +"has-flag@npm:^4.0.0": + version: 4.0.0 + resolution: "has-flag@npm:4.0.0" + checksum: 261a1357037ead75e338156b1f9452c016a37dcd3283a972a30d9e4a87441ba372c8b81f818cd0fbcd9c0354b4ae7e18b9e1afa1971164aef6d18c2b6095a8ad + languageName: node + linkType: hard + +"has-unicode@npm:^2.0.1": + version: 2.0.1 + resolution: "has-unicode@npm:2.0.1" + checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 + languageName: node + linkType: hard + +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 + languageName: node + linkType: hard + +"http-proxy-agent@npm:^5.0.0": + version: 5.0.0 + resolution: "http-proxy-agent@npm:5.0.0" + dependencies: + "@tootallnate/once": "npm:2" + agent-base: "npm:6" + debug: "npm:4" + checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 + languageName: node + linkType: hard + +"https-proxy-agent@npm:^5.0.0": + version: 5.0.1 + resolution: "https-proxy-agent@npm:5.0.1" + dependencies: + agent-base: "npm:6" + debug: "npm:4" + checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 + languageName: node + linkType: hard + +"humanize-ms@npm:^1.2.1": + version: 1.2.1 + resolution: "humanize-ms@npm:1.2.1" + dependencies: + ms: "npm:^2.0.0" + checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 + languageName: node + linkType: hard + +"iconv-lite@npm:^0.6.2": + version: 0.6.3 + resolution: "iconv-lite@npm:0.6.3" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf + languageName: node + linkType: hard + +"ieee754@npm:^1.2.1": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e + languageName: node + linkType: hard + +"imurmurhash@npm:^0.1.4": + version: 0.1.4 + resolution: "imurmurhash@npm:0.1.4" + checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 + languageName: node + linkType: hard + +"indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:^2.0.3": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 + languageName: node + linkType: hard + +"ip@npm:^2.0.0": + version: 2.0.0 + resolution: "ip@npm:2.0.0" + checksum: cfcfac6b873b701996d71ec82a7dd27ba92450afdb421e356f44044ed688df04567344c36cbacea7d01b1c39a4c732dc012570ebe9bebfb06f27314bca625349 + languageName: node + linkType: hard + +"is-fullwidth-code-point@npm:^3.0.0": + version: 3.0.0 + resolution: "is-fullwidth-code-point@npm:3.0.0" + checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 + languageName: node + linkType: hard + +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a + languageName: node + linkType: hard + +"isexe@npm:^2.0.0": + version: 2.0.0 + resolution: "isexe@npm:2.0.0" + checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 + languageName: node + linkType: hard + +"isomorphic-ws@npm:^4.0.1": + version: 4.0.1 + resolution: "isomorphic-ws@npm:4.0.1" + peerDependencies: + ws: "*" + checksum: d7190eadefdc28bdb93d67b5f0c603385aaf87724fa2974abb382ac1ec9756ed2cfb27065cbe76122879c2d452e2982bc4314317f3d6c737ddda6c047328771a + languageName: node + linkType: hard + +"jackspeak@npm:^2.0.3": + version: 2.3.1 + resolution: "jackspeak@npm:2.3.1" + dependencies: + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 34ea4d618d8d36ac104fe1053c85dfb6a63306cfe87e157ef42f18a7aa30027887370a4e163dd4993e45c6bf8a8ae003bf8476fdb8538e8ee5cd1938c27b15d0 + languageName: node + linkType: hard + +"jayson@npm:^4.1.0": + version: 4.1.0 + resolution: "jayson@npm:4.1.0" + dependencies: + "@types/connect": "npm:^3.4.33" + "@types/node": "npm:^12.12.54" + "@types/ws": "npm:^7.4.4" + JSONStream: "npm:^1.3.5" + commander: "npm:^2.20.3" + delay: "npm:^5.0.0" + es6-promisify: "npm:^5.0.0" + eyes: "npm:^0.1.8" + isomorphic-ws: "npm:^4.0.1" + json-stringify-safe: "npm:^5.0.1" + uuid: "npm:^8.3.2" + ws: "npm:^7.4.5" + bin: + jayson: bin/jayson.js + checksum: 86464322fbdc6db65d2bb4fc278cb6c86fad5c2a506065490d39459f09ba0d30f2b4fb740b33828a1424791419b6c8bd295dc54d361a4ad959bf70cc62b1ca7e + languageName: node + linkType: hard + +"js-sha256@npm:^0.9.0": + version: 0.9.0 + resolution: "js-sha256@npm:0.9.0" + checksum: ffad54b3373f81581e245866abfda50a62c483803a28176dd5c28fd2d313e0bdf830e77dac7ff8afd193c53031618920f3d98daf21cbbe80082753ab639c0365 + languageName: node + linkType: hard + +"js-sha3@npm:^0.8.0": + version: 0.8.0 + resolution: "js-sha3@npm:0.8.0" + checksum: 75df77c1fc266973f06cce8309ce010e9e9f07ec35ab12022ed29b7f0d9c8757f5a73e1b35aa24840dced0dea7059085aa143d817aea9e188e2a80d569d9adce + languageName: node + linkType: hard + +"json-stringify-safe@npm:^5.0.1": + version: 5.0.1 + resolution: "json-stringify-safe@npm:5.0.1" + checksum: 48ec0adad5280b8a96bb93f4563aa1667fd7a36334f79149abd42446d0989f2ddc58274b479f4819f1f00617957e6344c886c55d05a4e15ebb4ab931e4a6a8ee + languageName: node + linkType: hard + +"jsonparse@npm:^1.2.0": + version: 1.3.1 + resolution: "jsonparse@npm:1.3.1" + checksum: 6514a7be4674ebf407afca0eda3ba284b69b07f9958a8d3113ef1005f7ec610860c312be067e450c569aab8b89635e332cee3696789c750692bb60daba627f4d + languageName: node + linkType: hard + +"lower-case@npm:^2.0.2": + version: 2.0.2 + resolution: "lower-case@npm:2.0.2" + dependencies: + tslib: "npm:^2.0.3" + checksum: 83a0a5f159ad7614bee8bf976b96275f3954335a84fad2696927f609ddae902802c4f3312d86668722e668bef41400254807e1d3a7f2e8c3eede79691aa1f010 + languageName: node + linkType: hard + +"lru-cache@npm:^6.0.0": + version: 6.0.0 + resolution: "lru-cache@npm:6.0.0" + dependencies: + yallist: "npm:^4.0.0" + checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 + languageName: node + linkType: hard + +"lru-cache@npm:^7.7.1": + version: 7.18.3 + resolution: "lru-cache@npm:7.18.3" + checksum: e550d772384709deea3f141af34b6d4fa392e2e418c1498c078de0ee63670f1f46f5eee746e8ef7e69e1c895af0d4224e62ee33e66a543a14763b0f2e74c1356 + languageName: node + linkType: hard + +"lru-cache@npm:^9.1.1 || ^10.0.0": + version: 10.0.1 + resolution: "lru-cache@npm:10.0.1" + checksum: 06f8d0e1ceabd76bb6f644a26dbb0b4c471b79c7b514c13c6856113879b3bf369eb7b497dad4ff2b7e2636db202412394865b33c332100876d838ad1372f0181 + languageName: node + linkType: hard + +"make-error@npm:^1.1.1": + version: 1.3.6 + resolution: "make-error@npm:1.3.6" + checksum: b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 + languageName: node + linkType: hard + +"make-fetch-happen@npm:^11.0.3": + version: 11.1.1 + resolution: "make-fetch-happen@npm:11.1.1" + dependencies: + agentkeepalive: "npm:^4.2.1" + cacache: "npm:^17.0.0" + http-cache-semantics: "npm:^4.1.1" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^7.7.1" + minipass: "npm:^5.0.0" + minipass-fetch: "npm:^3.0.0" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^7.0.0" + ssri: "npm:^10.0.0" + checksum: 7268bf274a0f6dcf0343829489a4506603ff34bd0649c12058753900b0eb29191dce5dba12680719a5d0a983d3e57810f594a12f3c18494e93a1fbc6348a4540 + languageName: node + linkType: hard + +"micromatch@npm:^4.0.0": + version: 4.0.5 + resolution: "micromatch@npm:4.0.5" + dependencies: + braces: "npm:^3.0.2" + picomatch: "npm:^2.3.1" + checksum: 02a17b671c06e8fefeeb6ef996119c1e597c942e632a21ef589154f23898c9c6a9858526246abb14f8bca6e77734aa9dcf65476fca47cedfb80d9577d52843fc + languageName: node + linkType: hard + +"mime-db@npm:1.52.0": + version: 1.52.0 + resolution: "mime-db@npm:1.52.0" + checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12": + version: 2.1.35 + resolution: "mime-types@npm:2.1.35" + dependencies: + mime-db: "npm:1.52.0" + checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836 + languageName: node + linkType: hard + +"minimatch@npm:^3.1.1": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + +"minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + +"minipass-collect@npm:^1.0.2": + version: 1.0.2 + resolution: "minipass-collect@npm:1.0.2" + dependencies: + minipass: "npm:^3.0.0" + checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 + languageName: node + linkType: hard + +"minipass-fetch@npm:^3.0.0": + version: 3.0.4 + resolution: "minipass-fetch@npm:3.0.4" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^7.0.3" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: af7aad15d5c128ab1ebe52e043bdf7d62c3c6f0cecb9285b40d7b395e1375b45dcdfd40e63e93d26a0e8249c9efd5c325c65575aceee192883970ff8cb11364a + languageName: node + linkType: hard + +"minipass-flush@npm:^1.0.5": + version: 1.0.5 + resolution: "minipass-flush@npm:1.0.5" + dependencies: + minipass: "npm:^3.0.0" + checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf + languageName: node + linkType: hard + +"minipass-pipeline@npm:^1.2.4": + version: 1.2.4 + resolution: "minipass-pipeline@npm:1.2.4" + dependencies: + minipass: "npm:^3.0.0" + checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b + languageName: node + linkType: hard + +"minipass-sized@npm:^1.0.3": + version: 1.0.3 + resolution: "minipass-sized@npm:1.0.3" + dependencies: + minipass: "npm:^3.0.0" + checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 + languageName: node + linkType: hard + +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: a30d083c8054cee83cdcdc97f97e4641a3f58ae743970457b1489ce38ee1167b3aaf7d815cd39ec7a99b9c40397fd4f686e83750e73e652b21cb516f6d845e48 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 425dab288738853fded43da3314a0b5c035844d6f3097a8e3b5b29b328da8f3c1af6fc70618b32c29ff906284cf6406b6841376f21caaadd0793c1d5a6a620ea + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": + version: 7.0.3 + resolution: "minipass@npm:7.0.3" + checksum: 6f1614f5b5b55568a46bca5fec0e7c46dac027691db27d0e1923a8192866903144cd962ac772c0e9f89b608ea818b702709c042bce98e190d258847d85461531 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" + dependencies: + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" + bin: + mkdirp: bin/cmd.js + checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + +"ms@npm:^2.0.0": + version: 2.1.3 + resolution: "ms@npm:2.1.3" + checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d + languageName: node + linkType: hard + +"multiformats@npm:^9.6.4": + version: 9.9.0 + resolution: "multiformats@npm:9.9.0" + checksum: d3e8c1be400c09a014f557ea02251a2710dbc9fca5aa32cc702ff29f636c5471e17979f30bdcb0a9cbb556f162a8591dc2e1219c24fc21394a56115b820bb84e + languageName: node + linkType: hard + +"negotiator@npm:^0.6.3": + version: 0.6.3 + resolution: "negotiator@npm:0.6.3" + checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 + languageName: node + linkType: hard + +"no-case@npm:^3.0.4": + version: 3.0.4 + resolution: "no-case@npm:3.0.4" + dependencies: + lower-case: "npm:^2.0.2" + tslib: "npm:^2.0.3" + checksum: 0b2ebc113dfcf737d48dde49cfebf3ad2d82a8c3188e7100c6f375e30eafbef9e9124aadc3becef237b042fd5eb0aad2fd78669c20972d045bbe7fea8ba0be5c + languageName: node + linkType: hard + +"node-fetch@npm:^2.6.12, node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 + languageName: node + linkType: hard + +"node-gyp-build@npm:^4.3.0": + version: 4.6.1 + resolution: "node-gyp-build@npm:4.6.1" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: c3676d337b36803bc7792e35bf7fdcda7cdcb7e289b8f9855a5535702a82498eb976842fefcf487258c58005ca32ce3d537fbed91280b04409161dcd7232a882 + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 9.4.0 + resolution: "node-gyp@npm:9.4.0" + dependencies: + env-paths: "npm:^2.2.0" + exponential-backoff: "npm:^3.1.1" + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.6" + make-fetch-happen: "npm:^11.0.3" + nopt: "npm:^6.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.2" + which: "npm:^2.0.2" + bin: + node-gyp: bin/node-gyp.js + checksum: 78b404e2e0639d64e145845f7f5a3cb20c0520cdaf6dda2f6e025e9b644077202ea7de1232396ba5bde3fee84cdc79604feebe6ba3ec84d464c85d407bb5da99 + languageName: node + linkType: hard + +"nopt@npm:^6.0.0": + version: 6.0.0 + resolution: "nopt@npm:6.0.0" + dependencies: + abbrev: "npm:^1.0.0" + bin: + nopt: bin/nopt.js + checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac + languageName: node + linkType: hard + +"npmlog@npm:^6.0.0": + version: 6.0.2 + resolution: "npmlog@npm:6.0.2" + dependencies: + are-we-there-yet: "npm:^3.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^4.0.3" + set-blocking: "npm:^2.0.0" + checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a + languageName: node + linkType: hard + +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 + languageName: node + linkType: hard + +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c + languageName: node + linkType: hard + +"pako@npm:^2.0.3": + version: 2.1.0 + resolution: "pako@npm:2.1.0" + checksum: 71666548644c9a4d056bcaba849ca6fd7242c6cf1af0646d3346f3079a1c7f4a66ffec6f7369ee0dc88f61926c10d6ab05da3e1fca44b83551839e89edd75a3e + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 + languageName: node + linkType: hard + +"path-key@npm:^3.1.0": + version: 3.1.1 + resolution: "path-key@npm:3.1.1" + checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 + languageName: node + linkType: hard + +"path-scurry@npm:^1.10.1": + version: 1.10.1 + resolution: "path-scurry@npm:1.10.1" + dependencies: + lru-cache: "npm:^9.1.1 || ^10.0.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: e2557cff3a8fb8bc07afdd6ab163a92587884f9969b05bbbaf6fe7379348bfb09af9ed292af12ed32398b15fb443e81692047b786d1eeb6d898a51eb17ed7d90 + languageName: node + linkType: hard + +"picomatch@npm:^2.3.1": + version: 2.3.1 + resolution: "picomatch@npm:2.3.1" + checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf + languageName: node + linkType: hard + +"promise-retry@npm:^2.0.1": + version: 2.0.1 + resolution: "promise-retry@npm:2.0.1" + dependencies: + err-code: "npm:^2.0.2" + retry: "npm:^0.12.0" + checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 + languageName: node + linkType: hard + +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 + languageName: node + linkType: hard + +"readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: 1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3 + languageName: node + linkType: hard + +"retry@npm:^0.12.0": + version: 0.12.0 + resolution: "retry@npm:0.12.0" + checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c + languageName: node + linkType: hard + +"rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 + languageName: node + linkType: hard + +"rpc-websockets@npm:^7.5.1": + version: 7.6.0 + resolution: "rpc-websockets@npm:7.6.0" + dependencies: + "@babel/runtime": "npm:^7.17.2" + bufferutil: "npm:^4.0.1" + eventemitter3: "npm:^4.0.7" + utf-8-validate: "npm:^5.0.2" + uuid: "npm:^8.3.2" + ws: "npm:^8.5.0" + dependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: af2b254f65985610bd354e8e13de07b5a36010b94672b0b5a9d226b9bb1b8b17d01c63221cad97263845888f3610e55867a32e4c0017dfb92fddf89417c4cb6c + languageName: node + linkType: hard + +"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0": + version: 2.1.2 + resolution: "safer-buffer@npm:2.1.2" + checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 + languageName: node + linkType: hard + +"semver@npm:^7.3.4, semver@npm:^7.3.5": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 + languageName: node + linkType: hard + +"shebang-command@npm:^2.0.0": + version: 2.0.0 + resolution: "shebang-command@npm:2.0.0" + dependencies: + shebang-regex: "npm:^3.0.0" + checksum: 6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa + languageName: node + linkType: hard + +"shebang-regex@npm:^3.0.0": + version: 3.0.0 + resolution: "shebang-regex@npm:3.0.0" + checksum: 1a2bcae50de99034fcd92ad4212d8e01eedf52c7ec7830eedcf886622804fe36884278f2be8be0ea5fde3fd1c23911643a4e0f726c8685b61871c8908af01222 + languageName: node + linkType: hard + +"signal-exit@npm:^3.0.7": + version: 3.0.7 + resolution: "signal-exit@npm:3.0.7" + checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 + languageName: node + linkType: hard + +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 64c757b498cb8629ffa5f75485340594d2f8189e9b08700e69199069c8e3070fb3e255f7ab873c05dc0b3cec412aea7402e10a5990cb6a050bd33ba062a6c549 + languageName: node + linkType: hard + +"smart-buffer@npm:^4.2.0": + version: 4.2.0 + resolution: "smart-buffer@npm:4.2.0" + checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b + languageName: node + linkType: hard + +"snake-case@npm:^3.0.4": + version: 3.0.4 + resolution: "snake-case@npm:3.0.4" + dependencies: + dot-case: "npm:^3.0.4" + tslib: "npm:^2.0.3" + checksum: 0a7a79900bbb36f8aaa922cf111702a3647ac6165736d5dc96d3ef367efc50465cac70c53cd172c382b022dac72ec91710608e5393de71f76d7142e6fd80e8a3 + languageName: node + linkType: hard + +"socks-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "socks-proxy-agent@npm:7.0.0" + dependencies: + agent-base: "npm:^6.0.2" + debug: "npm:^4.3.3" + socks: "npm:^2.6.2" + checksum: 720554370154cbc979e2e9ce6a6ec6ced205d02757d8f5d93fe95adae454fc187a5cbfc6b022afab850a5ce9b4c7d73e0f98e381879cf45f66317a4895953846 + languageName: node + linkType: hard + +"socks@npm:^2.6.2": + version: 2.7.1 + resolution: "socks@npm:2.7.1" + dependencies: + ip: "npm:^2.0.0" + smart-buffer: "npm:^4.2.0" + checksum: 259d9e3e8e1c9809a7f5c32238c3d4d2a36b39b83851d0f573bfde5f21c4b1288417ce1af06af1452569cd1eb0841169afd4998f0e04ba04656f6b7f0e46d748 + languageName: node + linkType: hard + +"ssri@npm:^10.0.0": + version: 10.0.5 + resolution: "ssri@npm:10.0.5" + dependencies: + minipass: "npm:^7.0.3" + checksum: 0a31b65f21872dea1ed3f7c200d7bc1c1b91c15e419deca14f282508ba917cbb342c08a6814c7f68ca4ca4116dd1a85da2bbf39227480e50125a1ceffeecb750 + languageName: node + linkType: hard + +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" + dependencies: + emoji-regex: "npm:^8.0.0" + is-fullwidth-code-point: "npm:^3.0.0" + strip-ansi: "npm:^6.0.1" + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + languageName: node + linkType: hard + +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 + languageName: node + linkType: hard + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: "npm:^5.0.1" + checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c + languageName: node + linkType: hard + +"strip-ansi@npm:^7.0.1": + version: 7.1.0 + resolution: "strip-ansi@npm:7.1.0" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 859c73fcf27869c22a4e4d8c6acfe690064659e84bef9458aa6d13719d09ca88dcfd40cbf31fd0be63518ea1a643fe070b4827d353e09533a5b0b9fd4553d64d + languageName: node + linkType: hard + +"superstruct@npm:^0.14.2": + version: 0.14.2 + resolution: "superstruct@npm:0.14.2" + checksum: c5c4840f432da82125b923ec45faca5113217e83ae416e314d80eae012b8bb603d2e745025d173450758d116348820bc7028157f8c9a72b6beae879f94b837c0 + languageName: node + linkType: hard + +"superstruct@npm:^0.15.4": + version: 0.15.5 + resolution: "superstruct@npm:0.15.5" + checksum: 6d1f5249fee789424b7178fa0a1ffb2ace629c5480c39505885bd8c0046a4ff8b267569a3442fa53b8c560a7ba6599cf3f8af94225aebeb2cf6023f7dd911050 + languageName: node + linkType: hard + +"supports-color@npm:^7.1.0": + version: 7.2.0 + resolution: "supports-color@npm:7.2.0" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 3dda818de06ebbe5b9653e07842d9479f3555ebc77e9a0280caf5a14fb877ffee9ed57007c3b78f5a6324b8dbeec648d9e97a24e2ed9fdb81ddc69ea07100f4a + languageName: node + linkType: hard + +"tapable@npm:^2.2.0": + version: 2.2.1 + resolution: "tapable@npm:2.2.1" + checksum: 3b7a1b4d86fa940aad46d9e73d1e8739335efd4c48322cb37d073eb6f80f5281889bf0320c6d8ffcfa1a0dd5bfdbd0f9d037e252ef972aca595330538aac4d51 + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": + version: 6.1.15 + resolution: "tar@npm:6.1.15" + dependencies: + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: f23832fceeba7578bf31907aac744ae21e74a66f4a17a9e94507acf460e48f6db598c7023882db33bab75b80e027c21f276d405e4a0322d58f51c7088d428268 + languageName: node + linkType: hard + +"text-encoding-utf-8@npm:^1.0.2": + version: 1.0.2 + resolution: "text-encoding-utf-8@npm:1.0.2" + checksum: ec4c15d50e738c5dba7327ad432ebf0725ec75d4d69c0bd55609254c5a3bc5341272d7003691084a0a73d60d981c8eb0e87603676fdb6f3fed60f4c9192309f9 + languageName: node + linkType: hard + +"through@npm:>=2.2.7 <3": + version: 2.3.8 + resolution: "through@npm:2.3.8" + checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd + languageName: node + linkType: hard + +"to-regex-range@npm:^5.0.1": + version: 5.0.1 + resolution: "to-regex-range@npm:5.0.1" + dependencies: + is-number: "npm:^7.0.0" + checksum: f76fa01b3d5be85db6a2a143e24df9f60dd047d151062d0ba3df62953f2f697b16fe5dad9b0ac6191c7efc7b1d9dcaa4b768174b7b29da89d4428e64bc0a20ed + languageName: node + linkType: hard + +"toml@npm:^3.0.0": + version: 3.0.0 + resolution: "toml@npm:3.0.0" + checksum: 5d7f1d8413ad7780e9bdecce8ea4c3f5130dd53b0a4f2e90b93340979a137739879d7b9ce2ce05c938b8cc828897fe9e95085197342a1377dd8850bf5125f15f + languageName: node + linkType: hard + +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 726321c5eaf41b5002e17ffbd1fb7245999a073e8979085dacd47c4b4e8068ff5777142fc6726d6ca1fd2ff16921b48788b87225cbc57c72636f6efa8efbffe3 + languageName: node + linkType: hard + +"ts-loader@npm:^9.2.3": + version: 9.4.4 + resolution: "ts-loader@npm:9.4.4" + dependencies: + chalk: "npm:^4.1.0" + enhanced-resolve: "npm:^5.0.0" + micromatch: "npm:^4.0.0" + semver: "npm:^7.3.4" + peerDependencies: + typescript: "*" + webpack: ^5.0.0 + checksum: 8e5e6b839b0edfa40d2156c880d88ccab58226894ea5978221bc48c7db3215e2e856bfd0093f148e925a2befc42d6c94cafa9a994a7da274541efaa916012b63 + languageName: node + linkType: hard + +"ts-node@npm:^10.9.1": + version: 10.9.1 + resolution: "ts-node@npm:10.9.1" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 090adff1302ab20bd3486e6b4799e90f97726ed39e02b39e566f8ab674fd5bd5f727f43615debbfc580d33c6d9d1c6b1b3ce7d8e3cca3e20530a145ffa232c35 + languageName: node + linkType: hard + +"tslib@npm:^2.0.3": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + +"typescript-collections@npm:^1.3.3": + version: 1.3.3 + resolution: "typescript-collections@npm:1.3.3" + checksum: a27f07dffdfe8407c4302eedb3e578b1360de783626cbd53519bd9c7943293a2940ed1ec3004cafae9dce049768b143185a36ca812d0d5c3c0f621a289239633 + languageName: node + linkType: hard + +"typescript@npm:^5.2.2": + version: 5.2.2 + resolution: "typescript@npm:5.2.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c + languageName: node + linkType: hard + +"typescript@patch:typescript@^5.2.2#~builtin": + version: 5.2.2 + resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin::version=5.2.2&hash=f3b441" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca + languageName: node + linkType: hard + +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" + dependencies: + unique-slug: "npm:^4.0.0" + checksum: 8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df + languageName: node + linkType: hard + +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" + dependencies: + imurmurhash: "npm:^0.1.4" + checksum: 0884b58365af59f89739e6f71e3feacb5b1b41f2df2d842d0757933620e6de08eff347d27e9d499b43c40476cbaf7988638d3acb2ffbcb9d35fd035591adfd15 + languageName: node + linkType: hard + +"utf-8-validate@npm:^5.0.2": + version: 5.0.10 + resolution: "utf-8-validate@npm:5.0.10" + dependencies: + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 5579350a023c66a2326752b6c8804cc7b39dcd251bb088241da38db994b8d78352e388dcc24ad398ab98385ba3c5ffcadb6b5b14b2637e43f767869055e46ba6 + languageName: node + linkType: hard + +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 + languageName: node + linkType: hard + +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df + languageName: node + linkType: hard + +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 78089ad549e21bcdbfca10c08850022b22024cdcc2da9b168bcf5a73a6ed7bf01a9cebb9eac28e03cd23a684d81e0502797e88f3ccd27a32aeab1cfc44c39da0 + languageName: node + linkType: hard + +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: c92a0a6ab95314bde9c32e1d0a6dfac83b578f8fa5f21e675bc2706ed6981bc26b7eb7e6a1fab158e5ce4adf9caa4a0aee49a52505d4d13c7be545f15021b17c + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: b8daed4ad3356cc4899048a15b2c143a9aed0dfae1f611ebd55073310c7b910f522ad75d727346ad64203d7e6c79ef25eafd465f4d12775ca44b90fa82ed9e2c + languageName: node + linkType: hard + +"which@npm:^2.0.1, which@npm:^2.0.2": + version: 2.0.2 + resolution: "which@npm:2.0.2" + dependencies: + isexe: "npm:^2.0.0" + bin: + node-which: ./bin/node-which + checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 + languageName: node + linkType: hard + +"wide-align@npm:^1.1.5": + version: 1.1.5 + resolution: "wide-align@npm:1.1.5" + dependencies: + string-width: "npm:^1.0.2 || 2 || 3 || 4" + checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 + languageName: node + linkType: hard + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 + languageName: node + linkType: hard + +"ws@npm:^7.4.5": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 + languageName: node + linkType: hard + +"ws@npm:^8.5.0": + version: 8.13.0 + resolution: "ws@npm:8.13.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c + languageName: node + linkType: hard + +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 + languageName: node + linkType: hard + +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 + languageName: node + linkType: hard diff --git a/programs/position-voting-rewards/Cargo.toml b/programs/position-voting-rewards/Cargo.toml index de24595c6..4ffa83e1e 100644 --- a/programs/position-voting-rewards/Cargo.toml +++ b/programs/position-voting-rewards/Cargo.toml @@ -25,5 +25,6 @@ anchor-lang = { workspace = true } anchor-spl = { workspace = true } shared-utils = { workspace = true } solana-security-txt = { workspace = true } -voter-stake-registry = { path = "../voter-stake-registry" } +voter-stake-registry = { path = "../voter-stake-registry", features = ["no-entrypoint", "cpi"] } default-env = { workspace = true } +proposal = { path = "../../utils/proposal" } diff --git a/programs/position-voting-rewards/src/instructions/enroll_v0.rs b/programs/position-voting-rewards/src/instructions/enroll_v0.rs index 79e3d5509..bb50f2aa4 100644 --- a/programs/position-voting-rewards/src/instructions/enroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/enroll_v0.rs @@ -204,6 +204,7 @@ pub fn handler(ctx: Context) -> Result<()> { initialized: false, registrar: ctx.accounts.registrar.key(), rewards_amount: 0, + recent_proposals: [Pubkey::default(); 4], }, }, &[ diff --git a/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs new file mode 100644 index 000000000..e95ca8af1 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs @@ -0,0 +1,42 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::Mint; +use voter_stake_registry::state::Registrar; + +use crate::state::VeTokenTrackerV0; + +#[derive(Accounts)] +pub struct InitializeVeTokenTrackerV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, + #[account( + init, + payer = payer, + space = 60 + VeTokenTrackerV0::INIT_SPACE, + seeds = ["vetoken_tracker".as_bytes(), registrar.key().as_ref()], + bump, + )] + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + #[account( + has_one = realm_authority, + )] + pub registrar: Account<'info, Registrar>, + pub rewards_mint: Account<'info, Mint>, + pub realm_authority: Signer<'info>, + /// CHECK: Just an argument + pub rewards_authority: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} + +pub fn handler(ctx: Context) -> Result<()> { + ctx.accounts.vetoken_tracker.set_inner(VeTokenTrackerV0 { + registrar: ctx.accounts.registrar.key(), + rewards_authority: ctx.accounts.rewards_authority.key(), + rewards_mint: ctx.accounts.rewards_mint.key(), + vetoken_last_calculated_ts: ctx.accounts.registrar.clock_unix_timestamp(), + vetoken_fall_rate: 0, + total_vetokens: 0, + recent_proposals: [Pubkey::default(); 4], + bump_seed: ctx.bumps["vetoken_tracker"], + }); + Ok(()) +} diff --git a/programs/position-voting-rewards/src/instructions/mod.rs b/programs/position-voting-rewards/src/instructions/mod.rs index e8bbe6cfb..c9b15cac5 100644 --- a/programs/position-voting-rewards/src/instructions/mod.rs +++ b/programs/position-voting-rewards/src/instructions/mod.rs @@ -1,11 +1,17 @@ pub mod claim_rewards_v0; pub mod enroll_v0; +pub mod initialize_vetoken_tracker_v0; pub mod reset_lockup_v0; +pub mod reward_for_epoch_v0; +pub mod track_vote_v0; pub mod transfer_v0; pub mod unenroll_v0; pub use claim_rewards_v0::*; pub use enroll_v0::*; +pub use initialize_vetoken_tracker_v0::*; pub use reset_lockup_v0::*; +pub use reward_for_epoch_v0::*; +pub use track_vote_v0::*; pub use transfer_v0::*; pub use unenroll_v0::*; diff --git a/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs new file mode 100644 index 000000000..d830acdc1 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs @@ -0,0 +1,13 @@ +use anchor_lang::prelude::*; + +use crate::state::VeTokenTrackerV0; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct RewardForEpochArgsV0 { + pub amount: u64, +} + +#[derive(Accounts)] +pub struct RewardForEpochV0<'info> { + pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, +} diff --git a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs new file mode 100644 index 000000000..f87a23f23 --- /dev/null +++ b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs @@ -0,0 +1,53 @@ +use anchor_lang::prelude::*; +use proposal::ProposalV0; +use voter_stake_registry::{ + state::{PositionV0, VoteMarkerV0}, + VoterStakeRegistry, +}; + +use crate::state::{EnrolledPositionV0, VeTokenTrackerV0}; + +#[derive(Accounts)] +pub struct TrackVoteV0<'info> { + // Proves that the call is coming from vsrr + pub registrar: Signer<'info>, + pub proposal: Account<'info, ProposalV0>, + #[account( + mut, + has_one = registrar + )] + pub position: Box>, + /// CHECK: Checked by seeds + #[account( + seeds = [b"marker", position.mint.as_ref(), proposal.key().as_ref()], + bump , + seeds::program = vsr_program + )] + pub marker: AccountInfo<'info>, + #[account(mut, has_one = registrar)] + pub ve_token_tracker: Account<'info, VeTokenTrackerV0>, + pub enrolled_position: Account<'info, EnrolledPositionV0>, + pub vsr_program: Program<'info, VoterStakeRegistry>, +} + +pub fn handler(ctx: Context) -> Result<()> { + let data = ctx.accounts.marker.data.try_borrow().unwrap(); + let has_data = !data.is_empty(); + let mut voted = has_data; + if has_data { + let marker = VoteMarkerV0::try_from_slice(&data)?; + voted = !marker.choices.is_empty(); + } + if voted { + ctx.accounts.enrolled_position.add_recent_proposal( + ctx.accounts.proposal.key(), + ctx.accounts.proposal.created_at, + ); + } else { + ctx + .accounts + .enrolled_position + .remove_recent_proposal(ctx.accounts.proposal.key()); + } + Ok(()) +} diff --git a/programs/position-voting-rewards/src/lib.rs b/programs/position-voting-rewards/src/lib.rs index 369092abe..295697466 100644 --- a/programs/position-voting-rewards/src/lib.rs +++ b/programs/position-voting-rewards/src/lib.rs @@ -4,8 +4,6 @@ use {default_env::default_env, solana_security_txt::security_txt}; declare_id!("pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM"); -use anchor_spl::token::{Mint, Token, TokenAccount}; - #[cfg(not(feature = "no-entrypoint"))] security_txt! { name: "Position Voting Rewards", @@ -28,8 +26,37 @@ pub mod instructions; pub mod state; pub mod util; +use instructions::*; + #[program] pub mod position_voting_rewards { - use super::*; + + pub fn initialize_vetoken_tracker_v0(ctx: Context) -> Result<()> { + initialize_vetoken_tracker_v0::handler(ctx) + } + + pub fn enroll_v0(ctx: Context) -> Result<()> { + enroll_v0::handler(ctx) + } + + pub fn claim_rewards_v0(ctx: Context, args: ClaimRewardsArgsV0) -> Result<()> { + claim_rewards_v0::handler(ctx, args) + } + + pub fn reset_lockup_v0(ctx: Context, args: ResetLockupArgsV0) -> Result<()> { + reset_lockup_v0::handler(ctx, args) + } + + pub fn transfer_v0(ctx: Context, args: TransferArgsV0) -> Result<()> { + transfer_v0::handler(ctx, args) + } + + pub fn unenroll_v0(ctx: Context) -> Result<()> { + unenroll_v0::handler(ctx) + } + + pub fn track_vote_v0(ctx: Context) -> Result<()> { + track_vote_v0::handler(ctx) + } } diff --git a/programs/position-voting-rewards/src/state.rs b/programs/position-voting-rewards/src/state.rs index 814dfafe1..bf2f11c92 100644 --- a/programs/position-voting-rewards/src/state.rs +++ b/programs/position-voting-rewards/src/state.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering; - use anchor_lang::prelude::*; use crate::{ @@ -15,13 +13,19 @@ pub struct EnrolledPositionV0 { pub position: Pubkey, pub start_ts: i64, pub is_rewards_enrolled: bool, - pub recent_proposals: [Pubkey; 4], pub last_claimed_epoch: u64, // the latest epoch not included claimed_epochs_bitmap // A bitmap of epochs past last_claimed_epoch (exclusive) that have been claimed. // This bitmap gets rotated as last_claimed_epoch increases. // This allows for claiming ~128 epochs worth of rewards in parallel. pub claimed_epochs_bitmap: u128, pub bump_seed: u8, + pub recent_proposals: Vec, +} + +#[derive(Default, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct RecentProposal { + pub proposal: Pubkey, + pub ts: i64, } pub const TESTING: bool = std::option_env!("TESTING").is_some(); @@ -59,62 +63,27 @@ impl EnrolledPositionV0 { } // Add a proposal to the recent proposals list - pub fn add_recent_proposal(&mut self, proposal_key: Pubkey, valid_proposals: &[Pubkey]) { - self.clean_recent_proposals(valid_proposals); + pub fn add_recent_proposal(&mut self, proposal: Pubkey, ts: i64) { + let new_proposal = RecentProposal { proposal, ts }; - let recent_proposals = &mut self.recent_proposals; - if !recent_proposals.contains(&proposal_key) && valid_proposals.contains(&proposal_key) { - if let Some(index) = recent_proposals - .iter() - .position(|&x| x == Pubkey::default()) - { - recent_proposals[index] = proposal_key; - } else { - // Shift array to the left - for i in 0..recent_proposals.len() - 1 { - recent_proposals[i] = recent_proposals[i + 1]; - } - recent_proposals[recent_proposals.len() - 1] = proposal_key; - } - } - } - - // Remove a proposal from the recent proposals list - pub fn remove_recent_proposal(&mut self, proposal_key: Pubkey, valid_proposals: &[Pubkey]) { - self.clean_recent_proposals(valid_proposals); + // Find the insertion point to maintain descending order by timestamp + let insert_index = self + .recent_proposals + .iter() + .position(|p| p.ts <= ts) + .unwrap_or(self.recent_proposals.len()); - let recent_proposals = &mut self.recent_proposals; - if let Some(index) = recent_proposals.iter().position(|&x| x == proposal_key) { - // Shift elements to the left, overwriting the removed proposal - for i in index..recent_proposals.len() - 1 { - recent_proposals[i] = recent_proposals[i + 1]; - } - // Set the last element to Pubkey::default() - recent_proposals[recent_proposals.len() - 1] = Pubkey::default(); - } + // Insert the new proposal + self.recent_proposals.insert(insert_index, new_proposal); } - // Remove all proposals that are not in the valid_proposals list - fn clean_recent_proposals(&mut self, valid_proposals: &[Pubkey]) { - for proposal in self.recent_proposals.iter_mut() { - if *proposal != Pubkey::default() && !valid_proposals.contains(proposal) { - *proposal = Pubkey::default(); - } - } - self.compact_recent_proposals(); + pub fn remove_recent_proposal(&mut self, proposal: Pubkey) { + self.recent_proposals.retain(|p| p.proposal != proposal); } - // Remove all Pubkey defaults and place them at the end of the array - fn compact_recent_proposals(&mut self) { - let non_default = self - .recent_proposals - .iter() - .filter(|&&pubkey| pubkey != Pubkey::default()) - .cloned() - .collect::>(); - - self.recent_proposals.fill(Pubkey::default()); - self.recent_proposals[..non_default.len()].copy_from_slice(&non_default); + // Remove proposals older than the given timestamp + pub fn remove_proposals_older_than(&mut self, ts: i64) { + self.recent_proposals.retain(|p| p.ts >= ts); } } @@ -125,6 +94,7 @@ pub struct VsrEpochInfoV0 { pub vetoken_tracker: Pubkey, pub registrar: Pubkey, pub initialized: bool, + pub recent_proposals: [Pubkey; 4], pub vetokens_at_epoch_start: u128, /// The number of enrollment rewards issued this epoch, so that enrollies can claim their share of the rewards pub rewards_amount: u64, @@ -165,9 +135,11 @@ impl VsrEpochInfoV0 { } #[account] +#[derive(InitSpace)] pub struct VeTokenTrackerV0 { pub registrar: Pubkey, pub rewards_mint: Pubkey, + pub rewards_authority: Pubkey, pub vetoken_last_calculated_ts: i64, pub vetoken_fall_rate: u128, // the vetoken amount that the position decays by per second, with 12 decimals of extra precision pub total_vetokens: u128, // the total amount of vetoken staked to this subdao, with 12 decimals of extra precision @@ -175,6 +147,17 @@ pub struct VeTokenTrackerV0 { pub bump_seed: u8, } +#[macro_export] +macro_rules! vetoken_tracker_seeds { + ( $vt:expr ) => { + &[ + b"vetoken_tracker".as_ref(), + $vt.registrar.as_ref(), + &[$vt.bump_seed], + ] + }; +} + impl VeTokenTrackerV0 { pub fn update_vetokens( &mut self, diff --git a/tsconfig.json b/tsconfig.json index 4c1f31c15..12d281482 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,9 @@ { "path": "./packages/voter-stake-registry-hooks" }, + { + "path": "./packages/position-voting-rewards-sdk" + }, { "path": "./packages/distributor-oracle" }, diff --git a/yarn.lock b/yarn.lock index 1674ad00d..40805246e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1362,6 +1362,24 @@ __metadata: languageName: node linkType: hard +"@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk": + version: 0.0.0-use.local + resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.9.7 + "@helium/idls": ^0.9.7 + "@helium/spl-utils": ^0.9.7 + "@solana/spl-token": ^0.3.8 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/price-oracle-sdk@^0.9.7, @helium/price-oracle-sdk@workspace:packages/price-oracle-sdk": version: 0.0.0-use.local resolution: "@helium/price-oracle-sdk@workspace:packages/price-oracle-sdk" From 5d24a0fe24848129342084fbc68da1eb7a79a96c Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Thu, 10 Oct 2024 14:09:00 -0700 Subject: [PATCH 03/17] Think the code is done... on to tets --- packages/helium-admin-cli/src/create-dao.ts | 1 + .../helium-admin-cli/src/create-subdao.ts | 1 + programs/position-voting-rewards/src/error.rs | 2 + .../src/instructions/claim_rewards_v0.rs | 82 ++- .../src/instructions/enroll_v0.rs | 23 +- .../initialize_vetoken_tracker_v0.rs | 6 +- .../src/instructions/mod.rs | 4 - .../src/instructions/reset_lockup_v0.rs | 106 --- .../src/instructions/reward_for_epoch_v0.rs | 76 +- .../src/instructions/track_vote_v0.rs | 26 +- .../src/instructions/transfer_v0.rs | 110 --- .../src/instructions/unenroll_v0.rs | 12 +- programs/position-voting-rewards/src/lib.rs | 11 +- programs/position-voting-rewards/src/state.rs | 7 +- programs/voter-stake-registry/src/error.rs | 2 + .../src/instructions/freeze_position_v0.rs | 23 + .../instructions/initialize_position_v0.rs | 1 + .../instructions/initialize_registrar_v0.rs | 2 + .../src/instructions/mod.rs | 4 + .../src/instructions/reset_lockup_v0.rs | 10 +- .../src/instructions/thaw_position_v0.rs | 23 + .../src/instructions/transfer_v0.rs | 17 +- .../src/instructions/update_registrar_v0.rs | 15 +- programs/voter-stake-registry/src/lib.rs | 15 +- .../src/state/position.rs | 27 +- .../src/state/registrar.rs | 1 + tests/position-voting-rewards.ts | 650 ++++++++++++++++++ tests/utils/vsr.ts | 1 + tests/voter-stake-registry.ts | 27 +- 29 files changed, 990 insertions(+), 295 deletions(-) delete mode 100644 programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs delete mode 100644 programs/position-voting-rewards/src/instructions/transfer_v0.rs create mode 100644 programs/voter-stake-registry/src/instructions/freeze_position_v0.rs create mode 100644 programs/voter-stake-registry/src/instructions/thaw_position_v0.rs create mode 100644 tests/position-voting-rewards.ts diff --git a/packages/helium-admin-cli/src/create-dao.ts b/packages/helium-admin-cli/src/create-dao.ts index dff7cb3ea..693360804 100644 --- a/packages/helium-admin-cli/src/create-dao.ts +++ b/packages/helium-admin-cli/src/create-dao.ts @@ -348,6 +348,7 @@ export async function run(args: any = process.argv) { await heliumVsrProgram.methods .initializeRegistrarV0({ positionUpdateAuthority: (await daoKey(hntKeypair.publicKey))[0], + positionFreezeAuthorities: [], }) .accounts({ realm, diff --git a/packages/helium-admin-cli/src/create-subdao.ts b/packages/helium-admin-cli/src/create-subdao.ts index ccf312f6d..e5a360d21 100644 --- a/packages/helium-admin-cli/src/create-subdao.ts +++ b/packages/helium-admin-cli/src/create-subdao.ts @@ -362,6 +362,7 @@ export async function run(args: any = process.argv) { await heliumVsrProgram.methods .initializeRegistrarV0({ positionUpdateAuthority: null, + positionFreezeAuthorities: [], }) .accounts({ realm, diff --git a/programs/position-voting-rewards/src/error.rs b/programs/position-voting-rewards/src/error.rs index 9f1d0ebdb..90b373447 100644 --- a/programs/position-voting-rewards/src/error.rs +++ b/programs/position-voting-rewards/src/error.rs @@ -15,4 +15,6 @@ pub enum ErrorCode { NoEnrollEndingPosition, #[msg("Cannot change position while enrolled")] PositionChangeWhileEnrolled, + #[msg("Marker has incorrect registrar")] + InvalidMarker, } diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs index a42505403..84d2300ba 100644 --- a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -1,7 +1,9 @@ +use std::collections::HashSet; + use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token::{transfer, Mint, Token, TokenAccount, Transfer}, + token::{burn, transfer, Burn, Mint, Token, TokenAccount, Transfer}, }; use voter_stake_registry::{ state::{PositionV0, Registrar}, @@ -47,6 +49,7 @@ pub struct ClaimRewardsV0<'info> { bump, )] pub enrolled_position: Account<'info, EnrolledPositionV0>, + #[account(mut)] pub rewards_mint: Box>, #[account( @@ -85,6 +88,16 @@ impl<'info> ClaimRewardsV0<'info> { CpiContext::new(self.token_program.to_account_info(), cpi_accounts) } + + fn burn_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> { + let cpi_accounts = Burn { + from: self.rewards_pool.to_account_info(), + authority: self.vsr_epoch_info.to_account_info(), + mint: self.rewards_mint.to_account_info(), + }; + + CpiContext::new(self.token_program.to_account_info(), cpi_accounts) + } } pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result<()> { @@ -128,14 +141,63 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result enrolled_position.set_claimed(args.epoch)?; let amount_left = ctx.accounts.rewards_pool.amount; - transfer( - ctx - .accounts - .transfer_ctx() - .with_signer(&[vsr_epoch_info_seeds!(ctx.accounts.vsr_epoch_info)]), - // Due to rounding down of vetokens fall rates it's possible the vetokens on the dao does not exactly match the - // vetokens remaining. It could be off by a little bit of dust. - std::cmp::min(rewards, amount_left), - )?; + let amount = std::cmp::min(rewards, amount_left); + let first_ts = ctx + .accounts + .vsr_epoch_info + .recent_proposals + .first() + .unwrap() + .ts; + let last_ts = ctx + .accounts + .vsr_epoch_info + .recent_proposals + .last() + .unwrap() + .ts; + ctx + .accounts + .enrolled_position + .remove_proposals_older_than(first_ts - 1); + let proposal_set = ctx + .accounts + .enrolled_position + .recent_proposals + .iter() + .filter(|p| p.ts <= last_ts) + .map(|rp| rp.proposal) + .collect::>(); + + // Check eligibility based on recent proposals + let eligible_count = ctx + .accounts + .vsr_epoch_info + .recent_proposals + .iter() + .filter(|&proposal| proposal_set.contains(&proposal.proposal)) + .count(); + if eligible_count >= 2 { + msg!("Position is eligible, transferring"); + transfer( + ctx + .accounts + .transfer_ctx() + .with_signer(&[vsr_epoch_info_seeds!(ctx.accounts.vsr_epoch_info)]), + // Due to rounding down of vetokens fall rates it's possible the vetokens on the dao does not exactly match the + // vetokens remaining. It could be off by a little bit of dust. + amount, + )?; + } else { + msg!("Position is not eligible, burning"); + burn( + ctx + .accounts + .burn_ctx() + .with_signer(&[vsr_epoch_info_seeds!(ctx.accounts.vsr_epoch_info)]), + amount, + )?; + } + Ok(()) } diff --git a/programs/position-voting-rewards/src/instructions/enroll_v0.rs b/programs/position-voting-rewards/src/instructions/enroll_v0.rs index bb50f2aa4..c82129299 100644 --- a/programs/position-voting-rewards/src/instructions/enroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/enroll_v0.rs @@ -1,6 +1,9 @@ +use std::array; + use anchor_lang::{prelude::*, Discriminator}; use anchor_spl::token::{Mint, TokenAccount}; use voter_stake_registry::{ + cpi::{accounts::FreezePositionV0, freeze_position_v0}, state::{LockupKind, PositionV0, Registrar}, VoterStakeRegistry, }; @@ -10,8 +13,9 @@ use crate::{ create_account::{create_and_serialize_account_signed, AccountMaxSize}, error::ErrorCode, id, - state::{EnrolledPositionV0, VeTokenTrackerV0, VsrEpochInfoV0}, - util::*, + state::{EnrolledPositionV0, RecentProposal, VeTokenTrackerV0, VsrEpochInfoV0}, + util::{calculate_vetoken_info, current_epoch, VetokenInfo}, + vetoken_tracker_seeds, }; #[derive(Accounts)] @@ -41,7 +45,7 @@ pub struct EnrollV0<'info> { mut, has_one = registrar, )] - pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + pub vetoken_tracker: Box>, #[account( init_if_needed, payer = payer, @@ -204,7 +208,7 @@ pub fn handler(ctx: Context) -> Result<()> { initialized: false, registrar: ctx.accounts.registrar.key(), rewards_amount: 0, - recent_proposals: [Pubkey::default(); 4], + recent_proposals: array::from_fn(|_| RecentProposal::default()), }, }, &[ @@ -259,7 +263,16 @@ pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); - ctx.accounts.vsr_epoch_info.initialized = true; + + freeze_position_v0(CpiContext::new_with_signer( + ctx.accounts.vsr_program.to_account_info(), + FreezePositionV0 { + authority: ctx.accounts.position_authority.to_account_info(), + registrar: ctx.accounts.registrar.to_account_info(), + position: ctx.accounts.position.to_account_info(), + }, + &[vetoken_tracker_seeds!(ctx.accounts.vetoken_tracker)], + ))?; Ok(()) } diff --git a/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs index e95ca8af1..927ddedf8 100644 --- a/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs +++ b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs @@ -1,8 +1,10 @@ +use std::array; + use anchor_lang::prelude::*; use anchor_spl::token::Mint; use voter_stake_registry::state::Registrar; -use crate::state::VeTokenTrackerV0; +use crate::state::{RecentProposal, VeTokenTrackerV0}; #[derive(Accounts)] pub struct InitializeVeTokenTrackerV0<'info> { @@ -35,7 +37,7 @@ pub fn handler(ctx: Context) -> Result<()> { vetoken_last_calculated_ts: ctx.accounts.registrar.clock_unix_timestamp(), vetoken_fall_rate: 0, total_vetokens: 0, - recent_proposals: [Pubkey::default(); 4], + recent_proposals: array::from_fn(|_| RecentProposal::default()), bump_seed: ctx.bumps["vetoken_tracker"], }); Ok(()) diff --git a/programs/position-voting-rewards/src/instructions/mod.rs b/programs/position-voting-rewards/src/instructions/mod.rs index c9b15cac5..42f19050d 100644 --- a/programs/position-voting-rewards/src/instructions/mod.rs +++ b/programs/position-voting-rewards/src/instructions/mod.rs @@ -1,17 +1,13 @@ pub mod claim_rewards_v0; pub mod enroll_v0; pub mod initialize_vetoken_tracker_v0; -pub mod reset_lockup_v0; pub mod reward_for_epoch_v0; pub mod track_vote_v0; -pub mod transfer_v0; pub mod unenroll_v0; pub use claim_rewards_v0::*; pub use enroll_v0::*; pub use initialize_vetoken_tracker_v0::*; -pub use reset_lockup_v0::*; pub use reward_for_epoch_v0::*; pub use track_vote_v0::*; -pub use transfer_v0::*; pub use unenroll_v0::*; diff --git a/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs b/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs deleted file mode 100644 index 170983441..000000000 --- a/programs/position-voting-rewards/src/instructions/reset_lockup_v0.rs +++ /dev/null @@ -1,106 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, TokenAccount}; -use voter_stake_registry::{ - cpi::{accounts::ResetLockupV0 as VsrResetLockupV0, reset_lockup_v0}, - state::{LockupKind as VsrLockupKind, PositionV0, Registrar}, - ResetLockupArgsV0 as VsrResetLockupArgsV0, VoterStakeRegistry, -}; - -use crate::{error::ErrorCode, state::*}; - -#[derive(Accounts)] -pub struct ResetLockupV0<'info> { - pub registrar: Box>, - #[account( - has_one = registrar, - )] - pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, - #[account( - mut, - seeds = [b"position".as_ref(), mint.key().as_ref()], - bump = position.bump_seed, - seeds::program = vsr_program.key(), - has_one = registrar, - has_one = mint - )] - pub position: Box>, - /// CHECK: This account needs to be closed. You can't reset_lockup while enrolled - #[account( - seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], - bump, - constraint = enrolled_position.data_is_empty() && enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled - )] - pub enrolled_position: UncheckedAccount<'info>, - pub mint: Box>, - #[account( - token::mint = mint, - token::authority = position_authority, - constraint = position_token_account.amount > 0 - )] - pub position_token_account: Box>, - pub position_authority: Signer<'info>, - pub vsr_program: Program<'info, VoterStakeRegistry>, -} - -#[repr(u8)] -#[derive(Default, AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Eq)] -pub enum LockupKind { - /// No lockup, tokens can be withdrawn as long as not engaged in a proposal. - #[default] - None, - - /// Lock up for a number of days - Cliff, - - /// Lock up permanently. The number of days specified becomes the minimum - /// unlock period when the deposit (or a part of it) is changed to Cliff. - Constant, -} - -impl From for VsrLockupKind { - fn from(kind: LockupKind) -> Self { - match kind { - LockupKind::None => VsrLockupKind::None, - LockupKind::Cliff => VsrLockupKind::Cliff, - LockupKind::Constant => VsrLockupKind::Constant, - } - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct ResetLockupArgsV0 { - pub kind: LockupKind, - pub periods: u32, -} - -/// Resets a lockup to start at the current slot timestamp and to last for -/// `periods`, which must be >= the number of periods left on the lockup. -/// This will re-lock any non-withdrawn vested funds. -pub fn handler(ctx: Context, args: ResetLockupArgsV0) -> Result<()> { - let ResetLockupArgsV0 { kind, periods } = args; - - reset_lockup_v0( - CpiContext::new_with_signer( - ctx.accounts.vsr_program.to_account_info(), - VsrResetLockupV0 { - registrar: ctx.accounts.registrar.to_account_info(), - position_update_authority: ctx.accounts.vetoken_tracker.to_account_info(), - position: ctx.accounts.position.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - position_token_account: ctx.accounts.position_token_account.to_account_info(), - position_authority: ctx.accounts.position_authority.to_account_info(), - }, - &[&[ - b"vetoken_tracker", - ctx.accounts.vetoken_tracker.registrar.key().as_ref(), - &[ctx.accounts.vetoken_tracker.bump_seed], - ]], - ), - VsrResetLockupArgsV0 { - kind: kind.into(), - periods, - }, - )?; - - Ok(()) -} diff --git a/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs index d830acdc1..20ba65989 100644 --- a/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs +++ b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs @@ -1,13 +1,87 @@ use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{transfer, Mint, Token, TokenAccount, Transfer}, +}; +use voter_stake_registry::state::Registrar; -use crate::state::VeTokenTrackerV0; +use crate::state::{VeTokenTrackerV0, VsrEpochInfoV0}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct RewardForEpochArgsV0 { + pub epoch: u64, pub amount: u64, } #[derive(Accounts)] +#[instruction(args: RewardForEpochArgsV0)] pub struct RewardForEpochV0<'info> { + pub rewards_authority: Signer<'info>, + pub rewards_payer: Signer<'info>, + #[account(mut)] + pub rent_payer: Signer<'info>, + #[account( + has_one = rewards_authority, + has_one = rewards_mint, + )] pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + pub registrar: Account<'info, Registrar>, + #[account( + init_if_needed, + payer = rent_payer, + space = 60 + VsrEpochInfoV0::INIT_SPACE, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), &args.epoch.to_le_bytes()], + bump, + has_one = vetoken_tracker, + has_one = registrar, + )] + pub vsr_epoch_info: Account<'info, VsrEpochInfoV0>, + pub rewards_mint: Box>, + #[account( + init_if_needed, + payer = rent_payer, + associated_token::mint = rewards_mint, + associated_token::authority = vsr_epoch_info, + )] + pub rewards_pool: Box>, + #[account( + mut, + associated_token::mint = rewards_mint, + associated_token::authority = rewards_payer, + )] + pub payer_ata: Box>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context, args: RewardForEpochArgsV0) -> Result<()> { + ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + ctx.accounts.vsr_epoch_info.epoch = args.epoch; + ctx.accounts.vsr_epoch_info.bump_seed = ctx.bumps["vsr_epoch_info"]; + ctx.accounts.vsr_epoch_info.recent_proposals = + ctx.accounts.vetoken_tracker.recent_proposals.clone(); + ctx.accounts.vsr_epoch_info.rewards_amount = args.amount; + ctx.accounts.vsr_epoch_info.rewards_issued_at = + Some(ctx.accounts.registrar.clock_unix_timestamp()); + + let curr_ts = ctx.accounts.registrar.clock_unix_timestamp(); + ctx + .accounts + .vetoken_tracker + .update_vetokens(&mut ctx.accounts.vsr_epoch_info, curr_ts)?; + + transfer( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + Transfer { + from: ctx.accounts.payer_ata.to_account_info(), + to: ctx.accounts.rewards_pool.to_account_info(), + authority: ctx.accounts.rewards_payer.to_account_info(), + }, + ), + args.amount, + )?; + + Ok(()) } diff --git a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs index f87a23f23..a4e5bbc6a 100644 --- a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs +++ b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs @@ -1,11 +1,15 @@ use anchor_lang::prelude::*; +use anchor_spl::token::Mint; use proposal::ProposalV0; use voter_stake_registry::{ state::{PositionV0, VoteMarkerV0}, VoterStakeRegistry, }; -use crate::state::{EnrolledPositionV0, VeTokenTrackerV0}; +use crate::{ + error::ErrorCode, + state::{EnrolledPositionV0, VeTokenTrackerV0}, +}; #[derive(Accounts)] pub struct TrackVoteV0<'info> { @@ -14,19 +18,22 @@ pub struct TrackVoteV0<'info> { pub proposal: Account<'info, ProposalV0>, #[account( mut, - has_one = registrar + has_one = registrar, + has_one = mint )] pub position: Box>, + pub mint: Box>, /// CHECK: Checked by seeds #[account( - seeds = [b"marker", position.mint.as_ref(), proposal.key().as_ref()], - bump , + seeds = [b"marker", mint.key().as_ref(), proposal.key().as_ref()], + bump, seeds::program = vsr_program )] - pub marker: AccountInfo<'info>, + pub marker: UncheckedAccount<'info>, #[account(mut, has_one = registrar)] - pub ve_token_tracker: Account<'info, VeTokenTrackerV0>, - pub enrolled_position: Account<'info, EnrolledPositionV0>, + pub vetoken_tracker: Box>, + #[account(mut, has_one = vetoken_tracker)] + pub enrolled_position: Box>, pub vsr_program: Program<'info, VoterStakeRegistry>, } @@ -36,6 +43,11 @@ pub fn handler(ctx: Context) -> Result<()> { let mut voted = has_data; if has_data { let marker = VoteMarkerV0::try_from_slice(&data)?; + require_eq!( + marker.registrar, + ctx.accounts.registrar.key(), + ErrorCode::InvalidMarker + ); voted = !marker.choices.is_empty(); } if voted { diff --git a/programs/position-voting-rewards/src/instructions/transfer_v0.rs b/programs/position-voting-rewards/src/instructions/transfer_v0.rs deleted file mode 100644 index d01ff4fb2..000000000 --- a/programs/position-voting-rewards/src/instructions/transfer_v0.rs +++ /dev/null @@ -1,110 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::AssociatedToken, - token::{Mint, Token, TokenAccount}, -}; -use voter_stake_registry::{ - cpi::{accounts::TransferV0 as VsrTransferV0, transfer_v0}, - state::{PositionV0, Registrar}, - TransferArgsV0 as VsrTransferArgsV0, VoterStakeRegistry, -}; - -use crate::{error::ErrorCode, state::*}; - -#[derive(Accounts)] -pub struct TransferV0<'info> { - pub registrar: Box>, - #[account( - has_one = registrar, - )] - pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, - #[account( - mut, - seeds = [b"position".as_ref(), mint.key().as_ref()], - seeds::program = vsr_program.key(), - bump = source_position.bump_seed, - has_one = registrar, - has_one = mint - )] - pub source_position: Box>, - /// CHECK: This account needs to be closed. You can't transfer while enrolled - #[account( - seeds = ["enrolled_position".as_bytes(), source_position.key().as_ref()], - bump, - constraint = source_enrolled_position.data_is_empty() && source_enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled - )] - pub source_enrolled_position: UncheckedAccount<'info>, - pub mint: Box>, - #[account( - token::mint = mint, - token::authority = position_authority, - constraint = position_token_account.amount > 0 - )] - pub position_token_account: Box>, - pub position_authority: Signer<'info>, - #[account( - mut, - has_one = registrar, - )] - pub target_position: Box>, - /// CHECK: This account needs to be closed. You can't transfer while enrolled - #[account( - seeds = ["enrolled_position".as_bytes(), target_position.key().as_ref()], - bump, - constraint = target_enrolled_position.data_is_empty() && target_enrolled_position.lamports() == 0 @ ErrorCode::PositionChangeWhileEnrolled - )] - pub target_enrolled_position: UncheckedAccount<'info>, - pub deposit_mint: Box>, - #[account( - mut, - associated_token::authority = source_position, - associated_token::mint = deposit_mint, - )] - pub source_vault: Box>, - #[account( - mut, - associated_token::authority = target_position, - associated_token::mint = deposit_mint, - )] - pub target_vault: Box>, - pub vsr_program: Program<'info, VoterStakeRegistry>, - pub token_program: Program<'info, Token>, - pub associated_token_program: Program<'info, AssociatedToken>, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] -pub struct TransferArgsV0 { - pub amount: u64, -} - -pub fn handler(ctx: Context, args: TransferArgsV0) -> Result<()> { - transfer_v0( - CpiContext::new_with_signer( - ctx.accounts.vsr_program.to_account_info(), - VsrTransferV0 { - registrar: ctx.accounts.registrar.to_account_info(), - position_update_authority: ctx.accounts.vetoken_tracker.to_account_info(), - source_position: ctx.accounts.source_position.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - position_token_account: ctx.accounts.position_token_account.to_account_info(), - position_authority: ctx.accounts.position_authority.to_account_info(), - target_position: ctx.accounts.target_position.to_account_info(), - deposit_mint: ctx.accounts.deposit_mint.to_account_info(), - source_vault: ctx.accounts.source_vault.to_account_info(), - target_vault: ctx.accounts.target_vault.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - associated_token_program: ctx.accounts.associated_token_program.to_account_info(), - }, - &[&[ - b"vetoken_tracker", - ctx.accounts.vetoken_tracker.registrar.key().as_ref(), - &[ctx.accounts.vetoken_tracker.bump_seed], - ]], - ), - VsrTransferArgsV0 { - amount: args.amount, - }, - )?; - - Ok(()) -} diff --git a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs index 7076408ce..905e1e268 100644 --- a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; use voter_stake_registry::{ + cpi::{accounts::ThawPositionV0, thaw_position_v0}, state::{LockupKind, PositionV0, Registrar}, VoterStakeRegistry, }; @@ -9,6 +10,7 @@ use crate::{ id, state::*, util::{calculate_vetoken_info, current_epoch, VetokenInfo}, + vetoken_tracker_seeds, }; #[derive(Accounts)] @@ -223,7 +225,6 @@ pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); - ctx.accounts.vsr_epoch_info.initialized = true; // EDGE CASE: When the closing time epoch infos are the same as the current epoch info, // update_subdao_vetokens will have already removed the fall rates and vetokens from the sub dao. @@ -264,5 +265,14 @@ pub fn handler(ctx: Context) -> Result<()> { .enrolled_position .close(ctx.accounts.position_authority.to_account_info())?; } + thaw_position_v0(CpiContext::new_with_signer( + ctx.accounts.vsr_program.to_account_info(), + ThawPositionV0 { + authority: ctx.accounts.position_authority.to_account_info(), + registrar: ctx.accounts.registrar.to_account_info(), + position: ctx.accounts.position.to_account_info(), + }, + &[vetoken_tracker_seeds!(ctx.accounts.vetoken_tracker)], + ))?; Ok(()) } diff --git a/programs/position-voting-rewards/src/lib.rs b/programs/position-voting-rewards/src/lib.rs index 295697466..a4733a4e7 100644 --- a/programs/position-voting-rewards/src/lib.rs +++ b/programs/position-voting-rewards/src/lib.rs @@ -44,12 +44,11 @@ pub mod position_voting_rewards { claim_rewards_v0::handler(ctx, args) } - pub fn reset_lockup_v0(ctx: Context, args: ResetLockupArgsV0) -> Result<()> { - reset_lockup_v0::handler(ctx, args) - } - - pub fn transfer_v0(ctx: Context, args: TransferArgsV0) -> Result<()> { - transfer_v0::handler(ctx, args) + pub fn reward_for_epoch_v0( + ctx: Context, + args: RewardForEpochArgsV0, + ) -> Result<()> { + reward_for_epoch_v0::handler(ctx, args) } pub fn unenroll_v0(ctx: Context) -> Result<()> { diff --git a/programs/position-voting-rewards/src/state.rs b/programs/position-voting-rewards/src/state.rs index bf2f11c92..5292f3b9c 100644 --- a/programs/position-voting-rewards/src/state.rs +++ b/programs/position-voting-rewards/src/state.rs @@ -22,7 +22,7 @@ pub struct EnrolledPositionV0 { pub recent_proposals: Vec, } -#[derive(Default, Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(InitSpace, Default, Clone, AnchorSerialize, AnchorDeserialize)] pub struct RecentProposal { pub proposal: Pubkey, pub ts: i64, @@ -94,7 +94,7 @@ pub struct VsrEpochInfoV0 { pub vetoken_tracker: Pubkey, pub registrar: Pubkey, pub initialized: bool, - pub recent_proposals: [Pubkey; 4], + pub recent_proposals: [RecentProposal; 4], pub vetokens_at_epoch_start: u128, /// The number of enrollment rewards issued this epoch, so that enrollies can claim their share of the rewards pub rewards_amount: u64, @@ -143,7 +143,7 @@ pub struct VeTokenTrackerV0 { pub vetoken_last_calculated_ts: i64, pub vetoken_fall_rate: u128, // the vetoken amount that the position decays by per second, with 12 decimals of extra precision pub total_vetokens: u128, // the total amount of vetoken staked to this subdao, with 12 decimals of extra precision - pub recent_proposals: [Pubkey; 4], + pub recent_proposals: [RecentProposal; 4], pub bump_seed: u8, } @@ -210,6 +210,7 @@ impl VeTokenTrackerV0 { msg!("Setting vetoken_at_epoch_start to {}", self.total_vetokens,); curr_epoch_info.vetokens_at_epoch_start = apply_fall_rate_factor(self.total_vetokens).unwrap(); + curr_epoch_info.initialized = true; } // Step 2. Update fall rate according to this epoch's closed position corrections if curr_epoch_info.fall_rates_from_closing_positions > 0 diff --git a/programs/voter-stake-registry/src/error.rs b/programs/voter-stake-registry/src/error.rs index 44dda1f1c..38ddea528 100644 --- a/programs/voter-stake-registry/src/error.rs +++ b/programs/voter-stake-registry/src/error.rs @@ -161,4 +161,6 @@ pub enum VsrError { SamePosition, MaxChoicesExceeded, NoVoteForThisChoice, + PositionFrozen, + UnauthorizedFreezeAuthority, } diff --git a/programs/voter-stake-registry/src/instructions/freeze_position_v0.rs b/programs/voter-stake-registry/src/instructions/freeze_position_v0.rs new file mode 100644 index 000000000..5af8cf839 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/freeze_position_v0.rs @@ -0,0 +1,23 @@ +use anchor_lang::prelude::*; + +use crate::state::{PositionV0, Registrar}; + +#[derive(Accounts)] +pub struct FreezePositionV0<'info> { + #[account( + mut, + constraint = registrar.position_freeze_authorities.contains(&authority.key()) + )] + pub authority: Signer<'info>, + pub registrar: Account<'info, Registrar>, + #[account(mut, has_one = registrar)] + pub position: Account<'info, PositionV0>, +} + +pub fn handler(ctx: Context) -> Result<()> { + ctx + .accounts + .position + .freeze(&ctx.accounts.registrar, &ctx.accounts.authority.key())?; + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs b/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs index 7d7d867a2..0afd824b0 100644 --- a/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs +++ b/programs/voter-stake-registry/src/instructions/initialize_position_v0.rs @@ -168,6 +168,7 @@ pub fn handler(ctx: Context, args: InitializePositionArgsV }; ctx.accounts.position.set_inner(PositionV0 { + freeze_bitmap: 0, registrar: ctx.accounts.registrar.key(), mint: ctx.accounts.mint.key(), bump_seed: ctx.bumps["position"], diff --git a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs index 1fc95de9e..6945d02d6 100644 --- a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs +++ b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs @@ -19,6 +19,7 @@ use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize, Default, Clone)] pub struct InitializeRegistrarArgsV0 { pub position_update_authority: Option, + pub position_freeze_authorities: Vec, } #[derive(Accounts)] @@ -193,6 +194,7 @@ pub fn handler(ctx: Context, args: InitializeRegistrarArg reserved1: [0; 4], reserved2: [0; 3], recent_proposals: [Pubkey::default(); 4], + position_freeze_authorities: args.position_freeze_authorities, voting_mints: Vec::new(), proxy_config: ctx .accounts diff --git a/programs/voter-stake-registry/src/instructions/mod.rs b/programs/voter-stake-registry/src/instructions/mod.rs index a80e35698..c32024887 100644 --- a/programs/voter-stake-registry/src/instructions/mod.rs +++ b/programs/voter-stake-registry/src/instructions/mod.rs @@ -1,6 +1,7 @@ pub use close_position_v0::*; pub use configure_voting_mint_v0::*; pub use deposit_v0::*; +pub use freeze_position_v0::*; pub use initialize_position_v0::*; pub use initialize_registrar_v0::*; pub use ledger_transfer_position_v0::*; @@ -10,6 +11,7 @@ pub use relinquish_expired_vote_v0::*; pub use relinquish_vote_v1::*; pub use reset_lockup_v0::*; pub use set_time_offset_v0::*; +pub use thaw_position_v0::*; pub use transfer_v0::*; pub use update_registrar_authority_v0::*; pub use update_registrar_v0::*; @@ -19,6 +21,7 @@ pub use withdraw_v0::*; pub mod close_position_v0; pub mod configure_voting_mint_v0; pub mod deposit_v0; +pub mod freeze_position_v0; pub mod initialize_position_v0; pub mod initialize_registrar_v0; pub mod ledger_transfer_position_v0; @@ -28,6 +31,7 @@ pub mod relinquish_expired_vote_v0; pub mod relinquish_vote_v1; pub mod reset_lockup_v0; pub mod set_time_offset_v0; +pub mod thaw_position_v0; pub mod transfer_v0; pub mod update_registrar_authority_v0; pub mod update_registrar_v0; diff --git a/programs/voter-stake-registry/src/instructions/reset_lockup_v0.rs b/programs/voter-stake-registry/src/instructions/reset_lockup_v0.rs index 662bceeac..4ac8e7b05 100644 --- a/programs/voter-stake-registry/src/instructions/reset_lockup_v0.rs +++ b/programs/voter-stake-registry/src/instructions/reset_lockup_v0.rs @@ -1,8 +1,7 @@ -use crate::error::*; -use crate::state::*; use anchor_lang::prelude::*; -use anchor_spl::token::Mint; -use anchor_spl::token::TokenAccount; +use anchor_spl::token::{Mint, TokenAccount}; + +use crate::{error::*, state::*}; #[derive(Accounts)] pub struct ResetLockupV0<'info> { @@ -19,7 +18,8 @@ pub struct ResetLockupV0<'info> { seeds = [b"position".as_ref(), mint.key().as_ref()], bump = position.bump_seed, has_one = registrar, - has_one = mint + has_one = mint, + constraint = !position.is_frozen() @ VsrError::PositionFrozen )] pub position: Box>, pub mint: Box>, diff --git a/programs/voter-stake-registry/src/instructions/thaw_position_v0.rs b/programs/voter-stake-registry/src/instructions/thaw_position_v0.rs new file mode 100644 index 000000000..174d61bb8 --- /dev/null +++ b/programs/voter-stake-registry/src/instructions/thaw_position_v0.rs @@ -0,0 +1,23 @@ +use anchor_lang::prelude::*; + +use crate::state::{PositionV0, Registrar}; + +#[derive(Accounts)] +pub struct ThawPositionV0<'info> { + #[account( + mut, + constraint = registrar.position_freeze_authorities.contains(&authority.key()) + )] + pub authority: Signer<'info>, + pub registrar: Account<'info, Registrar>, + #[account(mut, has_one = registrar)] + pub position: Account<'info, PositionV0>, +} + +pub fn handler(ctx: Context) -> Result<()> { + ctx + .accounts + .position + .thaw(&ctx.accounts.registrar, &ctx.accounts.authority.key())?; + Ok(()) +} diff --git a/programs/voter-stake-registry/src/instructions/transfer_v0.rs b/programs/voter-stake-registry/src/instructions/transfer_v0.rs index 517fb73f9..bb6d261ee 100644 --- a/programs/voter-stake-registry/src/instructions/transfer_v0.rs +++ b/programs/voter-stake-registry/src/instructions/transfer_v0.rs @@ -1,12 +1,11 @@ -use crate::error::*; -use crate::position_seeds; -use crate::state::*; use anchor_lang::prelude::*; -use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token; -use anchor_spl::token::Mint; -use anchor_spl::token::Token; -use anchor_spl::token::TokenAccount; +use anchor_spl::{ + associated_token::AssociatedToken, + token, + token::{Mint, Token, TokenAccount}, +}; + +use crate::{error::*, position_seeds, state::*}; #[derive(Accounts)] pub struct TransferV0<'info> { @@ -26,6 +25,7 @@ pub struct TransferV0<'info> { has_one = registrar, has_one = mint, constraint = source_position.key() != target_position.key() @ VsrError::SamePosition, + constraint = !source_position.is_frozen() @ VsrError::PositionFrozen, )] pub source_position: Box>, pub mint: Box>, @@ -40,6 +40,7 @@ pub struct TransferV0<'info> { mut, has_one = registrar, constraint = target_position.num_active_votes == 0 @ VsrError::ActiveVotesExist, + constraint = !target_position.is_frozen() @ VsrError::PositionFrozen, )] pub target_position: Box>, pub deposit_mint: Box>, diff --git a/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs b/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs index bce75c205..8f953ab70 100644 --- a/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs +++ b/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs @@ -1,7 +1,13 @@ -use crate::state::*; use anchor_lang::prelude::*; use nft_proxy::ProxyConfigV0; +use crate::state::*; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct UpdateRegistrarArgsV0 { + pub position_freeze_authorities: Option>, +} + #[derive(Accounts)] #[instruction()] pub struct UpdateRegistrarV0<'info> { @@ -15,12 +21,17 @@ pub struct UpdateRegistrarV0<'info> { pub proxy_config: Option>, } -pub fn handler(ctx: Context) -> Result<()> { +pub fn handler(ctx: Context, args: UpdateRegistrarArgsV0) -> Result<()> { ctx.accounts.registrar.proxy_config = ctx .accounts .proxy_config .clone() .map(|k| k.key()) .unwrap_or_default(); + + if let Some(authorities) = args.position_freeze_authorities { + ctx.accounts.registrar.position_freeze_authorities = authorities; + } + Ok(()) } diff --git a/programs/voter-stake-registry/src/lib.rs b/programs/voter-stake-registry/src/lib.rs index 237e778bd..76101b678 100644 --- a/programs/voter-stake-registry/src/lib.rs +++ b/programs/voter-stake-registry/src/lib.rs @@ -125,7 +125,18 @@ pub mod voter_stake_registry { instructions::proxied_vote_v0::handler(ctx, args) } - pub fn update_registrar_v0(ctx: Context) -> Result<()> { - instructions::update_registrar_v0::handler(ctx) + pub fn update_registrar_v0( + ctx: Context, + args: UpdateRegistrarArgsV0, + ) -> Result<()> { + instructions::update_registrar_v0::handler(ctx, args) + } + + pub fn freeze_position_v0(ctx: Context) -> Result<()> { + instructions::freeze_position_v0::handler(ctx) + } + + pub fn thaw_position_v0(ctx: Context) -> Result<()> { + instructions::thaw_position_v0::handler(ctx) } } diff --git a/programs/voter-stake-registry/src/state/position.rs b/programs/voter-stake-registry/src/state/position.rs index 7dea634c6..29a3f5559 100644 --- a/programs/voter-stake-registry/src/state/position.rs +++ b/programs/voter-stake-registry/src/state/position.rs @@ -2,7 +2,7 @@ use std::cmp::min; use anchor_lang::prelude::*; -use super::{Lockup, LockupKind, VotingMintConfigV0}; +use super::{registrar, Lockup, LockupKind, Registrar, VotingMintConfigV0}; use crate::error::*; pub const PRECISION_FACTOR: u128 = 1_000_000_000_000; @@ -29,9 +29,34 @@ pub struct PositionV0 { pub genesis_end: i64, pub bump_seed: u8, pub vote_controller: Pubkey, + pub freeze_bitmap: u8, } impl PositionV0 { + pub fn is_frozen(&self) -> bool { + self.freeze_bitmap > 0 + } + + pub fn freeze(&mut self, registrar: &Registrar, authority: &Pubkey) -> Result<()> { + let idx = registrar + .position_freeze_authorities + .iter() + .position(|x| x == authority) + .ok_or(VsrError::UnauthorizedFreezeAuthority)?; + self.freeze_bitmap |= 1 << idx; + Ok(()) + } + + pub fn thaw(&mut self, registrar: &Registrar, authority: &Pubkey) -> Result<()> { + let idx = registrar + .position_freeze_authorities + .iter() + .position(|x| x == authority) + .ok_or(VsrError::UnauthorizedFreezeAuthority)?; + self.freeze_bitmap &= !(1 << idx); + Ok(()) + } + // # Voting Power Caclulation // // Returns the voting power for the position, giving locked tokens boosted diff --git a/programs/voter-stake-registry/src/state/registrar.rs b/programs/voter-stake-registry/src/state/registrar.rs index 451449207..305f361b5 100644 --- a/programs/voter-stake-registry/src/state/registrar.rs +++ b/programs/voter-stake-registry/src/state/registrar.rs @@ -25,6 +25,7 @@ pub struct Registrar { pub proxy_config: Pubkey, pub voting_mints: Vec, pub recent_proposals: [Pubkey; 4], + pub position_freeze_authorities: Vec, } impl Registrar { diff --git a/tests/position-voting-rewards.ts b/tests/position-voting-rewards.ts new file mode 100644 index 000000000..b459ca676 --- /dev/null +++ b/tests/position-voting-rewards.ts @@ -0,0 +1,650 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { init, positionVotingRewardsResolvers } from "@helium/position-voting-rewards-sdk"; +import { PositionVotingRewards } from "@helium/idls/lib/types/position_voting_rewards"; +import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { init as vsrInit } from "../packages/voter-stake-registry-sdk/src"; +import { ensureVSRIdl } from "./utils/fixtures"; +import { PublicKey } from "@solana/web3.js"; +import { initVsr } from "./utils/vsr"; +import { createMint } from "../packages/spl-utils/src"; +import { daoKey } from "../packages/helium-sub-daos-sdk/src"; + +describe("position-voting-rewards", () => { + // Configure the client to use the local cluster. + anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); + + const program = new Program( + anchor.workspace.PositionVotingRewards.idl, + anchor.workspace.PositionVotingRewards.programId, + anchor.workspace.PositionVotingRewards.provider, + anchor.workspace.PositionVotingRewards.coder, + () => { + return positionVotingRewardsResolvers; + } + ); + + let vsrProgram: Program; + let registrar: PublicKey; + let genesisVotePowerMultiplierExpirationTs = 1; + let hntMint: PublicKey; + + const provider = anchor.getProvider() as anchor.AnchorProvider; + const me = provider.wallet.publicKey; + + before(async () => { + hntMint = await createMint(provider, 8, me, me); + vsrProgram = await vsrInit( + provider, + anchor.workspace.VoterStakeRegistry.programId, + anchor.workspace.VoterStakeRegistry.idl + ); + ensureVSRIdl(vsrProgram); + + ({ registrar } = await initVsr( + vsrProgram, + provider, + me, + hntMint, + daoKey(hntMint)[0], + genesisVotePowerMultiplierExpirationTs, + 3 + )); + }) + + const vehntOptions = [ + { + name: "Case 1", + options: { + delay: 1000, + lockupPeriods: 365, + lockupAmount: 100, + expectedMultiplier: + Math.min((SECS_PER_DAY * 365) / MAX_LOCKUP, 1) * SCALE, + }, + }, + { + name: "Case 2", + options: { + delay: 15000, + lockupPeriods: 183 * 4, + lockupAmount: 50, + expectedMultiplier: + Math.min((SECS_PER_DAY * 183 * 4) / MAX_LOCKUP, 1) * SCALE, + }, + }, + { + name: "Case 3", + options: { + delay: 0, + lockupPeriods: 365 * 4, + lockupAmount: 50, + expectedMultiplier: + Math.min((SECS_PER_DAY * 365 * 4) / MAX_LOCKUP, 1) * SCALE, + }, + }, + { + name: "Case 4 (Cliff 100 4 years)", + options: { + delay: 15000, + lockupPeriods: 365 * 4, + lockupAmount: 100, + kind: { cliff: {} }, + expectedMultiplier: + Math.min((SECS_PER_DAY * 365 * 4) / MAX_LOCKUP, 1) * SCALE, + }, + }, + { + name: "Case 5 (Constant 100 4 years)", + options: { + delay: 0, + lockupPeriods: 365 * 4, + lockupAmount: 100, + kind: { constant: {} }, + expectedMultiplier: + Math.min((SECS_PER_DAY * 365 * 4) / MAX_LOCKUP, 1) * SCALE, + }, + }, + ]; + + vehntOptions.forEach(function ({ name, options }) { + describe("vehnt tests - " + name, () => { + before(() => { + genesisVotePowerMultiplierExpirationTs = 1; + }); + + beforeEach(async () => { + ({ position, vault } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + options, + positionAuthorityKp + )); + }); + + it("allows vehnt delegation", async () => { + const lockupAmount = toBN(options.lockupAmount, 8); + const method = program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]); + const { delegatedPosition } = await method.pubkeys(); + await method.rpc({ skipPreflight: true }); + + const acc = await program.account.delegatedPositionV0.fetch( + delegatedPosition! + ); + const sdAcc = await program.account.subDaoV0.fetch(subDao); + const positionAcc = await vsrProgram.account.positionV0.fetch(position); + const endTs = positionAcc.lockup.endTs.toNumber(); + const startTs = positionAcc.lockup.startTs.toNumber(); + const multiplier = + typeof positionAcc.lockup.kind.cliff === "undefined" + ? 1 + : (endTs - sdAcc.vehntLastCalculatedTs.toNumber()) / + (endTs - startTs); + + const expectedVeHnt = + options.lockupAmount * options.expectedMultiplier * multiplier; + + expectBnAccuracy( + toBN(expectedVeHnt, 8).mul(new BN("1000000000000")), + sdAcc.vehntDelegated, + typeof options.kind?.constant !== "undefined" ? 0 : 0.00000000001 + ); + expectBnAccuracy(lockupAmount, acc.hntAmount, 0.01); + }); + + it("calculates subdao rewards", async () => { + // Onboard one hotspot to add to the utility score + const { rewardableEntityConfig } = await initTestRewardableEntityConfig( + hemProgram, + subDao + ); + const { maker, collection, makerKeypair, merkle } = await initTestMaker( + hemProgram, + provider, + rewardableEntityConfig, + dao + ); + const eccVerifier = loadKeypair( + __dirname + "/keypairs/verifier-test.json" + ); + const ecc = (await HeliumKeypair.makeRandom()).address.b58; + const hotspotOwner = Keypair.generate(); + + const { getAssetFn, getAssetProofFn, hotspot } = + await createMockCompression({ + collection, + dao, + merkle, + ecc, + hotspotOwner, + }); + console.log("I AM ISSUING"); + const issueMethod = hemProgram.methods + .issueEntityV0({ + entityKey: Buffer.from(bs58.decode(ecc)), + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 500000 }), + ]) + .accounts({ + maker, + recipient: hotspotOwner.publicKey, + issuingAuthority: makerKeypair.publicKey, + dao, + eccVerifier: eccVerifier.publicKey, + }) + .signers([makerKeypair, eccVerifier]); + + await issueMethod.rpc({ skipPreflight: true }); + await dcProgram.methods + .mintDataCreditsV0({ + // $50 onboard, $10 location assert + dcAmount: toBN(60, 5), + hntAmount: null, + }) + .accounts({ dcMint }) + .rpc({ skipPreflight: true }); + + const method = ( + await onboardIotHotspot({ + program: hemProgram, + assetId: hotspot, + maker, + dao, + rewardableEntityConfig, + location: new BN(1000), + getAssetFn, + getAssetProofFn, + dcFeePayer: me, + }) + ).signers([makerKeypair, hotspotOwner]); + + const { + pubkeys: { iotInfo: infoKey }, + } = await method.rpcAndKeys({ skipPreflight: true }); + + await hemProgram.methods + .setEntityActiveV0({ + isActive: true, + entityKey: Buffer.from(bs58.decode(ecc)), + }) + .accounts({ + activeDeviceAuthority: me, + rewardableEntityConfig, + info: infoKey! as PublicKey, + }) + .rpc({ skipPreflight: true }); + + const { subDaoEpochInfo } = await burnDc(1600000); + const epoch = ( + await program.account.subDaoEpochInfoV0.fetch(subDaoEpochInfo) + ).epoch; + + // delegate some vehnt + await program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc({ skipPreflight: true }); + + const instr = program.methods + .calculateUtilityScoreV0({ + epoch, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), + ]) + .accounts({ + subDao, + dao, + }); + + const pubkeys = await instr.pubkeys(); + await instr.rpc({ + skipPreflight: true, + commitment: "confirmed", + }); + + const subDaoInfo = await program.account.subDaoEpochInfoV0.fetch( + subDaoEpochInfo + ); + const daoInfo = await program.account.daoEpochInfoV0.fetch( + pubkeys.daoEpochInfo! + ); + + expect(daoInfo.numUtilityScoresCalculated).to.eq(1); + + const supply = (await getMint(provider.connection, hntMint)).supply; + const veHnt = toNumber(subDaoInfo.vehntAtEpochStart, 8); + const totalUtility = + Math.max(veHnt, 1) * Math.pow(50, 1 / 4) * Math.sqrt(16) * 1; + expect(daoInfo.totalRewards.toString()).to.eq(EPOCH_REWARDS.toString()); + expect(daoInfo.currentHntSupply.toString()).to.eq( + new BN(supply.toString()).add(new BN(EPOCH_REWARDS)).toString() + ); + + expectBnAccuracy( + toBN(totalUtility, 12), + daoInfo.totalUtilityScore, + 0.00000002 + ); + expectBnAccuracy( + toBN(totalUtility, 12), + subDaoInfo.utilityScore!, + 0.00000002 + ); + }); + + it("allows transfers", async () => { + const { position: newPos } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + options, + positionAuthorityKp + ); + await program.methods + .transferV0({ amount: toBN(10, 8) }) + .accounts({ + sourcePosition: position, + targetPosition: newPos, + depositMint: hntMint, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc(); + }); + + it("allows lockup resets", async () => { + await program.methods + .resetLockupV0({ + kind: { constant: {} }, + periods: 365 * 4, + }) + .accounts({ + dao, + position: position, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc(); + }); + + describe("with delegated vehnt", () => { + beforeEach(async () => { + await program.methods + .delegateV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc({ skipPreflight: true }); + }); + + it("does not allow transfers", async () => { + const { position: newPos } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + options, + positionAuthorityKp + ); + await expect( + program.methods + .transferV0({ amount: toBN(10, 8) }) + .accounts({ + sourcePosition: position, + targetPosition: newPos, + depositMint: hntMint, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc() + ).to.eventually.be.rejectedWith( + "AnchorError caused by account: source_delegated_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is delegated." + ); + }); + + it("does not allow lockup resets", async () => { + await expect( + program.methods + .resetLockupV0({ + kind: { constant: {} }, + periods: 182 * 4, + }) + .accounts({ + dao, + position: position, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc() + ).to.eventually.be.rejectedWith( + "AnchorError caused by account: delegated_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is delegated." + ); + }); + + it("allows closing delegate", async () => { + await sleep(options.delay); + const method = program.methods + .closeDelegationV0() + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]); + + const { delegatedPosition, subDaoEpochInfo } = await method.pubkeys(); + await method.rpc({ skipPreflight: true }); + + const sdAcc = await program.account.subDaoV0.fetch(subDao); + + expect(sdAcc.vehntFallRate.toNumber()).to.eq(0); + // Extremely precise u128 can be off by dust. + expect(sdAcc.vehntDelegated.toNumber()).to.be.closeTo(0, 15); + + assert.isFalse( + !!(await provider.connection.getAccountInfo(delegatedPosition!)) + ); + }); + + describe("with multiple delegated vehnt", () => { + let basePosition: PublicKey; + let basePositionOptions = { + lockupPeriods: 365 * 1, + lockupAmount: 1000000, + kind: { cliff: {} }, + expectedMultiplier: + Math.min((SECS_PER_DAY * 365 * 1) / MAX_LOCKUP, 1) * SCALE, + }; + + beforeEach(async () => { + ({ position: basePosition } = await createPosition( + vsrProgram, + provider, + registrar, + hntMint, + basePositionOptions, + positionAuthorityKp + )); + + await program.methods + .delegateV0() + .accounts({ + position: basePosition, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpc({ skipPreflight: true }); + }); + + it("delegates proper vehnt amount", async () => { + const sdAcc = await program.account.subDaoV0.fetch(subDao); + + const expectedVehnt = + options.lockupAmount * options.expectedMultiplier + + basePositionOptions.lockupAmount * + basePositionOptions.expectedMultiplier; + + expectBnAccuracy( + toBN(expectedVehnt, 8).mul(new BN("1000000000000")), + sdAcc.vehntDelegated, + 0.0000001 + ); + }); + + it("allows closing delegate", async () => { + const method = program.methods + .closeDelegationV0() + .accounts({ + position: basePosition, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]); + + const { delegatedPosition, subDaoEpochInfo } = + await method.pubkeys(); + // const sed = await program.account.subDaoEpochInfoV0.fetch( + // subDaoEpochInfo! + // ); + // console.log(sed.fallRatesFromClosingPositions.toString()); + // console.log(sed.vehntInClosingPositions.toString()); + await method.rpc({ skipPreflight: true }); + + const sdAcc = await program.account.subDaoV0.fetch(subDao); + const positionAcc = await vsrProgram.account.positionV0.fetch( + position + ); + const endTs = positionAcc.lockup.endTs.toNumber(); + const startTs = positionAcc.lockup.startTs.toNumber(); + const multiplier = + typeof positionAcc.lockup.kind.cliff === "undefined" + ? 1 + : (endTs - sdAcc.vehntLastCalculatedTs.toNumber()) / + (endTs - startTs); + + const expectedVehnt = + options.lockupAmount * options.expectedMultiplier * multiplier; + + expectBnAccuracy( + toBN(expectedVehnt, 8).mul(new BN("1000000000000")), + sdAcc.vehntDelegated, + 0.0000000001 + ); + + expect(sdAcc.vehntFallRate.toNumber()).to.be.closeTo( + typeof positionAcc.lockup.kind.cliff !== "undefined" + ? ((options.lockupAmount * options.expectedMultiplier) / + (endTs - startTs)) * + 100000000000000000000 + : 0, + 1 + ); + + assert.isFalse( + !!(await provider.connection.getAccountInfo(delegatedPosition!)) + ); + }); + }); + + describe("with calculated rewards", () => { + let epoch: anchor.BN; + let subDaoEpochInfo: PublicKey; + + beforeEach(async () => { + await vsrProgram.methods + .setTimeOffsetV0(new BN(1 * 60 * 60 * 24)) + .accounts({ registrar }) + .rpc({ skipPreflight: true }); + + ({ subDaoEpochInfo } = await burnDc(1600000)); + epoch = ( + await program.account.subDaoEpochInfoV0.fetch(subDaoEpochInfo) + ).epoch; + + await program.methods + .calculateUtilityScoreV0({ + epoch, + }) + .preInstructions([ + ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), + ]) + .accounts({ + subDao, + dao, + }) + .rpc({ skipPreflight: true }); + }); + + it("issues hnt rewards to subdaos, dnt to rewards escrow, and hst to hst pool", async () => { + const preBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(treasury))?.data! + ).amount; + const preHstBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(hstPool))?.data! + ).amount; + const preMobileBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(rewardsEscrow))?.data! + ).amount; + await program.methods + .issueRewardsV0({ + epoch, + }) + .accounts({ + subDao, + }) + .rpc({ skipPreflight: true }); + + await program.methods + .issueHstPoolV0({ + epoch, + }) + .accounts({ + dao, + }) + .rpc({ skipPreflight: true }); + + const postBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(treasury))?.data! + ).amount; + const postMobileBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(rewardsEscrow))?.data! + ).amount; + const postHstBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(hstPool))?.data! + ).amount; + expect((postBalance - preBalance).toString()).to.eq( + ((1 - 0.32) * EPOCH_REWARDS).toString() + ); + expect((postHstBalance - preHstBalance).toString()).to.eq( + (0.32 * EPOCH_REWARDS).toString() + ); + expect((postMobileBalance - preMobileBalance).toString()).to.eq( + ((SUB_DAO_EPOCH_REWARDS / 100) * 94).toString() + ); + + const acc = await program.account.subDaoEpochInfoV0.fetch( + subDaoEpochInfo + ); + expect(Boolean(acc.rewardsIssuedAt)).to.be.true; + }); + + it("claim rewards", async () => { + // issue rewards + await sendInstructions(provider, [ + await program.methods + .issueRewardsV0({ + epoch, + }) + .accounts({ + subDao, + }) + .instruction(), + ]); + + const method = program.methods + .claimRewardsV0({ + epoch, + }) + .accounts({ + position, + subDao, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]); + const { delegatorAta } = await method.pubkeys(); + await method.rpc({ skipPreflight: true }); + + const postAtaBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(delegatorAta!))?.data! + ).amount; + expect(Number(postAtaBalance)).to.be.within( + (SUB_DAO_EPOCH_REWARDS * 6) / 100 - 5, + (SUB_DAO_EPOCH_REWARDS * 6) / 100 + ); + }); + }); + }); + }); + }); +}) \ No newline at end of file diff --git a/tests/utils/vsr.ts b/tests/utils/vsr.ts index 5b5651c46..ccaa99d51 100644 --- a/tests/utils/vsr.ts +++ b/tests/utils/vsr.ts @@ -57,6 +57,7 @@ export async function initVsr( const createRegistrar = program.methods .initializeRegistrarV0({ positionUpdateAuthority, + positionFreezeAuthorities: [] }) .accounts({ realm: realmPk, diff --git a/tests/voter-stake-registry.ts b/tests/voter-stake-registry.ts index f5c263bab..7afc568b9 100644 --- a/tests/voter-stake-registry.ts +++ b/tests/voter-stake-registry.ts @@ -107,24 +107,6 @@ describe("voter-stake-registry", () => { new anchor.BN(1) ); - await withSetRealmConfig( - instructions, - SPL_GOVERNANCE_PID, - programVersion, - realm, - me, - undefined, - MintMaxVoteWeightSource.FULL_SUPPLY_FRACTION, - new anchor.BN(1), - new GoverningTokenConfigAccountArgs({ - voterWeightAddin: program.programId, - maxVoterWeightAddin: undefined, - tokenType: GoverningTokenType.Liquid, - }), - undefined, - me - ); - ({ pubkeys: { proxyConfig }, } = await proxyProgram.methods @@ -141,7 +123,7 @@ describe("voter-stake-registry", () => { .accounts({ authority: me, }) - .rpcAndKeys()); + .rpcAndKeys({ skipPreflight: true })); const { instruction: createRegistrar, @@ -149,6 +131,7 @@ describe("voter-stake-registry", () => { } = await program.methods .initializeRegistrarV0({ positionUpdateAuthority: null, + positionFreezeAuthorities: [], }) .accounts({ realm: realm, @@ -306,7 +289,7 @@ describe("voter-stake-registry", () => { onVoteHook: PublicKey.default, authority: me, }) - .rpcAndKeys(); + .rpcAndKeys({ skipPreflight: true }); proposalConfig = proposalConfigK as PublicKey; const { pubkeys: { proposal: proposalK }, @@ -329,7 +312,7 @@ describe("voter-stake-registry", () => { tags: ["test", "tags"], }) .accounts({ proposalConfig }) - .rpcAndKeys(); + .rpcAndKeys({ skipPreflight: true }); proposal = proposalK as PublicKey; await proposalProgram.methods @@ -750,7 +733,7 @@ describe("voter-stake-registry", () => { targetPosition: newPos, depositMint: hntMint, }) - .rpc() + .rpc({ skipPreflight: true }) ).to.eventually.be.rejectedWith( "AnchorError caused by account: source_position. Error Code: ActiveVotesExist. Error Number: 6055. Error Message: Cannot change a position while active votes exist." ); From 9fd3be56ae65c71f3e52ebf0630aa88c16c4a9c6 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Thu, 10 Oct 2024 18:45:27 -0700 Subject: [PATCH 04/17] Tests halfway fixed --- .../helium-admin-cli/src/set-proxy-config.ts | 5 +- .../src/resolvers.ts | 20 +- programs/position-voting-rewards/src/error.rs | 1 + .../src/instructions/enroll_v0.rs | 5 +- .../src/instructions/unenroll_v0.rs | 1 + programs/position-voting-rewards/src/util.rs | 115 ++++- .../src/instructions/update_registrar_v0.rs | 8 +- tests/position-voting-rewards.ts | 403 +++++------------- 8 files changed, 242 insertions(+), 316 deletions(-) diff --git a/packages/helium-admin-cli/src/set-proxy-config.ts b/packages/helium-admin-cli/src/set-proxy-config.ts index 9bc3ed15a..df495bbee 100644 --- a/packages/helium-admin-cli/src/set-proxy-config.ts +++ b/packages/helium-admin-cli/src/set-proxy-config.ts @@ -92,7 +92,10 @@ export async function run(args: any = process.argv) { provider, instructions: [ await vsrProgram.methods - .updateRegistrarV0() + .updateRegistrarV0({ + positionFreezeAuthorities: [], + positionUpdateAuthority: null, + }) .accounts({ proxyConfig, registrar, diff --git a/packages/position-voting-rewards-sdk/src/resolvers.ts b/packages/position-voting-rewards-sdk/src/resolvers.ts index cbf56accb..4ef889490 100644 --- a/packages/position-voting-rewards-sdk/src/resolvers.ts +++ b/packages/position-voting-rewards-sdk/src/resolvers.ts @@ -34,12 +34,12 @@ export const vsrEpochInfoResolver = resolveIndividual( const unixTime = Number(clock!.data.readBigInt64LE(8 * 4)) + (registrar?.timeOffset.toNumber() || 0); - const vsrTracker = get(accounts, [ + const vetokenTracker = get(accounts, [ ...path.slice(0, path.length - 1), - "vsr_tracker", + "vetokenTracker", ]) as PublicKey; - if (vsrTracker) { - const [key] = await vsrEpochInfoKey(vsrTracker, unixTime, PROGRAM_ID); + if (vetokenTracker) { + const [key] = await vsrEpochInfoKey(vetokenTracker, unixTime, PROGRAM_ID); return key; } @@ -52,9 +52,9 @@ export const closingTimeEpochInfoResolver = resolveIndividual( if (path[path.length - 1] === "closingTimeVsrEpochInfo") { const program = await init(provider as AnchorProvider, VSR_PROGRAM_ID); - const vsrTracker = get(accounts, [ + const vetokenTracker = get(accounts, [ ...path.slice(0, path.length - 1), - "vsr_tracker", + "vetokenTracker", ]) as PublicKey; const position = get(accounts, [ ...path.slice(0, path.length - 1), @@ -64,7 +64,7 @@ export const closingTimeEpochInfoResolver = resolveIndividual( position && (await program.account.positionV0.fetch(position)); if (positionAcc) { const [key] = await vsrEpochInfoKey( - vsrTracker, + vetokenTracker, positionAcc.lockup.endTs ); @@ -85,9 +85,9 @@ export const genesisEndEpochInfoResolver = resolveIndividual( if (path[path.length - 1] === "genesisEndVsrEpochInfo") { const program = await init(provider as AnchorProvider, VSR_PROGRAM_ID); - const vsrTracker = get(accounts, [ + const vetokenTracker = get(accounts, [ ...path.slice(0, path.length - 1), - "vsr_tracker", + "vetokenTracker", ]) as PublicKey; const position = get(accounts, [ ...path.slice(0, path.length - 1), @@ -109,7 +109,7 @@ export const genesisEndEpochInfoResolver = resolveIndividual( positionAcc.genesisEnd.toNumber() < currTs ? positionAcc.lockup.endTs.toNumber() : positionAcc.genesisEnd; - const [key] = await vsrEpochInfoKey(vsrTracker, ts); + const [key] = await vsrEpochInfoKey(vetokenTracker, ts); return key; } diff --git a/programs/position-voting-rewards/src/error.rs b/programs/position-voting-rewards/src/error.rs index 90b373447..080873848 100644 --- a/programs/position-voting-rewards/src/error.rs +++ b/programs/position-voting-rewards/src/error.rs @@ -17,4 +17,5 @@ pub enum ErrorCode { PositionChangeWhileEnrolled, #[msg("Marker has incorrect registrar")] InvalidMarker, + ArithmeticError, } diff --git a/programs/position-voting-rewards/src/instructions/enroll_v0.rs b/programs/position-voting-rewards/src/instructions/enroll_v0.rs index c82129299..4c204a8b0 100644 --- a/programs/position-voting-rewards/src/instructions/enroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/enroll_v0.rs @@ -23,6 +23,7 @@ pub struct EnrollV0<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( + mut, seeds = [b"position".as_ref(), mint.key().as_ref()], seeds::program = vsr_program.key(), bump = position.bump_seed, @@ -86,7 +87,7 @@ pub struct EnrollV0<'info> { #[account( init, space = 60 + 8 + std::mem::size_of::(), - payer = position_authority, + payer = payer, seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], bump, )] @@ -267,7 +268,7 @@ pub fn handler(ctx: Context) -> Result<()> { freeze_position_v0(CpiContext::new_with_signer( ctx.accounts.vsr_program.to_account_info(), FreezePositionV0 { - authority: ctx.accounts.position_authority.to_account_info(), + authority: ctx.accounts.vetoken_tracker.to_account_info(), registrar: ctx.accounts.registrar.to_account_info(), position: ctx.accounts.position.to_account_info(), }, diff --git a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs index 905e1e268..8414119ed 100644 --- a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs @@ -18,6 +18,7 @@ pub struct UnenrollV0<'info> { #[account(mut)] pub payer: Signer<'info>, #[account( + mut, seeds = [b"position".as_ref(), mint.key().as_ref()], seeds::program = vsr_program.key(), bump = position.bump_seed, diff --git a/programs/position-voting-rewards/src/util.rs b/programs/position-voting-rewards/src/util.rs index 33197b295..f709a02b9 100644 --- a/programs/position-voting-rewards/src/util.rs +++ b/programs/position-voting-rewards/src/util.rs @@ -1,8 +1,13 @@ -use std::{cmp::Ordering, convert::TryInto}; +use std::{ + cmp::{min, Ordering}, + convert::TryInto, +}; use anchor_lang::prelude::*; use voter_stake_registry::state::{LockupKind, PositionV0, VotingMintConfigV0}; +use crate::error::ErrorCode; + pub const EPOCH_LENGTH: i64 = 24 * 60 * 60; pub fn current_epoch(unix_timestamp: i64) -> u64 { @@ -41,7 +46,7 @@ pub fn calculate_vetoken_info( position: &PositionV0, voting_mint_config: &VotingMintConfigV0, ) -> Result { - let vetokens_at_curr_ts = position.voting_power(voting_mint_config, curr_ts)?; + let vetokens_at_curr_ts = position.voting_power_precise(voting_mint_config, curr_ts)?; let has_genesis = position.genesis_end > curr_ts; let seconds_to_genesis = if has_genesis { @@ -72,19 +77,19 @@ pub fn calculate_vetoken_info( position.lockup.seconds_left(curr_ts) }; // One second before genesis end, the last moment we have the multiplier - let vetokens_at_genesis_end = position.voting_power( + let vetokens_at_genesis_end = position.voting_power_precise( voting_mint_config, curr_ts .checked_add(i64::try_from(seconds_to_genesis).unwrap()) .unwrap(), )?; let vetokens_at_genesis_end_exact = if has_genesis { - position.voting_power(voting_mint_config, position.genesis_end)? + position.voting_power_precise(voting_mint_config, position.genesis_end)? } else { - position.voting_power(voting_mint_config, curr_ts)? + position.voting_power_precise(voting_mint_config, curr_ts)? }; let vetokens_at_position_end = - position.voting_power(voting_mint_config, position.lockup.end_ts)?; + position.voting_power_precise(voting_mint_config, position.lockup.end_ts)?; let pre_genesis_end_fall_rate = calculate_fall_rate( vetokens_at_curr_ts, @@ -175,6 +180,104 @@ pub fn calculate_vetoken_info( }) } +pub trait PrecisePosition { + fn voting_power_precise( + &self, + voting_mint_config: &VotingMintConfigV0, + curr_ts: i64, + ) -> Result; + fn voting_power_precise_locked_precise( + &self, + curr_ts: i64, + max_locked_vote_weight: u128, + lockup_saturation_secs: u64, + ) -> Result; + + fn voting_power_precise_cliff_precise( + &self, + curr_ts: i64, + max_locked_vote_weight: u128, + lockup_saturation_secs: u64, + ) -> Result; +} + +impl PrecisePosition for PositionV0 { + fn voting_power_precise( + &self, + voting_mint_config: &VotingMintConfigV0, + curr_ts: i64, + ) -> Result { + let baseline_vote_weight = (voting_mint_config + .baseline_vote_weight(self.amount_deposited_native)?) + .checked_mul(FALL_RATE_FACTOR) + .unwrap(); + let max_locked_vote_weight = + voting_mint_config.max_extra_lockup_vote_weight(self.amount_deposited_native)?; + let genesis_multiplier = + if curr_ts < self.genesis_end && voting_mint_config.genesis_vote_power_multiplier > 0 { + voting_mint_config.genesis_vote_power_multiplier + } else { + 1 + }; + + let locked_vote_weight = self.voting_power_precise_locked_precise( + curr_ts, + max_locked_vote_weight, + voting_mint_config.lockup_saturation_secs, + )?; + + locked_vote_weight + .checked_add(baseline_vote_weight) + .unwrap() + .checked_mul(genesis_multiplier as u128) + .ok_or(error!(ErrorCode::ArithmeticError)) + } + + /// Vote power contribution from locked funds only. + fn voting_power_precise_locked_precise( + &self, + curr_ts: i64, + max_locked_vote_weight: u128, + lockup_saturation_secs: u64, + ) -> Result { + if self.lockup.expired(curr_ts) || (max_locked_vote_weight == 0) { + return Ok(0); + } + + match self.lockup.kind { + LockupKind::None => Ok(0), + LockupKind::Cliff => self.voting_power_precise_cliff_precise( + curr_ts, + max_locked_vote_weight, + lockup_saturation_secs, + ), + LockupKind::Constant => self.voting_power_precise_cliff_precise( + curr_ts, + max_locked_vote_weight, + lockup_saturation_secs, + ), + } + } + + fn voting_power_precise_cliff_precise( + &self, + curr_ts: i64, + max_locked_vote_weight: u128, + lockup_saturation_secs: u64, + ) -> Result { + let remaining = min(self.lockup.seconds_left(curr_ts), lockup_saturation_secs); + Ok( + (max_locked_vote_weight) + .checked_mul(remaining as u128) + .unwrap() + .checked_mul(FALL_RATE_FACTOR) + .unwrap() + .checked_div(lockup_saturation_secs as u128) + .unwrap(), + ) + } +} + // Use bankers rounding pub fn apply_fall_rate_factor(item: u128) -> Option { let fall_rate_sub_one = FALL_RATE_FACTOR / 10; diff --git a/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs b/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs index 8f953ab70..1ec319f92 100644 --- a/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs +++ b/programs/voter-stake-registry/src/instructions/update_registrar_v0.rs @@ -5,7 +5,8 @@ use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct UpdateRegistrarArgsV0 { - pub position_freeze_authorities: Option>, + pub position_freeze_authorities: Vec, + pub position_update_authority: Option, } #[derive(Accounts)] @@ -29,9 +30,8 @@ pub fn handler(ctx: Context, args: UpdateRegistrarArgsV0) -> .map(|k| k.key()) .unwrap_or_default(); - if let Some(authorities) = args.position_freeze_authorities { - ctx.accounts.registrar.position_freeze_authorities = authorities; - } + ctx.accounts.registrar.position_freeze_authorities = args.position_freeze_authorities; + ctx.accounts.registrar.position_update_authority = args.position_update_authority; Ok(()) } diff --git a/tests/position-voting-rewards.ts b/tests/position-voting-rewards.ts index b459ca676..83897c488 100644 --- a/tests/position-voting-rewards.ts +++ b/tests/position-voting-rewards.ts @@ -1,14 +1,27 @@ import * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; -import { init, positionVotingRewardsResolvers } from "@helium/position-voting-rewards-sdk"; -import { PositionVotingRewards } from "@helium/idls/lib/types/position_voting_rewards"; -import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { BN, Program } from "@coral-xyz/anchor"; +import { init, positionVotingRewardsResolvers, vsrEpochInfoKey } from "@helium/position-voting-rewards-sdk"; +import { PositionVotingRewards } from "../target/types/position_voting_rewards"; +import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; import { init as vsrInit } from "../packages/voter-stake-registry-sdk/src"; import { ensureVSRIdl } from "./utils/fixtures"; -import { PublicKey } from "@solana/web3.js"; -import { initVsr } from "./utils/vsr"; -import { createMint } from "../packages/spl-utils/src"; -import { daoKey } from "../packages/helium-sub-daos-sdk/src"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { createPosition, initVsr } from "./utils/vsr"; +import { createAtaAndMint, createMint, toBN, toNumber } from "../packages/spl-utils/src"; +import { currentEpoch, daoKey } from "../packages/helium-sub-daos-sdk/src"; +import chai, { assert, expect } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { expectBnAccuracy } from "./utils/expectBnAccuracy"; +import { getUnixTimestamp } from "./utils/solana"; +import { AccountLayout } from "@solana/spl-token"; + +chai.use(chaiAsPromised); + +const REWARDS = 10000000; +const SECS_PER_DAY = 86400; +const SECS_PER_YEAR = 365 * SECS_PER_DAY; +const MAX_LOCKUP = 4 * SECS_PER_YEAR; +const SCALE = 100; describe("position-voting-rewards", () => { // Configure the client to use the local cluster. @@ -28,12 +41,16 @@ describe("position-voting-rewards", () => { let registrar: PublicKey; let genesisVotePowerMultiplierExpirationTs = 1; let hntMint: PublicKey; + let positionAuthorityKp: Keypair; const provider = anchor.getProvider() as anchor.AnchorProvider; const me = provider.wallet.publicKey; - before(async () => { + beforeEach(async () => { + positionAuthorityKp = Keypair.generate(); hntMint = await createMint(provider, 8, me, me); + await createAtaAndMint(provider, hntMint, toBN(REWARDS, 8), me); + await createAtaAndMint(provider, hntMint, toBN(REWARDS, 8), positionAuthorityKp.publicKey); vsrProgram = await vsrInit( provider, anchor.workspace.VoterStakeRegistry.programId, @@ -109,12 +126,15 @@ describe("position-voting-rewards", () => { vehntOptions.forEach(function ({ name, options }) { describe("vehnt tests - " + name, () => { + let position: PublicKey; + let vetokenTracker: PublicKey; + before(() => { genesisVotePowerMultiplierExpirationTs = 1; }); beforeEach(async () => { - ({ position, vault } = await createPosition( + ({ position } = await createPosition( vsrProgram, provider, registrar, @@ -122,190 +142,56 @@ describe("position-voting-rewards", () => { options, positionAuthorityKp )); + let { pubkeys: { vetokenTracker: tracker } } = await program.methods.initializeVetokenTrackerV0().accounts({ + registrar, + rewardsMint: hntMint, + payer: me, + rewardsAuthority: me, + }).rpcAndKeys({ skipPreflight: true }); + vetokenTracker = tracker! as PublicKey; + + const registrarAcc = await vsrProgram.account.registrar.fetch(registrar); + await vsrProgram.methods + .updateRegistrarV0({ + positionFreezeAuthorities: [vetokenTracker], + positionUpdateAuthority: null, + }) + .accounts({ + registrar, + proxyConfig: null, + }) + .rpc({ skipPreflight: true }); }); - it("allows vehnt delegation", async () => { - const lockupAmount = toBN(options.lockupAmount, 8); + it("allows vehnt enrollment", async () => { const method = program.methods - .delegateV0() + .enrollV0() .accounts({ position, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]); - const { delegatedPosition } = await method.pubkeys(); await method.rpc({ skipPreflight: true }); - const acc = await program.account.delegatedPositionV0.fetch( - delegatedPosition! - ); - const sdAcc = await program.account.subDaoV0.fetch(subDao); + const trackerAcc = await program.account.veTokenTrackerV0.fetch(vetokenTracker); const positionAcc = await vsrProgram.account.positionV0.fetch(position); const endTs = positionAcc.lockup.endTs.toNumber(); const startTs = positionAcc.lockup.startTs.toNumber(); const multiplier = typeof positionAcc.lockup.kind.cliff === "undefined" ? 1 - : (endTs - sdAcc.vehntLastCalculatedTs.toNumber()) / + : (endTs - trackerAcc.vetokenLastCalculatedTs.toNumber()) / (endTs - startTs); const expectedVeHnt = options.lockupAmount * options.expectedMultiplier * multiplier; expectBnAccuracy( - toBN(expectedVeHnt, 8).mul(new BN("1000000000000")), - sdAcc.vehntDelegated, + toBN(expectedVeHnt, 8).mul(new anchor.BN("1000000000000")), + trackerAcc.totalVetokens, typeof options.kind?.constant !== "undefined" ? 0 : 0.00000000001 ); - expectBnAccuracy(lockupAmount, acc.hntAmount, 0.01); - }); - - it("calculates subdao rewards", async () => { - // Onboard one hotspot to add to the utility score - const { rewardableEntityConfig } = await initTestRewardableEntityConfig( - hemProgram, - subDao - ); - const { maker, collection, makerKeypair, merkle } = await initTestMaker( - hemProgram, - provider, - rewardableEntityConfig, - dao - ); - const eccVerifier = loadKeypair( - __dirname + "/keypairs/verifier-test.json" - ); - const ecc = (await HeliumKeypair.makeRandom()).address.b58; - const hotspotOwner = Keypair.generate(); - - const { getAssetFn, getAssetProofFn, hotspot } = - await createMockCompression({ - collection, - dao, - merkle, - ecc, - hotspotOwner, - }); - console.log("I AM ISSUING"); - const issueMethod = hemProgram.methods - .issueEntityV0({ - entityKey: Buffer.from(bs58.decode(ecc)), - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 500000 }), - ]) - .accounts({ - maker, - recipient: hotspotOwner.publicKey, - issuingAuthority: makerKeypair.publicKey, - dao, - eccVerifier: eccVerifier.publicKey, - }) - .signers([makerKeypair, eccVerifier]); - - await issueMethod.rpc({ skipPreflight: true }); - await dcProgram.methods - .mintDataCreditsV0({ - // $50 onboard, $10 location assert - dcAmount: toBN(60, 5), - hntAmount: null, - }) - .accounts({ dcMint }) - .rpc({ skipPreflight: true }); - - const method = ( - await onboardIotHotspot({ - program: hemProgram, - assetId: hotspot, - maker, - dao, - rewardableEntityConfig, - location: new BN(1000), - getAssetFn, - getAssetProofFn, - dcFeePayer: me, - }) - ).signers([makerKeypair, hotspotOwner]); - - const { - pubkeys: { iotInfo: infoKey }, - } = await method.rpcAndKeys({ skipPreflight: true }); - - await hemProgram.methods - .setEntityActiveV0({ - isActive: true, - entityKey: Buffer.from(bs58.decode(ecc)), - }) - .accounts({ - activeDeviceAuthority: me, - rewardableEntityConfig, - info: infoKey! as PublicKey, - }) - .rpc({ skipPreflight: true }); - - const { subDaoEpochInfo } = await burnDc(1600000); - const epoch = ( - await program.account.subDaoEpochInfoV0.fetch(subDaoEpochInfo) - ).epoch; - - // delegate some vehnt - await program.methods - .delegateV0() - .accounts({ - position, - subDao, - positionAuthority: positionAuthorityKp.publicKey, - }) - .signers([positionAuthorityKp]) - .rpc({ skipPreflight: true }); - - const instr = program.methods - .calculateUtilityScoreV0({ - epoch, - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), - ]) - .accounts({ - subDao, - dao, - }); - - const pubkeys = await instr.pubkeys(); - await instr.rpc({ - skipPreflight: true, - commitment: "confirmed", - }); - - const subDaoInfo = await program.account.subDaoEpochInfoV0.fetch( - subDaoEpochInfo - ); - const daoInfo = await program.account.daoEpochInfoV0.fetch( - pubkeys.daoEpochInfo! - ); - - expect(daoInfo.numUtilityScoresCalculated).to.eq(1); - - const supply = (await getMint(provider.connection, hntMint)).supply; - const veHnt = toNumber(subDaoInfo.vehntAtEpochStart, 8); - const totalUtility = - Math.max(veHnt, 1) * Math.pow(50, 1 / 4) * Math.sqrt(16) * 1; - expect(daoInfo.totalRewards.toString()).to.eq(EPOCH_REWARDS.toString()); - expect(daoInfo.currentHntSupply.toString()).to.eq( - new BN(supply.toString()).add(new BN(EPOCH_REWARDS)).toString() - ); - - expectBnAccuracy( - toBN(totalUtility, 12), - daoInfo.totalUtilityScore, - 0.00000002 - ); - expectBnAccuracy( - toBN(totalUtility, 12), - subDaoInfo.utilityScore!, - 0.00000002 - ); }); it("allows transfers", async () => { @@ -317,7 +203,7 @@ describe("position-voting-rewards", () => { options, positionAuthorityKp ); - await program.methods + await vsrProgram.methods .transferV0({ amount: toBN(10, 8) }) .accounts({ sourcePosition: position, @@ -330,13 +216,12 @@ describe("position-voting-rewards", () => { }); it("allows lockup resets", async () => { - await program.methods + await vsrProgram.methods .resetLockupV0({ kind: { constant: {} }, periods: 365 * 4, }) .accounts({ - dao, position: position, positionAuthority: positionAuthorityKp.publicKey, }) @@ -344,13 +229,13 @@ describe("position-voting-rewards", () => { .rpc(); }); - describe("with delegated vehnt", () => { + describe("with enrolled vehnt", () => { beforeEach(async () => { await program.methods - .delegateV0() + .enrollV0() .accounts({ position, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]) @@ -367,7 +252,7 @@ describe("position-voting-rewards", () => { positionAuthorityKp ); await expect( - program.methods + vsrProgram.methods .transferV0({ amount: toBN(10, 8) }) .accounts({ sourcePosition: position, @@ -378,55 +263,54 @@ describe("position-voting-rewards", () => { .signers([positionAuthorityKp]) .rpc() ).to.eventually.be.rejectedWith( - "AnchorError caused by account: source_delegated_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is delegated." + "AnchorError caused by account: source_enrolled_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is enrolled." ); }); it("does not allow lockup resets", async () => { await expect( - program.methods + vsrProgram.methods .resetLockupV0({ kind: { constant: {} }, periods: 182 * 4, }) .accounts({ - dao, position: position, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]) .rpc() ).to.eventually.be.rejectedWith( - "AnchorError caused by account: delegated_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is delegated." + "AnchorError caused by account: enrolled_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is enrolled." ); }); - it("allows closing delegate", async () => { + it("allows closing enroll", async () => { await sleep(options.delay); const method = program.methods - .closeDelegationV0() + .unenrollV0() .accounts({ position, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]); - const { delegatedPosition, subDaoEpochInfo } = await method.pubkeys(); + const { enrolledPosition, vsrEpochInfo } = await method.pubkeys(); await method.rpc({ skipPreflight: true }); - const sdAcc = await program.account.subDaoV0.fetch(subDao); + const sdAcc = await program.account.veTokenTrackerV0.fetch(vetokenTracker); - expect(sdAcc.vehntFallRate.toNumber()).to.eq(0); + expect(sdAcc.vetokenFallRate.toNumber()).to.eq(0); // Extremely precise u128 can be off by dust. - expect(sdAcc.vehntDelegated.toNumber()).to.be.closeTo(0, 15); + expect(sdAcc.totalVetokens.toNumber()).to.be.closeTo(0, 15); assert.isFalse( - !!(await provider.connection.getAccountInfo(delegatedPosition!)) + !!(await provider.connection.getAccountInfo(enrolledPosition!)) ); }); - describe("with multiple delegated vehnt", () => { + describe("with multiple enrolled vehnt", () => { let basePosition: PublicKey; let basePositionOptions = { lockupPeriods: 365 * 1, @@ -447,18 +331,18 @@ describe("position-voting-rewards", () => { )); await program.methods - .delegateV0() + .enrollV0() .accounts({ position: basePosition, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]) .rpc({ skipPreflight: true }); }); - it("delegates proper vehnt amount", async () => { - const sdAcc = await program.account.subDaoV0.fetch(subDao); + it("enrolls proper vehnt amount", async () => { + const sdAcc = await program.account.veTokenTrackerV0.fetch(vetokenTracker); const expectedVehnt = options.lockupAmount * options.expectedMultiplier + @@ -467,31 +351,25 @@ describe("position-voting-rewards", () => { expectBnAccuracy( toBN(expectedVehnt, 8).mul(new BN("1000000000000")), - sdAcc.vehntDelegated, + sdAcc.totalVetokens, 0.0000001 ); }); - it("allows closing delegate", async () => { + it("allows closing enroll", async () => { const method = program.methods - .closeDelegationV0() + .unenrollV0() .accounts({ position: basePosition, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]); - const { delegatedPosition, subDaoEpochInfo } = - await method.pubkeys(); - // const sed = await program.account.subDaoEpochInfoV0.fetch( - // subDaoEpochInfo! - // ); - // console.log(sed.fallRatesFromClosingPositions.toString()); - // console.log(sed.vehntInClosingPositions.toString()); + const { enrolledPosition } = await method.pubkeys(); await method.rpc({ skipPreflight: true }); - const sdAcc = await program.account.subDaoV0.fetch(subDao); + const sdAcc = await program.account.veTokenTrackerV0.fetch(vetokenTracker); const positionAcc = await vsrProgram.account.positionV0.fetch( position ); @@ -500,7 +378,7 @@ describe("position-voting-rewards", () => { const multiplier = typeof positionAcc.lockup.kind.cliff === "undefined" ? 1 - : (endTs - sdAcc.vehntLastCalculatedTs.toNumber()) / + : (endTs - sdAcc.vetokenLastCalculatedTs.toNumber()) / (endTs - startTs); const expectedVehnt = @@ -508,11 +386,11 @@ describe("position-voting-rewards", () => { expectBnAccuracy( toBN(expectedVehnt, 8).mul(new BN("1000000000000")), - sdAcc.vehntDelegated, + sdAcc.totalVetokens, 0.0000000001 ); - expect(sdAcc.vehntFallRate.toNumber()).to.be.closeTo( + expect(sdAcc.vetokenFallRate.toNumber()).to.be.closeTo( typeof positionAcc.lockup.kind.cliff !== "undefined" ? ((options.lockupAmount * options.expectedMultiplier) / (endTs - startTs)) * @@ -522,129 +400,68 @@ describe("position-voting-rewards", () => { ); assert.isFalse( - !!(await provider.connection.getAccountInfo(delegatedPosition!)) + !!(await provider.connection.getAccountInfo(enrolledPosition!)) ); }); }); describe("with calculated rewards", () => { let epoch: anchor.BN; - let subDaoEpochInfo: PublicKey; + let vsrEpochInfo: PublicKey; beforeEach(async () => { + const offset = new BN(1 * 60 * 60 * 24); await vsrProgram.methods - .setTimeOffsetV0(new BN(1 * 60 * 60 * 24)) + .setTimeOffsetV0(offset) .accounts({ registrar }) .rpc({ skipPreflight: true }); - - ({ subDaoEpochInfo } = await burnDc(1600000)); - epoch = ( - await program.account.subDaoEpochInfoV0.fetch(subDaoEpochInfo) - ).epoch; - - await program.methods - .calculateUtilityScoreV0({ - epoch, - }) - .preInstructions([ - ComputeBudgetProgram.setComputeUnitLimit({ units: 400000 }), - ]) - .accounts({ - subDao, - dao, - }) - .rpc({ skipPreflight: true }); + const unixTime = await getUnixTimestamp(provider) + const time = offset.add(new BN(unixTime.toString())) + epoch = currentEpoch(time); }); - it("issues hnt rewards to subdaos, dnt to rewards escrow, and hst to hst pool", async () => { - const preBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(treasury))?.data! - ).amount; - const preHstBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(hstPool))?.data! - ).amount; - const preMobileBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(rewardsEscrow))?.data! - ).amount; + it("claim rewards", async () => { + // issue rewards await program.methods - .issueRewardsV0({ + .rewardForEpochV0({ epoch, + amount: toBN(REWARDS, 8), }) .accounts({ - subDao, - }) - .rpc({ skipPreflight: true }); - - await program.methods - .issueHstPoolV0({ - epoch, + vsrEpochInfo, }) .accounts({ - dao, + vetokenTracker, }) .rpc({ skipPreflight: true }); - const postBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(treasury))?.data! - ).amount; - const postMobileBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(rewardsEscrow))?.data! - ).amount; - const postHstBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(hstPool))?.data! - ).amount; - expect((postBalance - preBalance).toString()).to.eq( - ((1 - 0.32) * EPOCH_REWARDS).toString() - ); - expect((postHstBalance - preHstBalance).toString()).to.eq( - (0.32 * EPOCH_REWARDS).toString() - ); - expect((postMobileBalance - preMobileBalance).toString()).to.eq( - ((SUB_DAO_EPOCH_REWARDS / 100) * 94).toString() - ); - - const acc = await program.account.subDaoEpochInfoV0.fetch( - subDaoEpochInfo - ); - expect(Boolean(acc.rewardsIssuedAt)).to.be.true; - }); - - it("claim rewards", async () => { - // issue rewards - await sendInstructions(provider, [ - await program.methods - .issueRewardsV0({ - epoch, - }) - .accounts({ - subDao, - }) - .instruction(), - ]); - const method = program.methods .claimRewardsV0({ epoch, }) .accounts({ position, - subDao, + vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]); - const { delegatorAta } = await method.pubkeys(); + const { enrolledAta } = await method.pubkeys(); await method.rpc({ skipPreflight: true }); const postAtaBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(delegatorAta!))?.data! + (await provider.connection.getAccountInfo(enrolledAta!))?.data! ).amount; expect(Number(postAtaBalance)).to.be.within( - (SUB_DAO_EPOCH_REWARDS * 6) / 100 - 5, - (SUB_DAO_EPOCH_REWARDS * 6) / 100 + (REWARDS * 8) / 100 - 5, + (REWARDS * 8) / 100 ); }); }); }); }); }); -}) \ No newline at end of file +}) + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} \ No newline at end of file From bb7600686f12253faa7cbed49e800e224c83b687 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 11 Oct 2024 09:44:35 -0700 Subject: [PATCH 05/17] Fix tests --- .../src/resolvers.ts | 6 +++++ .../src/instructions/claim_rewards_v0.rs | 2 +- .../src/instructions/reward_for_epoch_v0.rs | 9 +++---- .../src/instructions/unenroll_v0.rs | 26 ++++++------------- programs/position-voting-rewards/src/util.rs | 6 ++--- .../src/state/position.rs | 2 +- tests/position-voting-rewards.ts | 21 +++++++-------- 7 files changed, 33 insertions(+), 39 deletions(-) diff --git a/packages/position-voting-rewards-sdk/src/resolvers.ts b/packages/position-voting-rewards-sdk/src/resolvers.ts index 4ef889490..b55239e3b 100644 --- a/packages/position-voting-rewards-sdk/src/resolvers.ts +++ b/packages/position-voting-rewards-sdk/src/resolvers.ts @@ -138,5 +138,11 @@ export const positionVotingRewardsResolvers = combineResolvers( mint: "rewardsMint", owner: "vsrEpochInfo", }), + ataResolver({ + instruction: "rewardForEpochV0", + account: "payerAta", + mint: "rewardsMint", + owner: "rewardsPayer", + }), vsrResolvers ); diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs index 84d2300ba..d16393d93 100644 --- a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -133,7 +133,7 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result enrolled_vetokens_at_epoch .checked_mul(ctx.accounts.vsr_epoch_info.rewards_amount as u128) .unwrap() - .checked_div(ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start as u128) + .checked_div(ctx.accounts.vsr_epoch_info.vetokens_at_epoch_start) .unwrap(), ) .unwrap(); diff --git a/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs index 20ba65989..8614c9e90 100644 --- a/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs +++ b/programs/position-voting-rewards/src/instructions/reward_for_epoch_v0.rs @@ -23,19 +23,18 @@ pub struct RewardForEpochV0<'info> { #[account( has_one = rewards_authority, has_one = rewards_mint, + has_one = registrar, )] - pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, - pub registrar: Account<'info, Registrar>, + pub vetoken_tracker: Box>, + pub registrar: Box>, #[account( init_if_needed, payer = rent_payer, space = 60 + VsrEpochInfoV0::INIT_SPACE, seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), &args.epoch.to_le_bytes()], bump, - has_one = vetoken_tracker, - has_one = registrar, )] - pub vsr_epoch_info: Account<'info, VsrEpochInfoV0>, + pub vsr_epoch_info: Box>, pub rewards_mint: Box>, #[account( init_if_needed, diff --git a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs index 8414119ed..2abf385dc 100644 --- a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs @@ -2,21 +2,19 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Mint, TokenAccount}; use voter_stake_registry::{ cpi::{accounts::ThawPositionV0, thaw_position_v0}, - state::{LockupKind, PositionV0, Registrar}, + state::*, VoterStakeRegistry, }; -use crate::{ - id, - state::*, - util::{calculate_vetoken_info, current_epoch, VetokenInfo}, - vetoken_tracker_seeds, -}; +use crate::{id, state::*, util::*, vetoken_tracker_seeds}; #[derive(Accounts)] pub struct UnenrollV0<'info> { #[account(mut)] pub payer: Signer<'info>, + /// CHECK: Doesn't need to be checked, just who gets the rent. + #[account(mut)] + pub rent_refund: AccountInfo<'info>, #[account( mut, seeds = [b"position".as_ref(), mint.key().as_ref()], @@ -44,6 +42,7 @@ pub struct UnenrollV0<'info> { #[account( mut, + close = rent_refund, has_one = vetoken_tracker, seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], bump = enrolled_position.bump_seed, @@ -96,7 +95,7 @@ pub fn handler(ctx: Context) -> Result<()> { let registrar = &ctx.accounts.registrar; let voting_mint_config = ®istrar.voting_mints[position.voting_mint_config_idx as usize]; let curr_ts = registrar.clock_unix_timestamp(); - let vetokens_at_curr_ts = position.voting_power(voting_mint_config, curr_ts)?; + let vetokens_at_curr_ts = position.voting_power_precise(voting_mint_config, curr_ts)?; let vetokens_info = calculate_vetoken_info( ctx.accounts.enrolled_position.start_ts, position, @@ -257,19 +256,10 @@ pub fn handler(ctx: Context) -> Result<()> { .fall_rates_from_closing_positions = 0; } - // If the lockup is expired or no tokens staked, close the enrollment. - if ctx.accounts.position.lockup.expired(curr_ts) - || ctx.accounts.position.amount_deposited_native == 0 - { - ctx - .accounts - .enrolled_position - .close(ctx.accounts.position_authority.to_account_info())?; - } thaw_position_v0(CpiContext::new_with_signer( ctx.accounts.vsr_program.to_account_info(), ThawPositionV0 { - authority: ctx.accounts.position_authority.to_account_info(), + authority: ctx.accounts.vetoken_tracker.to_account_info(), registrar: ctx.accounts.registrar.to_account_info(), position: ctx.accounts.position.to_account_info(), }, diff --git a/programs/position-voting-rewards/src/util.rs b/programs/position-voting-rewards/src/util.rs index f709a02b9..752709c6d 100644 --- a/programs/position-voting-rewards/src/util.rs +++ b/programs/position-voting-rewards/src/util.rs @@ -129,12 +129,12 @@ pub fn calculate_vetoken_info( // will not include the genesis. When this happens, we'll miss a vetokens correction if genesis_end_epoch_start_ts == position.genesis_end { genesis_end_vetokens_correction = position - .voting_power(voting_mint_config, genesis_end_epoch_start_ts - 1)? + .voting_power_precise(voting_mint_config, genesis_end_epoch_start_ts - 1)? .checked_sub(vetokens_at_genesis_end_exact) .unwrap(); } else { genesis_end_vetokens_correction = position - .voting_power(voting_mint_config, genesis_end_epoch_start_ts)? + .voting_power_precise(voting_mint_config, genesis_end_epoch_start_ts)? .checked_sub(vetokens_at_genesis_end_exact) .unwrap() // Correction factor @@ -162,7 +162,7 @@ pub fn calculate_vetoken_info( let end_epoch_start_ts = i64::try_from(current_epoch(position.lockup.end_ts)).unwrap() * EPOCH_LENGTH; let vetokens_at_closing_epoch_start = - position.voting_power(voting_mint_config, end_epoch_start_ts)?; + position.voting_power_precise(voting_mint_config, end_epoch_start_ts)?; end_vetokens_correction = vetokens_at_closing_epoch_start; end_fall_rate_correction = post_genesis_end_fall_rate; diff --git a/programs/voter-stake-registry/src/state/position.rs b/programs/voter-stake-registry/src/state/position.rs index 29a3f5559..6227d4f93 100644 --- a/programs/voter-stake-registry/src/state/position.rs +++ b/programs/voter-stake-registry/src/state/position.rs @@ -2,7 +2,7 @@ use std::cmp::min; use anchor_lang::prelude::*; -use super::{registrar, Lockup, LockupKind, Registrar, VotingMintConfigV0}; +use super::{Lockup, LockupKind, Registrar, VotingMintConfigV0}; use crate::error::*; pub const PRECISION_FACTOR: u128 = 1_000_000_000_000; diff --git a/tests/position-voting-rewards.ts b/tests/position-voting-rewards.ts index 83897c488..04c02c281 100644 --- a/tests/position-voting-rewards.ts +++ b/tests/position-voting-rewards.ts @@ -263,7 +263,7 @@ describe("position-voting-rewards", () => { .signers([positionAuthorityKp]) .rpc() ).to.eventually.be.rejectedWith( - "AnchorError caused by account: source_enrolled_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is enrolled." + "AnchorError caused by account: source_position. Error Code: PositionFrozen. Error Number: 6060. Error Message: PositionFrozen." ); }); @@ -281,7 +281,7 @@ describe("position-voting-rewards", () => { .signers([positionAuthorityKp]) .rpc() ).to.eventually.be.rejectedWith( - "AnchorError caused by account: enrolled_position. Error Code: PositionChangeWhileDelegated. Error Number: 6014. Error Message: Cannot change a position while it is enrolled." + "AnchorError caused by account: position. Error Code: PositionFrozen. Error Number: 6060. Error Message: PositionFrozen." ); }); @@ -293,6 +293,7 @@ describe("position-voting-rewards", () => { position, vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, + rentRefund: me, }) .signers([positionAuthorityKp]); @@ -363,6 +364,7 @@ describe("position-voting-rewards", () => { position: basePosition, vetokenTracker, positionAuthority: positionAuthorityKp.publicKey, + rentRefund: me, }) .signers([positionAuthorityKp]); @@ -427,11 +429,11 @@ describe("position-voting-rewards", () => { epoch, amount: toBN(REWARDS, 8), }) - .accounts({ - vsrEpochInfo, - }) .accounts({ vetokenTracker, + registrar, + vsrEpochInfo, + rewardsPayer: me, }) .rpc({ skipPreflight: true }); @@ -445,16 +447,13 @@ describe("position-voting-rewards", () => { positionAuthority: positionAuthorityKp.publicKey, }) .signers([positionAuthorityKp]); - const { enrolledAta } = await method.pubkeys(); + const { rewardsPool } = await method.pubkeys(); await method.rpc({ skipPreflight: true }); const postAtaBalance = AccountLayout.decode( - (await provider.connection.getAccountInfo(enrolledAta!))?.data! + (await provider.connection.getAccountInfo(rewardsPool!))?.data! ).amount; - expect(Number(postAtaBalance)).to.be.within( - (REWARDS * 8) / 100 - 5, - (REWARDS * 8) / 100 - ); + expect(Number(postAtaBalance)).to.be.within(0, 5000); }); }); }); From 2b6264b43214b726b318dd91fec27d22d4994928 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 11 Oct 2024 12:32:47 -0700 Subject: [PATCH 06/17] Add tie-in to helim sub daos --- .github/workflows/tests.yaml | 1 + Cargo.lock | 1 + .../src/heliumCommonResolver.ts | 4 + .../helium-admin-cli/src/create-subdao.ts | 2 + .../helium-admin-cli/src/update-subdao.ts | 2 + packages/helium-sub-daos-sdk/src/resolvers.ts | 12 ++ programs/helium-sub-daos/Cargo.toml | 1 + programs/helium-sub-daos/src/error.rs | 3 + .../src/instructions/initialize_sub_dao_v0.rs | 19 ++- .../src/instructions/issue_rewards_v0.rs | 22 ++- .../instructions/issue_voting_rewards_v0.rs | 155 ++++++++++++++++++ .../helium-sub-daos/src/instructions/mod.rs | 2 + .../src/instructions/update_sub_dao_v0.rs | 14 +- programs/helium-sub-daos/src/lib.rs | 7 + programs/helium-sub-daos/src/state.rs | 12 +- tests/data-credits.ts | 2 + tests/distributor-oracle.ts | 1 + tests/helium-entity-manager.ts | 12 ++ tests/helium-sub-daos.ts | 82 ++++++++- tests/hexboosting.ts | 9 + tests/mobile-entity-manager.ts | 10 ++ tests/utils/daos.ts | 45 +++-- tests/utils/fixtures.ts | 20 ++- 23 files changed, 401 insertions(+), 37 deletions(-) create mode 100644 programs/helium-sub-daos/src/instructions/issue_voting_rewards_v0.rs diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d4270dedd..fea50219b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -152,6 +152,7 @@ jobs: - tests/voter-stake-registry.ts - tests/fanout.ts - tests/sus.ts + - tests/position-voting-rewards.ts steps: - uses: actions/checkout@v3 - uses: ./.github/actions/build-anchor/ diff --git a/Cargo.lock b/Cargo.lock index 56c36a50c..c0a52369d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,7 @@ dependencies = [ "circuit-breaker", "default-env", "mpl-token-metadata", + "position-voting-rewards", "rust_decimal", "shared-utils", "solana-security-txt", diff --git a/packages/anchor-resolvers/src/heliumCommonResolver.ts b/packages/anchor-resolvers/src/heliumCommonResolver.ts index 1ec207865..915ae31ce 100644 --- a/packages/anchor-resolvers/src/heliumCommonResolver.ts +++ b/packages/anchor-resolvers/src/heliumCommonResolver.ts @@ -26,6 +26,10 @@ export const heliumCommonResolver = resolveIndividual(async ({ path }) => { return new PublicKey("Sysvar1nstructions1111111111111111111111111"); case "lazyDistributorProgram": return new PublicKey("1azyuavdMyvsivtNxPoz6SucD18eDHeXzFCUPq5XU7w"); + case "positionVotingRewardsProgram": + return new PublicKey("pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM"); + case "positionVotingRewards": + return new PublicKey("pvr1pJdeAcW6tzFyPRSmkL5Xwysi1Tq79f7KF2XB4zM"); case "noEmitProgram": return new PublicKey("noEmmgLmQdk6DLiPV8CSwQv3qQDyGEhz9m5A4zhtByv"); default: diff --git a/packages/helium-admin-cli/src/create-subdao.ts b/packages/helium-admin-cli/src/create-subdao.ts index e5a360d21..debcc5fb6 100644 --- a/packages/helium-admin-cli/src/create-subdao.ts +++ b/packages/helium-admin-cli/src/create-subdao.ts @@ -513,6 +513,8 @@ export async function run(args: any = process.argv) { instructions: [ await heliumSubDaosProgram.methods .updateSubDaoV0({ + vetokenTracker: null, + votingRewardsPercent: null, authority, emissionSchedule, dcBurnAuthority: null, diff --git a/packages/helium-admin-cli/src/update-subdao.ts b/packages/helium-admin-cli/src/update-subdao.ts index 8b430edf2..1f21c382a 100644 --- a/packages/helium-admin-cli/src/update-subdao.ts +++ b/packages/helium-admin-cli/src/update-subdao.ts @@ -205,6 +205,8 @@ export async function run(args: any = process.argv) { instructions.push( await program.methods .updateSubDaoV0({ + vetokenTracker: null, + votingRewardsPercent: null, authority: argv.newAuthority ? new PublicKey(argv.newAuthority) : null, emissionSchedule: argv.newEmissionsSchedulePath ? await parseEmissionsSchedule(argv.newEmissionsSchedulePath) diff --git a/packages/helium-sub-daos-sdk/src/resolvers.ts b/packages/helium-sub-daos-sdk/src/resolvers.ts index 08f729c99..1a38b3889 100644 --- a/packages/helium-sub-daos-sdk/src/resolvers.ts +++ b/packages/helium-sub-daos-sdk/src/resolvers.ts @@ -164,6 +164,18 @@ export const heliumSubDaosResolvers = combineResolvers( mint: "mint", owner: "positionAuthority", }), + ataResolver({ + instruction: "issueVotingRewardsV0", + account: "rewardsPool", + mint: "rewardsMint", + owner: "vsrEpochInfo", + }), + ataResolver({ + instruction: "issueVotingRewardsV0", + account: "payerAta", + mint: "rewardsMint", + owner: "subDao", + }), resolveIndividual(async ({ args, path, accounts }) => { if (path[path.length - 1] == "clockwork") { return THREAD_PID; diff --git a/programs/helium-sub-daos/Cargo.toml b/programs/helium-sub-daos/Cargo.toml index 4c6921bad..93311033b 100644 --- a/programs/helium-sub-daos/Cargo.toml +++ b/programs/helium-sub-daos/Cargo.toml @@ -28,6 +28,7 @@ voter-stake-registry = { path = "../voter-stake-registry", features = ["no-entry shared-utils = { workspace = true } circuit-breaker = { workspace = true } treasury-management = { path = "../treasury-management", features = ["cpi"] } +position-voting-rewards = { path = "../position-voting-rewards", features = ["cpi"] } time = "0.3.17" solana-security-txt = { workspace = true } diff --git a/programs/helium-sub-daos/src/error.rs b/programs/helium-sub-daos/src/error.rs index 7fe26c9a4..4ccd0fe3a 100644 --- a/programs/helium-sub-daos/src/error.rs +++ b/programs/helium-sub-daos/src/error.rs @@ -52,4 +52,7 @@ pub enum ErrorCode { #[msg("Cannot delegate on a position ending this epoch")] NoDelegateEndingPosition, + + #[msg("Rewards were already issued")] + RewardsAlreadyIssued, } diff --git a/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs b/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs index ee267f204..ce8705ca6 100644 --- a/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/initialize_sub_dao_v0.rs @@ -1,17 +1,16 @@ -use crate::next_epoch_ts; -use crate::{state::*, EPOCH_LENGTH}; use anchor_lang::prelude::*; -use anchor_spl::associated_token::AssociatedToken; -use anchor_spl::token::spl_token::instruction::AuthorityType; -use anchor_spl::token::{set_authority, Mint, SetAuthority, Token, TokenAccount}; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{ + set_authority, spl_token::instruction::AuthorityType, Mint, SetAuthority, Token, TokenAccount, + }, +}; use circuit_breaker::{ cpi::{ - accounts::InitializeAccountWindowedBreakerV0, accounts::InitializeMintWindowedBreakerV0, + accounts::{InitializeAccountWindowedBreakerV0, InitializeMintWindowedBreakerV0}, initialize_account_windowed_breaker_v0, initialize_mint_windowed_breaker_v0, }, CircuitBreaker, InitializeAccountWindowedBreakerArgsV0, InitializeMintWindowedBreakerArgsV0, -}; -use circuit_breaker::{ ThresholdType as CBThresholdType, WindowedCircuitBreakerConfigV0 as CBWindowedCircuitBreakerConfigV0, }; @@ -26,6 +25,8 @@ use treasury_management::{ Curve as TreasuryCurve, InitializeTreasuryManagementArgsV0, TreasuryManagement, }; +use crate::{next_epoch_ts, state::*, EPOCH_LENGTH}; + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub enum Curve { // c^k @@ -284,6 +285,8 @@ pub fn handler(ctx: Context, args: InitializeSubDaoArgsV0) - onboarding_data_only_dc_fee: args.onboarding_data_only_dc_fee, active_device_authority: args.active_device_authority, dc_onboarding_fees_paid: 0, + voting_rewards_percent: 0, + vetoken_tracker: Pubkey::default(), }); resize_to_fit( diff --git a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs index aa837fb97..93f9c054e 100644 --- a/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs +++ b/programs/helium-sub-daos/src/instructions/issue_rewards_v0.rs @@ -1,13 +1,13 @@ -use crate::{current_epoch, error::ErrorCode, state::*, OrArithError, EPOCH_LENGTH, TESTING}; +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; use circuit_breaker::{ cpi::{accounts::MintV0, mint_v0}, CircuitBreaker, MintArgsV0, MintWindowedCircuitBreakerV0, }; - -use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; use shared_utils::precise_number::{InnerUint, PreciseNumber}; +use crate::{current_epoch, error::ErrorCode, state::*, OrArithError, EPOCH_LENGTH, TESTING}; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct IssueRewardsArgsV0 { pub epoch: u64, @@ -43,7 +43,7 @@ pub struct IssueRewardsV0<'info> { has_one = sub_dao, seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), &args.epoch.to_le_bytes()], bump = sub_dao_epoch_info.bump_seed, - constraint = TESTING || sub_dao_epoch_info.rewards_issued_at.is_none() + constraint = TESTING || sub_dao_epoch_info.rewards_issued_at.is_none() @ ErrorCode::RewardsAlreadyIssued )] pub sub_dao_epoch_info: Box>, #[account( @@ -172,7 +172,9 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result let max_percent = 100_u64.checked_mul(10_0000000).unwrap(); let dnt_emissions = (total_emissions as u128) .checked_mul(u128::from( - max_percent - ctx.accounts.sub_dao.delegator_rewards_percent, + max_percent + - ctx.accounts.sub_dao.delegator_rewards_percent + - ctx.accounts.sub_dao.voting_rewards_percent, )) .unwrap() .checked_div(max_percent as u128) // 100% with 2 decimals accuracy @@ -192,7 +194,13 @@ pub fn handler(ctx: Context, args: IssueRewardsArgsV0) -> Result )?; let delegation_rewards_amount = if delegators_present { - total_emissions.checked_sub(dnt_emissions).unwrap() + (total_emissions as u128) + .checked_mul(u128::from(ctx.accounts.sub_dao.delegator_rewards_percent)) + .unwrap() + .checked_div(max_percent as u128) // 100% with 2 decimals accuracy + .unwrap() + .try_into() + .unwrap() } else { 0 }; diff --git a/programs/helium-sub-daos/src/instructions/issue_voting_rewards_v0.rs b/programs/helium-sub-daos/src/instructions/issue_voting_rewards_v0.rs new file mode 100644 index 000000000..14f0e6133 --- /dev/null +++ b/programs/helium-sub-daos/src/instructions/issue_voting_rewards_v0.rs @@ -0,0 +1,155 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::{Mint, Token, TokenAccount}, +}; +use circuit_breaker::{ + cpi::{accounts::MintV0, mint_v0}, + CircuitBreaker, MintArgsV0, MintWindowedCircuitBreakerV0, +}; +use position_voting_rewards::{ + cpi::{accounts::RewardForEpochV0, reward_for_epoch_v0}, + instructions::RewardForEpochArgsV0, + program::PositionVotingRewards, + state::{VeTokenTrackerV0, VsrEpochInfoV0}, +}; +use voter_stake_registry::state::Registrar; + +use crate::{ + current_epoch, error::ErrorCode, sub_dao_seeds, GetEmissions, SubDaoV0, EPOCH_LENGTH, TESTING, +}; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct IssueVotingRewardsArgsV0 { + pub epoch: u64, +} + +#[derive(Accounts)] +#[instruction(args: IssueVotingRewardsArgsV0)] +pub struct IssueVotingRewardsV0<'info> { + #[account( + has_one = registrar, + has_one = vetoken_tracker, + )] + pub sub_dao: Box>, + #[account(mut)] + pub rent_payer: Signer<'info>, + #[account( + has_one = rewards_mint, + has_one = registrar, + )] + pub vetoken_tracker: Box>, + pub registrar: Box>, + #[account( + mut, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), &args.epoch.to_le_bytes()], + bump, + seeds::program = position_voting_rewards, + )] + /// CHECK: Checked by seeds + pub vsr_epoch_info: UncheckedAccount<'info>, + #[account(mut)] + pub rewards_mint: Box>, + #[account( + init_if_needed, + payer = rent_payer, + associated_token::mint = rewards_mint, + associated_token::authority = vsr_epoch_info, + )] + pub rewards_pool: Box>, + #[account( + init_if_needed, + payer = rent_payer, + associated_token::mint = rewards_mint, + associated_token::authority = sub_dao, + )] + pub payer_ata: Box>, + #[account( + mut, + seeds = ["mint_windowed_breaker".as_bytes(), rewards_mint.key().as_ref()], + seeds::program = circuit_breaker_program.key(), + bump = rewards_mint_circuit_breaker.bump_seed + )] + pub rewards_mint_circuit_breaker: Box>, + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub position_voting_rewards: Program<'info, PositionVotingRewards>, + pub circuit_breaker_program: Program<'info, CircuitBreaker>, +} + +pub fn handler(ctx: Context, args: IssueVotingRewardsArgsV0) -> Result<()> { + let curr_ts = Clock::get()?.unix_timestamp; + let epoch_curr_ts = current_epoch(curr_ts); + let end_of_epoch_ts = i64::try_from(args.epoch + 1).unwrap() * EPOCH_LENGTH; + + if !TESTING && args.epoch >= epoch_curr_ts { + return Err(error!(ErrorCode::EpochNotOver)); + } + + let data = ctx.accounts.vsr_epoch_info.try_borrow_data()?; + if !data.is_empty() { + let vsr_epoch_info = VsrEpochInfoV0::try_from_slice(&data)?; + if vsr_epoch_info.rewards_issued_at.is_some() { + return Err(error!(ErrorCode::RewardsAlreadyIssued)); + } + } + drop(data); + + let total_emissions = ctx + .accounts + .sub_dao + .emission_schedule + .get_emissions_at(end_of_epoch_ts) + .unwrap(); + let max_percent = 100_u64.checked_mul(10_0000000).unwrap(); + let amount = (total_emissions as u128) + .checked_mul(u128::from(ctx.accounts.sub_dao.voting_rewards_percent)) + .unwrap() + .checked_div(max_percent as u128) // 100% with 2 decimals accuracy + .unwrap() + .try_into() + .unwrap(); + + mint_v0( + CpiContext::new_with_signer( + ctx.accounts.circuit_breaker_program.to_account_info(), + MintV0 { + mint: ctx.accounts.rewards_mint.to_account_info(), + to: ctx.accounts.payer_ata.to_account_info(), + mint_authority: ctx.accounts.sub_dao.to_account_info(), + circuit_breaker: ctx.accounts.rewards_mint_circuit_breaker.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + &[sub_dao_seeds!(ctx.accounts.sub_dao)], + ), + MintArgsV0 { amount }, + )?; + + reward_for_epoch_v0( + CpiContext::new_with_signer( + ctx.accounts.position_voting_rewards.to_account_info(), + RewardForEpochV0 { + rewards_authority: ctx.accounts.sub_dao.to_account_info(), + rewards_payer: ctx.accounts.sub_dao.to_account_info(), + rent_payer: ctx.accounts.rent_payer.to_account_info(), + vetoken_tracker: ctx.accounts.vetoken_tracker.to_account_info(), + registrar: ctx.accounts.registrar.to_account_info(), + vsr_epoch_info: ctx.accounts.vsr_epoch_info.to_account_info(), + rewards_mint: ctx.accounts.rewards_mint.to_account_info(), + rewards_pool: ctx.accounts.rewards_pool.to_account_info(), + payer_ata: ctx.accounts.payer_ata.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), + }, + &[sub_dao_seeds!(ctx.accounts.sub_dao)], + ), + RewardForEpochArgsV0 { + epoch: args.epoch, + amount, + }, + )?; + + Ok(()) +} diff --git a/programs/helium-sub-daos/src/instructions/mod.rs b/programs/helium-sub-daos/src/instructions/mod.rs index 9cecb4373..eef24b5a5 100644 --- a/programs/helium-sub-daos/src/instructions/mod.rs +++ b/programs/helium-sub-daos/src/instructions/mod.rs @@ -6,6 +6,7 @@ pub mod initialize_dao_v0; pub mod initialize_sub_dao_v0; pub mod issue_hst_pool_v0; pub mod issue_rewards_v0; +pub mod issue_voting_rewards_v0; pub mod temp_update_sub_dao_epoch_info; pub mod track_dc_burn_v0; pub mod track_dc_onboarding_fees_v0; @@ -21,6 +22,7 @@ pub use initialize_dao_v0::*; pub use initialize_sub_dao_v0::*; pub use issue_hst_pool_v0::*; pub use issue_rewards_v0::*; +pub use issue_voting_rewards_v0::*; pub use temp_update_sub_dao_epoch_info::*; pub use track_dc_burn_v0::*; pub use track_dc_onboarding_fees_v0::*; diff --git a/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs b/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs index c2c9f873f..5b0ad3acc 100644 --- a/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs +++ b/programs/helium-sub-daos/src/instructions/update_sub_dao_v0.rs @@ -1,7 +1,8 @@ -use crate::state::*; use anchor_lang::prelude::*; use shared_utils::resize_to_fit; +use crate::state::*; + #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct UpdateSubDaoArgsV0 { pub authority: Option, @@ -12,6 +13,8 @@ pub struct UpdateSubDaoArgsV0 { pub delegator_rewards_percent: Option, pub onboarding_data_only_dc_fee: Option, pub active_device_authority: Option, + pub voting_rewards_percent: Option, + pub vetoken_tracker: Option, } #[derive(Accounts)] @@ -65,6 +68,15 @@ pub fn handler(ctx: Context, args: UpdateSubDaoArgsV0) -> Result ctx.accounts.sub_dao.active_device_authority = active_device_authority; } + if let Some(voting_rewards_percent) = args.voting_rewards_percent { + require_gte!(max_percent, voting_rewards_percent); + ctx.accounts.sub_dao.voting_rewards_percent = voting_rewards_percent; + } + + if let Some(vetoken_tracker) = args.vetoken_tracker { + ctx.accounts.sub_dao.vetoken_tracker = vetoken_tracker; + } + resize_to_fit( &ctx.accounts.payer.to_account_info(), &ctx.accounts.system_program.to_account_info(), diff --git a/programs/helium-sub-daos/src/lib.rs b/programs/helium-sub-daos/src/lib.rs index 3ec9d7c4e..010400c95 100644 --- a/programs/helium-sub-daos/src/lib.rs +++ b/programs/helium-sub-daos/src/lib.rs @@ -127,4 +127,11 @@ pub mod helium_sub_daos { ) -> Result<()> { admin_set_dc_onboarding_fees_paid_epoch_info::handler(ctx, args) } + + pub fn issue_voting_rewards_v0( + ctx: Context, + args: IssueVotingRewardsArgsV0, + ) -> Result<()> { + issue_voting_rewards_v0::handler(ctx, args) + } } diff --git a/programs/helium-sub-daos/src/state.rs b/programs/helium-sub-daos/src/state.rs index 749688e10..633294701 100644 --- a/programs/helium-sub-daos/src/state.rs +++ b/programs/helium-sub-daos/src/state.rs @@ -1,7 +1,6 @@ -use crate::error::ErrorCode; use anchor_lang::prelude::*; -use crate::EPOCH_LENGTH; +use crate::{error::ErrorCode, EPOCH_LENGTH}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct EmissionScheduleItem { @@ -232,6 +231,15 @@ pub struct SubDaoV0 { pub onboarding_data_only_dc_fee: u64, pub dc_onboarding_fees_paid: u64, // the total amount of dc onboarding fees paid to this subdao by active hotspots (inactive hotspots are excluded) pub active_device_authority: Pubkey, // authority that can mark hotspots as active/inactive + pub voting_rewards_percent: u64, // number between 0 - (100_u64 * 100_000_000). The % of DNT rewards voting rewards receive with 8 decimal places of accuracy + pub vetoken_tracker: Pubkey, // the vetoken tracker for subnetwork voting rewards +} + +#[macro_export] +macro_rules! sub_dao_seeds { + ( $s:expr ) => { + &[b"sub_dao".as_ref(), $s.dnt_mint.as_ref(), &[$s.bump_seed]] + }; } #[cfg(test)] diff --git a/tests/data-credits.ts b/tests/data-credits.ts index 85dd30291..7913ae05f 100644 --- a/tests/data-credits.ts +++ b/tests/data-credits.ts @@ -209,6 +209,7 @@ describe("data-credits", () => { ({ subDao } = await initTestSubdao({ hsdProgram, + vsrProgram, provider, authority: me, dao, @@ -337,6 +338,7 @@ describe("data-credits", () => { it("redelegates delegated data credits", async () => { const { subDao: destinationSubDao } = await initTestSubdao({ hsdProgram, + vsrProgram, provider, authority: me, dao, diff --git a/tests/distributor-oracle.ts b/tests/distributor-oracle.ts index d7c47df1e..7f9c0e2e5 100644 --- a/tests/distributor-oracle.ts +++ b/tests/distributor-oracle.ts @@ -327,6 +327,7 @@ describe("distributor-oracle", () => { hemProgram, hsdProgram, dcProgram, + vsrProgram, 1, 1, registrar, diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index b13aff16d..5aea03392 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -69,12 +69,15 @@ import { keyToAssetKey, mobileInfoKey, } from "@helium/helium-entity-manager-sdk"; +import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; +import { init as initVsr } from "../packages/voter-stake-registry-sdk/src"; chai.use(chaiAsPromised); describe("helium-entity-manager", () => { anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); + let vsrProgram: Program; let dcProgram: Program; let hsdProgram: Program; let hemProgram: Program; @@ -103,6 +106,12 @@ describe("helium-entity-manager", () => { anchor.workspace.NoEmit.idl ); + vsrProgram = await initVsr( + provider, + anchor.workspace.VoterStakeRegistry.programId, + anchor.workspace.VoterStakeRegistry.idl + ); + hsdProgram = await initHeliumSubDaos( provider, anchor.workspace.HeliumSubDaos.programId, @@ -130,6 +139,7 @@ describe("helium-entity-manager", () => { activeDeviceAuthority = Keypair.generate(); ({ subDao } = await initTestSubdao({ hsdProgram, + vsrProgram, provider, authority: me, dao, @@ -884,6 +894,8 @@ describe("helium-entity-manager", () => { await hsdProgram.methods .updateSubDaoV0({ + vetokenTracker: null, + votingRewardsPercent: null, authority: null, dcBurnAuthority: null, emissionSchedule: null, diff --git a/tests/helium-sub-daos.ts b/tests/helium-sub-daos.ts index 234990a06..04d8763d7 100644 --- a/tests/helium-sub-daos.ts +++ b/tests/helium-sub-daos.ts @@ -1,11 +1,15 @@ import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { init as cbInit } from "@helium/circuit-breaker-sdk"; +import { + init as rewardsInit, + vsrEpochInfoKey, +} from "@helium/position-voting-rewards-sdk"; import { Keypair as HeliumKeypair } from "@helium/crypto"; import { daoKey, EPOCH_LENGTH } from "@helium/helium-sub-daos-sdk"; -import { CircuitBreaker } from "@helium/idls/lib/types/circuit_breaker"; -import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; -import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; +import { CircuitBreaker } from "../target/types/circuit_breaker"; +import { HeliumSubDaos } from "../target/types/helium_sub_daos"; +import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; import { createAtaAndMint, createAtaAndTransfer, @@ -30,8 +34,10 @@ import { init as dcInit } from "../packages/data-credits-sdk/src"; import { init as issuerInit, onboardIotHotspot } from "../packages/helium-entity-manager-sdk/src"; import { currentEpoch, + delegatorRewardsPercent, heliumSubDaosResolvers, subDaoEpochInfoKey, + subDaoKey, } from "../packages/helium-sub-daos-sdk/src"; import { init as vsrInit } from "../packages/voter-stake-registry-sdk/src"; import { DataCredits } from "../target/types/data_credits"; @@ -41,6 +47,7 @@ import { createMockCompression } from "./utils/compression"; import { initTestDao, initTestSubdao } from "./utils/daos"; import { expectBnAccuracy } from "./utils/expectBnAccuracy"; import { + ensurePVRIdl, ensureDCIdl, ensureHSDIdl, ensureVSRIdl, @@ -52,6 +59,7 @@ import { getUnixTimestamp, loadKeypair } from "./utils/solana"; import { createPosition, initVsr } from "./utils/vsr"; // @ts-ignore import bs58 from "bs58"; +import { PositionVotingRewards } from "../target/types/position_voting_rewards"; chai.use(chaiAsPromised); @@ -88,8 +96,10 @@ describe("helium-sub-daos", () => { let hemProgram: Program; let cbProgram: Program; let vsrProgram: Program; + let rewardsProgram: Program; let registrar: PublicKey; + let subDaoRegistrar: PublicKey; let position: PublicKey; let vault: PublicKey; let hntMint: PublicKey; @@ -104,6 +114,12 @@ describe("helium-sub-daos", () => { anchor.workspace.DataCredits.programId, anchor.workspace.DataCredits.idl ); + rewardsProgram = await rewardsInit( + provider, + anchor.workspace.PositionVotingRewards.programId, + anchor.workspace.PositionVotingRewards.idl + ); + ensurePVRIdl(rewardsProgram); cbProgram = await cbInit( provider, anchor.workspace.CircuitBreaker.programId, @@ -145,7 +161,7 @@ describe("helium-sub-daos", () => { provider.wallet.publicKey ); const { subDao, treasury, mint, treasuryCircuitBreaker } = - await initTestSubdao({hsdProgram: program, provider, authority: provider.wallet.publicKey, dao}); + await initTestSubdao({hsdProgram: program, vsrProgram, provider, authority: provider.wallet.publicKey, dao}); const account = await program.account.subDaoV0.fetch(subDao!); const breaker = @@ -166,8 +182,10 @@ describe("helium-sub-daos", () => { let subDao: PublicKey; let treasury: PublicKey; let hstPool: PublicKey; + let dntMint: PublicKey; let dcMint: PublicKey; let rewardsEscrow: PublicKey; + let vetokenTracker: PublicKey; let genesisVotePowerMultiplierExpirationTs = 1; let initialSupply = toBN(223_000_000, 8); @@ -229,18 +247,50 @@ describe("helium-sub-daos", () => { ({ dataCredits: { dcMint }, - subDao: { subDao, treasury, rewardsEscrow }, + subDao: { subDao, treasury, rewardsEscrow, mint: dntMint, subDaoRegistrar }, dao: { dao }, } = await initWorld( provider, hemProgram, program, dcProgram, + vsrProgram, EPOCH_REWARDS, SUB_DAO_EPOCH_REWARDS, registrar, - hntMint + hntMint, )); + // Add on the voting rewards + let { + pubkeys: { vetokenTracker: tracker }, + } = await rewardsProgram.methods + .initializeVetokenTrackerV0() + .accounts({ + registrar: subDaoRegistrar, + rewardsMint: dntMint, + payer: me, + rewardsAuthority: subDao, + }) + .rpcAndKeys({ skipPreflight: true }); + vetokenTracker = tracker!; + await program.methods + .updateSubDaoV0({ + vetokenTracker, + votingRewardsPercent: delegatorRewardsPercent(2), + authority: null, + dcBurnAuthority: null, + emissionSchedule: null, + onboardingDcFee: null, + onboardingDataOnlyDcFee: null, + registrar: null, + delegatorRewardsPercent: null, + activeDeviceAuthority: null, + }) + .accounts({ + subDao, + }) + .rpc({ skipPreflight: true }); + hstPool = (await program.account.daoV0.fetch(dao)).hstPool; }); @@ -267,6 +317,8 @@ describe("helium-sub-daos", () => { const newAuth = Keypair.generate().publicKey; await program.methods .updateSubDaoV0({ + vetokenTracker: null, + votingRewardsPercent: null, authority: newAuth, dcBurnAuthority: null, emissionSchedule: null, @@ -845,6 +897,7 @@ describe("helium-sub-daos", () => { }); it("issues hnt rewards to subdaos, dnt to rewards escrow, and hst to hst pool", async () => { + const subDaoAcc = await program.account.subDaoV0.fetch(subDao); const preBalance = AccountLayout.decode( (await provider.connection.getAccountInfo(treasury))?.data! ).amount; @@ -863,6 +916,18 @@ describe("helium-sub-daos", () => { }) .rpc({ skipPreflight: true }); + const { pubkeys: { vsrEpochInfo } } =await program.methods + .issueVotingRewardsV0({ + epoch, + }) + .accounts({ + subDao, + vsrEpochInfo: vsrEpochInfoKey(vetokenTracker, epoch.mul(new BN(EPOCH_LENGTH)))[0], + }) + .rpcAndKeys({ skipPreflight: true }); + + const vsrEpochInfoAcc = await rewardsProgram.account.vsrEpochInfoV0.fetch(vsrEpochInfo!); + await program.methods .issueHstPoolV0({ epoch, @@ -888,8 +953,11 @@ describe("helium-sub-daos", () => { (0.32 * EPOCH_REWARDS).toString() ); expect((postMobileBalance - preMobileBalance).toString()).to.eq( - ((SUB_DAO_EPOCH_REWARDS / 100) * 94).toString() + ((SUB_DAO_EPOCH_REWARDS / 100) * 92).toString() ); + expect( + vsrEpochInfoAcc.rewardsAmount.toString() + ).to.eq((0.02 * SUB_DAO_EPOCH_REWARDS).toString()); const acc = await program.account.subDaoEpochInfoV0.fetch( subDaoEpochInfo diff --git a/tests/hexboosting.ts b/tests/hexboosting.ts index b650d53f6..2122a00b3 100644 --- a/tests/hexboosting.ts +++ b/tests/hexboosting.ts @@ -41,6 +41,8 @@ import { initTestDataCredits, } from "./utils/fixtures"; import { random } from "./utils/string"; +import { VoterStakeRegistry } from "../target/types/voter_stake_registry"; +import { init as initVsr } from "../packages/voter-stake-registry-sdk/src"; describe("hexboosting", () => { anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); @@ -54,6 +56,7 @@ describe("hexboosting", () => { "DQ4C1tzvu28cwo1roN1Wm6TW35sfJEjLh517k3ZeWevx" ); + let vsrProgram: Program; let hemProgram: Program; let hsdProgram: Program; let dcProgram: Program; @@ -74,6 +77,11 @@ describe("hexboosting", () => { pythSolanaReceiverIdl, new PublicKey("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ") ); + vsrProgram = await initVsr( + provider, + anchor.workspace.VoterStakeRegistry.programId, + anchor.workspace.VoterStakeRegistry.idl + ); dcProgram = await initDataCredits( provider, anchor.workspace.DataCredits.programId, @@ -112,6 +120,7 @@ describe("hexboosting", () => { ({ subDao, mint } = await initTestSubdao({ hsdProgram, provider, + vsrProgram, authority: me, dao, numTokens: new anchor.BN("1000000000000000"), diff --git a/tests/mobile-entity-manager.ts b/tests/mobile-entity-manager.ts index 056fd0d3d..22f982f08 100644 --- a/tests/mobile-entity-manager.ts +++ b/tests/mobile-entity-manager.ts @@ -29,13 +29,16 @@ import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, getConcurrentMerkleTreeAccountSize, } from '@solana/spl-account-compression'; +import { VoterStakeRegistry } from '../target/types/voter_stake_registry'; import { HeliumEntityManager } from '../target/types/helium_entity_manager'; +import { init as initVsr } from '../packages/voter-stake-registry-sdk/src'; chai.use(chaiAsPromised); describe('mobile-entity-manager', () => { anchor.setProvider(anchor.AnchorProvider.local("http://127.0.0.1:8899")); + let vsrProgram: Program; let dcProgram: Program; let hsdProgram: Program; let hemProgram: Program; @@ -55,6 +58,12 @@ describe('mobile-entity-manager', () => { anchor.workspace.DataCredits.idl ); + vsrProgram = await initVsr( + provider, + anchor.workspace.VoterStakeRegistry.programId, + anchor.workspace.VoterStakeRegistry.idl + ); + ensureDCIdl(dcProgram); hsdProgram = await initHeliumSubDaos( @@ -89,6 +98,7 @@ describe('mobile-entity-manager', () => { )); ({ subDao } = await initTestSubdao({ hsdProgram, + vsrProgram, provider, authority: me, dao, diff --git a/tests/utils/daos.ts b/tests/utils/daos.ts index bff54e3f7..4c948ef94 100644 --- a/tests/utils/daos.ts +++ b/tests/utils/daos.ts @@ -10,6 +10,8 @@ import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js"; import { toU128 } from "../../packages/treasury-management-sdk/src"; import { HeliumSubDaos } from "../../target/types/helium_sub_daos"; import { DC_FEE } from "./fixtures"; +import { initVsr } from "./vsr"; +import { VoterStakeRegistry } from "../../target/types/voter_stake_registry"; export async function initTestDao( program: anchor.Program, @@ -72,16 +74,26 @@ export async function initTestDao( return { mint: mint!, dao: dao! }; } -export async function initTestSubdao( - {hsdProgram, provider, authority, dao, epochRewards, registrar, numTokens, activeDeviceAuthority}: { - hsdProgram: anchor.Program, - provider: anchor.AnchorProvider, - authority: PublicKey, - dao: PublicKey, - epochRewards?: number, - registrar?: PublicKey, - numTokens?: number | BN, - activeDeviceAuthority?: PublicKey, +export async function initTestSubdao({ + hsdProgram, + vsrProgram, + provider, + authority, + dao, + epochRewards, + registrar, + numTokens, + activeDeviceAuthority, +}: { + hsdProgram: anchor.Program; + vsrProgram: anchor.Program; + provider: anchor.AnchorProvider; + authority: PublicKey; + dao: PublicKey; + epochRewards?: number; + registrar?: PublicKey; + numTokens?: number | BN; + activeDeviceAuthority?: PublicKey; }): Promise<{ mint: PublicKey; subDao: PublicKey; @@ -89,6 +101,7 @@ export async function initTestSubdao( rewardsEscrow: PublicKey; delegatorPool: PublicKey; treasuryCircuitBreaker: PublicKey; + subDaoRegistrar: PublicKey; }> { const daoAcc = await hsdProgram.account.daoV0.fetch(dao); const dntMint = await createMint(provider, 8, authority, authority); @@ -102,6 +115,17 @@ export async function initTestSubdao( provider.wallet.publicKey ); const subDao = subDaoKey(dntMint)[0]; + if (!registrar) { + ({ registrar } = await initVsr( + vsrProgram, + provider, + authority, + dntMint, + subDao, + 0, + 3 + )); + } const method = hsdProgram.methods .initializeSubDaoV0({ @@ -143,5 +167,6 @@ export async function initTestSubdao( treasury: treasury!, rewardsEscrow, delegatorPool: delegatorPool!, + subDaoRegistrar: registrar!, }; } diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index c07892246..c02dcc295 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -23,6 +23,7 @@ import { HeliumSubDaos } from "../../target/types/helium_sub_daos"; import { LazyDistributor } from "../../target/types/lazy_distributor"; import { initTestDao, initTestSubdao } from "./daos"; import { random } from "./string"; +import { PositionVotingRewards } from "../../target/types/position_voting_rewards"; // TODO: replace this with helium default uri once uploaded const DEFAULT_METADATA_URL = @@ -229,6 +230,20 @@ export async function ensureDCIdl(dcProgram: Program) { } } +export async function ensurePVRIdl(pvrProgram: Program) { + try { + execSync( + `${ANCHOR_PATH} idl init --filepath ${__dirname}/../../target/idl/position_voting_rewards.json ${pvrProgram.programId}`, + { stdio: "inherit", shell: "/bin/bash" } + ); + } catch { + execSync( + `${ANCHOR_PATH} idl upgrade --filepath ${__dirname}/../../target/idl/position_voting_rewards.json ${pvrProgram.programId}`, + { stdio: "inherit", shell: "/bin/bash" } + ); + } +} + export async function ensureMemIdl(memProgram: Program) { try { execSync( @@ -304,16 +319,17 @@ export const initWorld = async ( hemProgram: Program, hsdProgram: Program, dcProgram: Program, + vsrProgram: Program, epochRewards?: number, subDaoEpochRewards?: number, registrar?: PublicKey, hntMint?: PublicKey, - subDaoRegistrar?: PublicKey ): Promise<{ dao: { mint: PublicKey; dao: PublicKey }; subDao: { mint: PublicKey; subDao: PublicKey; + subDaoRegistrar: PublicKey; treasury: PublicKey; rewardsEscrow: PublicKey; delegatorPool: PublicKey; @@ -355,11 +371,11 @@ export const initWorld = async ( ); const subDao = await initTestSubdao({ hsdProgram, + vsrProgram, provider, authority: provider.wallet.publicKey, dao: dao.dao, epochRewards: subDaoEpochRewards, - registrar: subDaoRegistrar, // Enough to stake 4 makers numTokens: MAKER_STAKING_FEE.mul(new anchor.BN(4)) }); From 54350b647eaa24ae982cf734bc80a6b44411920e Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 11 Oct 2024 12:45:46 -0700 Subject: [PATCH 07/17] Add to end epoch scripts --- packages/crons/package.json | 1 + packages/crons/src/end-epoch.ts | 36 +++++++++++++- packages/crons/tsconfig.json | 3 ++ packages/crons/yarn.deploy.lock | 19 +++++++ packages/helium-admin-cli/package.json | 1 + packages/helium-admin-cli/src/end-epoch.ts | 58 +++++++++++++++++----- packages/helium-admin-cli/tsconfig.json | 3 ++ packages/helium-admin-cli/yarn.deploy.lock | 19 +++++++ yarn.lock | 4 +- 9 files changed, 130 insertions(+), 14 deletions(-) diff --git a/packages/crons/package.json b/packages/crons/package.json index fcab027cf..05924bd7c 100644 --- a/packages/crons/package.json +++ b/packages/crons/package.json @@ -43,6 +43,7 @@ "@helium/nft-proxy-sdk": "^0.0.12", "@helium/no-emit-sdk": "^0.9.7", "@helium/organization-sdk": "^0.0.12", + "@helium/position-voting-rewards-sdk": "^0.9.7", "@helium/price-oracle-sdk": "^0.9.7", "@helium/proposal-sdk": "^0.0.12", "@helium/rewards-oracle-sdk": "^0.9.7", diff --git a/packages/crons/src/end-epoch.ts b/packages/crons/src/end-epoch.ts index e2beea68d..56009ea31 100644 --- a/packages/crons/src/end-epoch.ts +++ b/packages/crons/src/end-epoch.ts @@ -33,7 +33,8 @@ import { truthy, } from "@helium/spl-utils"; import { getAccount } from "@solana/spl-token"; -import { ComputeBudgetProgram as CBP, Connection, Keypair, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js"; +import { init as initPVR, vsrEpochInfoKey } from "@helium/position-voting-rewards-sdk"; +import { ComputeBudgetProgram as CBP, Connection, Keypair, PublicKey, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js"; import BN from "bn.js"; import bs58 from "bs58"; @@ -63,6 +64,7 @@ async function getSolanaUnixTimestamp(connection: Connection): Promise { const errors: string[] = []; const provider = anchor.getProvider() as anchor.AnchorProvider; const heliumSubDaosProgram = await initDao(provider); + const pvrProgram = await initPVR(provider); const hntMint = HNT_MINT; const iotMint = IOT_MINT; const unixNow = new Date().valueOf() / 1000; @@ -173,6 +175,38 @@ async function getSolanaUnixTimestamp(connection: Connection): Promise { ); } } + + const hasVeTokenTracker = !subDao.account.vetokenTracker.equals(PublicKey.default); + if (hasVeTokenTracker) { + const [vsrEpoch] = vsrEpochInfoKey(subDao.account.vetokenTracker, targetTs); + const vsrEpochInfo = + await pvrProgram.account.vsrEpochInfoV0.fetchNullable( + vsrEpoch + ); + if (!vsrEpochInfo || !vsrEpochInfo.rewardsIssuedAt) { + try { + await sendInstructionsWithPriorityFee( + provider, + [ + await heliumSubDaosProgram.methods + .issueVotingRewardsV0({ epoch }) + .accounts({ + subDao: subDao.publicKey, + vsrEpochInfo: vsrEpoch, + }) + .instruction(), + ], + { + basePriorityFee: BASE_PRIORITY_FEE, + } + ); + } catch (err: any) { + errors.push( + `Failed to issue voting rewards for ${subDao.account.dntMint.toBase58()}: ${err}` + ); + } + } + } } } diff --git a/packages/crons/tsconfig.json b/packages/crons/tsconfig.json index 3159e8bfc..43988ceff 100644 --- a/packages/crons/tsconfig.json +++ b/packages/crons/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../no-emit-sdk" }, + { + "path": "../position-voting-rewards-sdk" + }, { "path": "../helium-sub-daos-sdk" }, diff --git a/packages/crons/yarn.deploy.lock b/packages/crons/yarn.deploy.lock index 152c99a2e..a62c4852e 100644 --- a/packages/crons/yarn.deploy.lock +++ b/packages/crons/yarn.deploy.lock @@ -203,6 +203,7 @@ __metadata: "@helium/nft-proxy-sdk": ^0.0.12 "@helium/no-emit-sdk": ^0.9.7 "@helium/organization-sdk": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/proposal-sdk": ^0.0.12 "@helium/rewards-oracle-sdk": ^0.9.7 @@ -446,6 +447,24 @@ __metadata: languageName: node linkType: hard +"@helium/position-voting-rewards-sdk@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.9.7 + "@helium/idls": ^0.9.7 + "@helium/spl-utils": ^0.9.7 + "@solana/spl-token": ^0.3.8 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/price-oracle-sdk@^0.9.7": version: 0.0.0-use.local resolution: "@helium/price-oracle-sdk@workspace:packages/price-oracle-sdk" diff --git a/packages/helium-admin-cli/package.json b/packages/helium-admin-cli/package.json index 7709a3398..418a8958d 100644 --- a/packages/helium-admin-cli/package.json +++ b/packages/helium-admin-cli/package.json @@ -50,6 +50,7 @@ "@helium/lazy-distributor-sdk": "^0.9.7", "@helium/mobile-entity-manager-sdk": "^0.9.7", "@helium/nft-proxy-sdk": "^0.0.12", + "@helium/position-voting-rewards-sdk": "^0.9.7", "@helium/price-oracle-sdk": "^0.9.7", "@helium/spl-utils": "^0.9.7", "@helium/treasury-management-sdk": "^0.9.7", diff --git a/packages/helium-admin-cli/src/end-epoch.ts b/packages/helium-admin-cli/src/end-epoch.ts index 6b0b0a471..180fd280e 100644 --- a/packages/helium-admin-cli/src/end-epoch.ts +++ b/packages/helium-admin-cli/src/end-epoch.ts @@ -3,7 +3,7 @@ import { daoEpochInfoKey, daoKey, EPOCH_LENGTH, - init as initDao + init as initDao, } from "@helium/helium-sub-daos-sdk"; import * as anchor from "@coral-xyz/anchor"; import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js"; @@ -12,6 +12,10 @@ import b58 from "bs58"; import os from "os"; import yargs from "yargs/yargs"; import { sendInstructionsWithPriorityFee } from "@helium/spl-utils"; +import { + init as initPVR, + vsrEpochInfoKey, +} from "@helium/position-voting-rewards-sdk"; export async function run(args: any = process.argv) { const yarg = yargs(args).options({ @@ -42,19 +46,24 @@ export async function run(args: any = process.argv) { const provider = anchor.getProvider() as anchor.AnchorProvider; const heliumSubDaosProgram = await initDao(provider); + const pvrProgram = await initPVR(provider); const hntMint = new PublicKey(argv.hntMint!); const dao = await daoKey(hntMint)[0]; - const subdaos = await heliumSubDaosProgram.account.subDaoV0.all([{ - memcmp: { - offset: 8, - bytes: b58.encode(dao.toBuffer()), - } - }]); - let targetTs = argv.from ? new BN(argv.from) : subdaos[0].account.vehntLastCalculatedTs; + const subdaos = await heliumSubDaosProgram.account.subDaoV0.all([ + { + memcmp: { + offset: 8, + bytes: b58.encode(dao.toBuffer()), + }, + }, + ]); + let targetTs = argv.from + ? new BN(argv.from) + : subdaos[0].account.vehntLastCalculatedTs; while (targetTs.toNumber() < new Date().valueOf() / 1000) { const epoch = currentEpoch(targetTs); - console.log(epoch.toNumber(), targetTs.toNumber()) + console.log(epoch.toNumber(), targetTs.toNumber()); const daoEpochInfo = await heliumSubDaosProgram.account.daoEpochInfoV0.fetchNullable( daoEpochInfoKey(dao, targetTs)[0] @@ -99,10 +108,35 @@ export async function run(args: any = process.argv) { }` ); } - } - + const hasVeTokenTracker = !subDao.account.vetokenTracker.equals( + PublicKey.default + ); + if (hasVeTokenTracker) { + const [vsrEpoch] = vsrEpochInfoKey( + subDao.account.vetokenTracker, + targetTs + ); + const vsrEpochInfo = + await pvrProgram.account.vsrEpochInfoV0.fetchNullable(vsrEpoch); + if (!vsrEpochInfo || !vsrEpochInfo.rewardsIssuedAt) { + try { + await sendInstructionsWithPriorityFee(provider, [ + await heliumSubDaosProgram.methods + .issueVotingRewardsV0({ epoch }) + .accounts({ subDao: subDao.publicKey, vsrEpochInfo: vsrEpoch }) + .instruction(), + ]); + } catch (err: any) { + console.log( + `Failed to issue voting rewards for ${subDao.account.dntMint.toBase58()}: ${err}` + ); + } + } + } + } } + try { if (!daoEpochInfo?.doneIssuingHstPool) { await sendInstructionsWithPriorityFee(provider, [ @@ -115,7 +149,7 @@ export async function run(args: any = process.argv) { } catch (e: any) { console.log(`Failed to issue hst pool: ${e.message}`); } - + targetTs = targetTs.add(new BN(EPOCH_LENGTH)); } } diff --git a/packages/helium-admin-cli/tsconfig.json b/packages/helium-admin-cli/tsconfig.json index 3c77a27bd..624442644 100644 --- a/packages/helium-admin-cli/tsconfig.json +++ b/packages/helium-admin-cli/tsconfig.json @@ -16,6 +16,9 @@ { "path": "../data-credits-sdk" }, + { + "path": "../position-voting-rewards-sdk" + }, { "path": "../circuit-breaker-sdk" }, diff --git a/packages/helium-admin-cli/yarn.deploy.lock b/packages/helium-admin-cli/yarn.deploy.lock index 0de253a8b..6e8bd7cfe 100644 --- a/packages/helium-admin-cli/yarn.deploy.lock +++ b/packages/helium-admin-cli/yarn.deploy.lock @@ -326,6 +326,7 @@ __metadata: "@helium/lazy-distributor-sdk": ^0.9.7 "@helium/mobile-entity-manager-sdk": ^0.9.7 "@helium/nft-proxy-sdk": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 "@helium/treasury-management-sdk": ^0.9.7 @@ -488,6 +489,24 @@ __metadata: languageName: unknown linkType: soft +"@helium/position-voting-rewards-sdk@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.9.7 + "@helium/idls": ^0.9.7 + "@helium/spl-utils": ^0.9.7 + "@solana/spl-token": ^0.3.8 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/price-oracle-sdk@^0.9.7": version: 0.0.0-use.local resolution: "@helium/price-oracle-sdk@workspace:packages/price-oracle-sdk" diff --git a/yarn.lock b/yarn.lock index 40805246e..273b2d2fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -679,6 +679,7 @@ __metadata: "@helium/nft-proxy-sdk": ^0.0.12 "@helium/no-emit-sdk": ^0.9.7 "@helium/organization-sdk": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/proposal-sdk": ^0.0.12 "@helium/rewards-oracle-sdk": ^0.9.7 @@ -912,6 +913,7 @@ __metadata: "@helium/lazy-distributor-sdk": ^0.9.7 "@helium/mobile-entity-manager-sdk": ^0.9.7 "@helium/nft-proxy-sdk": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 "@helium/treasury-management-sdk": ^0.9.7 @@ -1362,7 +1364,7 @@ __metadata: languageName: node linkType: hard -"@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk": +"@helium/position-voting-rewards-sdk@^0.9.7, @helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk": version: 0.0.0-use.local resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" dependencies: From da038ab9e7509457d76b23e648fbd1c3368968ca Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 11 Oct 2024 15:13:47 -0700 Subject: [PATCH 08/17] WIP: Need to get proposal tracking to work --- .../src/instructions/claim_rewards_v0.rs | 2 +- .../initialize_vetoken_tracker_v0.rs | 3 + .../src/instructions/track_vote_v0.rs | 25 ++-- programs/position-voting-rewards/src/state.rs | 2 + tests/helium-sub-daos.ts | 1 + tests/position-voting-rewards.ts | 115 ++++++++++++++++++ 6 files changed, 138 insertions(+), 10 deletions(-) diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs index d16393d93..bdfb6f1ea 100644 --- a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -189,7 +189,7 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result amount, )?; } else { - msg!("Position is not eligible, burning"); + msg!("Position is not eligible, burning. Position proposals {:?}, recent proposals {:?}"); burn( ctx .accounts diff --git a/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs index 927ddedf8..6a592b892 100644 --- a/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs +++ b/programs/position-voting-rewards/src/instructions/initialize_vetoken_tracker_v0.rs @@ -18,6 +18,8 @@ pub struct InitializeVeTokenTrackerV0<'info> { bump, )] pub vetoken_tracker: Account<'info, VeTokenTrackerV0>, + /// CHECK: Check not needed + pub proposal_namespace: UncheckedAccount<'info>, #[account( has_one = realm_authority, )] @@ -31,6 +33,7 @@ pub struct InitializeVeTokenTrackerV0<'info> { pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.vetoken_tracker.set_inner(VeTokenTrackerV0 { + proposal_namespace: ctx.accounts.proposal_namespace.key(), registrar: ctx.accounts.registrar.key(), rewards_authority: ctx.accounts.rewards_authority.key(), rewards_mint: ctx.accounts.rewards_mint.key(), diff --git a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs index a4e5bbc6a..3847dbfdc 100644 --- a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs +++ b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::Mint; use proposal::ProposalV0; use voter_stake_registry::{ - state::{PositionV0, VoteMarkerV0}, + state::{PositionV0, Registrar, VoteMarkerV0}, VoterStakeRegistry, }; @@ -13,13 +13,14 @@ use crate::{ #[derive(Accounts)] pub struct TrackVoteV0<'info> { - // Proves that the call is coming from vsrr - pub registrar: Signer<'info>, + #[account( + constraint = proposal.namespace == vetoken_tracker.proposal_namespace + )] pub proposal: Account<'info, ProposalV0>, #[account( mut, - has_one = registrar, - has_one = mint + has_one = mint, + constraint = position.registrar == vetoken_tracker.registrar )] pub position: Box>, pub mint: Box>, @@ -30,22 +31,28 @@ pub struct TrackVoteV0<'info> { seeds::program = vsr_program )] pub marker: UncheckedAccount<'info>, - #[account(mut, has_one = registrar)] + #[account(mut)] pub vetoken_tracker: Box>, - #[account(mut, has_one = vetoken_tracker)] + #[account(mut, + has_one = vetoken_tracker, + seeds = ["enrolled_position".as_bytes(), position.key().as_ref()], + bump = enrolled_position.bump_seed, + )] pub enrolled_position: Box>, + /// TODO: Add vsr epoch info here and update the recent proposals pub vsr_program: Program<'info, VoterStakeRegistry>, } pub fn handler(ctx: Context) -> Result<()> { let data = ctx.accounts.marker.data.try_borrow().unwrap(); let has_data = !data.is_empty(); + drop(data); let mut voted = has_data; if has_data { - let marker = VoteMarkerV0::try_from_slice(&data)?; + let marker: Account = Account::try_from(&ctx.accounts.marker.to_account_info())?; require_eq!( marker.registrar, - ctx.accounts.registrar.key(), + ctx.accounts.position.registrar, ErrorCode::InvalidMarker ); voted = !marker.choices.is_empty(); diff --git a/programs/position-voting-rewards/src/state.rs b/programs/position-voting-rewards/src/state.rs index 5292f3b9c..6c780693e 100644 --- a/programs/position-voting-rewards/src/state.rs +++ b/programs/position-voting-rewards/src/state.rs @@ -138,6 +138,8 @@ impl VsrEpochInfoV0 { #[derive(InitSpace)] pub struct VeTokenTrackerV0 { pub registrar: Pubkey, + /// The org to track proposals from + pub proposal_namespace: Pubkey, pub rewards_mint: Pubkey, pub rewards_authority: Pubkey, pub vetoken_last_calculated_ts: i64, diff --git a/tests/helium-sub-daos.ts b/tests/helium-sub-daos.ts index 04d8763d7..2777b1771 100644 --- a/tests/helium-sub-daos.ts +++ b/tests/helium-sub-daos.ts @@ -267,6 +267,7 @@ describe("helium-sub-daos", () => { .initializeVetokenTrackerV0() .accounts({ registrar: subDaoRegistrar, + proposalNamespace: me, rewardsMint: dntMint, payer: me, rewardsAuthority: subDao, diff --git a/tests/position-voting-rewards.ts b/tests/position-voting-rewards.ts index 04c02c281..842ab5069 100644 --- a/tests/position-voting-rewards.ts +++ b/tests/position-voting-rewards.ts @@ -14,6 +14,9 @@ import chaiAsPromised from "chai-as-promised"; import { expectBnAccuracy } from "./utils/expectBnAccuracy"; import { getUnixTimestamp } from "./utils/solana"; import { AccountLayout } from "@solana/spl-token"; +import { Proposal } from "@helium/modular-governance-idls/lib/types/proposal"; +import { init as initProposal } from "@helium/proposal-sdk"; +import { random } from "./utils/string"; chai.use(chaiAsPromised); @@ -37,6 +40,7 @@ describe("position-voting-rewards", () => { } ); + let proposalProgram: Program; let vsrProgram: Program; let registrar: PublicKey; let genesisVotePowerMultiplierExpirationTs = 1; @@ -51,6 +55,7 @@ describe("position-voting-rewards", () => { hntMint = await createMint(provider, 8, me, me); await createAtaAndMint(provider, hntMint, toBN(REWARDS, 8), me); await createAtaAndMint(provider, hntMint, toBN(REWARDS, 8), positionAuthorityKp.publicKey); + proposalProgram = await initProposal(provider); vsrProgram = await vsrInit( provider, anchor.workspace.VoterStakeRegistry.programId, @@ -147,6 +152,7 @@ describe("position-voting-rewards", () => { rewardsMint: hntMint, payer: me, rewardsAuthority: me, + proposalNamespace: me, }).rpcAndKeys({ skipPreflight: true }); vetokenTracker = tracker! as PublicKey; @@ -455,6 +461,115 @@ describe("position-voting-rewards", () => { ).amount; expect(Number(postAtaBalance)).to.be.within(0, 5000); }); + + it("claim rewards after voting on two proposals", async () => { + // Create and vote on two proposals + const { + pubkeys: { proposalConfig }, + } = await proposalProgram.methods + .initializeProposalConfigV0({ + name: random(10), + voteController: registrar, + stateController: me, + onVoteHook: PublicKey.default, + authority: me, + }) + .rpcAndKeys({ skipPreflight: true }); + for (let i = 0; i < 2; i++) { + const proposalName = `Proposal ${random(10)}`; + const { + pubkeys: { proposal }, + } = await proposalProgram.methods + .initializeProposalV0({ + seed: Buffer.from(proposalName, "utf-8"), + maxChoicesPerVoter: 1, + name: proposalName, + uri: "https://example.com", + choices: [ + { name: "Yes", uri: null }, + { name: "No", uri: null }, + ], + tags: ["test"], + }) + .accounts({ proposalConfig }) + .rpcAndKeys({ skipPreflight: true }); + + await proposalProgram.methods + .updateStateV0({ + newState: { + voting: { + startTs: new anchor.BN(new Date().valueOf() / 1000), + } as any, + }, + }) + .accounts({ proposal }) + .rpc({ skipPreflight: true }); + + const { + pubkeys: { marker }, + } = await vsrProgram.methods + .voteV0({ + choice: 0, + }) + .accounts({ + position, + proposal: proposal as PublicKey, + voter: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]) + .rpcAndKeys({ skipPreflight: true }); + + await program.methods + .trackVoteV0() + .accounts({ + marker: marker as PublicKey, + vetokenTracker, + proposal: proposal as PublicKey, + position, + }) + .rpc({ skipPreflight: true }); + } + + // Issue rewards + await program.methods + .rewardForEpochV0({ + epoch, + amount: toBN(REWARDS, 8), + }) + .accounts({ + vetokenTracker, + registrar, + vsrEpochInfo, + rewardsPayer: me, + }) + .rpc({ skipPreflight: true }); + + + // Claim rewards + const method = program.methods + .claimRewardsV0({ + epoch, + }) + .accounts({ + position, + vetokenTracker, + positionAuthority: positionAuthorityKp.publicKey, + }) + .signers([positionAuthorityKp]); + const { enrolledAta } = await method.pubkeys(); + const preAtaBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(enrolledAta!))?.data! + ).amount; + await method.rpc({ skipPreflight: true }); + + const postAtaBalance = AccountLayout.decode( + (await provider.connection.getAccountInfo(enrolledAta!))?.data! + ).amount; + expect(Number(preAtaBalance) - Number(postAtaBalance)).to.be.within( + REWARDS - 5000, + REWARDS + ); + }); }); }); }); From e1fccaee2887686e756ed961f5ead28d8a2b721c Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 14 Oct 2024 15:01:01 -0700 Subject: [PATCH 09/17] Fix functionality for 2/4 proposals --- .../src/instructions/claim_rewards_v0.rs | 11 ++++-- .../src/instructions/enroll_v0.rs | 2 ++ .../src/instructions/track_vote_v0.rs | 36 +++++++++++++++++-- .../src/instructions/unenroll_v0.rs | 1 + programs/position-voting-rewards/src/state.rs | 29 ++++++++++++++- .../instructions/initialize_registrar_v0.rs | 1 - .../src/state/registrar.rs | 1 - tests/position-voting-rewards.ts | 12 +++---- 8 files changed, 79 insertions(+), 14 deletions(-) diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs index bdfb6f1ea..1676a449a 100644 --- a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -146,14 +146,14 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result .accounts .vsr_epoch_info .recent_proposals - .first() + .last() .unwrap() .ts; let last_ts = ctx .accounts .vsr_epoch_info .recent_proposals - .last() + .first() .unwrap() .ts; ctx @@ -177,6 +177,7 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result .iter() .filter(|&proposal| proposal_set.contains(&proposal.proposal)) .count(); + if eligible_count >= 2 { msg!("Position is eligible, transferring"); transfer( @@ -189,7 +190,11 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result amount, )?; } else { - msg!("Position is not eligible, burning. Position proposals {:?}, recent proposals {:?}"); + msg!( + "Position is not eligible, burning. Position proposals {:?}, recent proposals {:?}", + ctx.accounts.enrolled_position.recent_proposals, + ctx.accounts.vsr_epoch_info.recent_proposals + ); burn( ctx .accounts diff --git a/programs/position-voting-rewards/src/instructions/enroll_v0.rs b/programs/position-voting-rewards/src/instructions/enroll_v0.rs index 4c204a8b0..d83e93af8 100644 --- a/programs/position-voting-rewards/src/instructions/enroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/enroll_v0.rs @@ -180,6 +180,7 @@ pub fn handler(ctx: Context) -> Result<()> { .checked_add(end_vetoken_correction) .unwrap(); ctx.accounts.closing_time_vsr_epoch_info.vetoken_tracker = vetoken_tracker.key(); + ctx.accounts.closing_time_vsr_epoch_info.registrar = ctx.accounts.registrar.key(); ctx.accounts.closing_time_vsr_epoch_info.epoch = current_epoch(position.lockup.end_ts); ctx.accounts.closing_time_vsr_epoch_info.bump_seed = ctx.bumps["closing_time_vsr_epoch_info"]; @@ -264,6 +265,7 @@ pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); + ctx.accounts.vsr_epoch_info.registrar = ctx.accounts.registrar.key(); freeze_position_v0(CpiContext::new_with_signer( ctx.accounts.vsr_program.to_account_info(), diff --git a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs index 3847dbfdc..aa50ea2e1 100644 --- a/programs/position-voting-rewards/src/instructions/track_vote_v0.rs +++ b/programs/position-voting-rewards/src/instructions/track_vote_v0.rs @@ -8,18 +8,23 @@ use voter_stake_registry::{ use crate::{ error::ErrorCode, - state::{EnrolledPositionV0, VeTokenTrackerV0}, + state::{EnrolledPositionV0, VeTokenTrackerV0, VsrEpochInfoV0}, + util::current_epoch, }; #[derive(Accounts)] pub struct TrackVoteV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, #[account( constraint = proposal.namespace == vetoken_tracker.proposal_namespace )] pub proposal: Account<'info, ProposalV0>, + pub registrar: Box>, #[account( mut, has_one = mint, + has_one = registrar, constraint = position.registrar == vetoken_tracker.registrar )] pub position: Box>, @@ -39,11 +44,34 @@ pub struct TrackVoteV0<'info> { bump = enrolled_position.bump_seed, )] pub enrolled_position: Box>, - /// TODO: Add vsr epoch info here and update the recent proposals + #[account( + init_if_needed, + payer = payer, + space = 60 + VsrEpochInfoV0::INIT_SPACE, + seeds = ["vsr_epoch_info".as_bytes(), vetoken_tracker.key().as_ref(), ¤t_epoch(registrar.clock_unix_timestamp()).to_le_bytes()], + bump, + )] + pub vsr_epoch_info: Account<'info, VsrEpochInfoV0>, pub vsr_program: Program<'info, VoterStakeRegistry>, + pub system_program: Program<'info, System>, } pub fn handler(ctx: Context) -> Result<()> { + ctx.accounts.vsr_epoch_info.epoch = current_epoch(ctx.accounts.registrar.clock_unix_timestamp()); + ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + ctx.accounts.vsr_epoch_info.registrar = ctx.accounts.registrar.key(); + ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); + + ctx.accounts.vetoken_tracker.add_recent_proposal( + ctx.accounts.proposal.key(), + ctx.accounts.proposal.created_at, + ); + ctx.accounts.vsr_epoch_info.recent_proposals = + ctx.accounts.vetoken_tracker.recent_proposals.clone(); + ctx.accounts.vetoken_tracker.update_vetokens( + &mut ctx.accounts.vsr_epoch_info, + ctx.accounts.registrar.clock_unix_timestamp(), + )?; let data = ctx.accounts.marker.data.try_borrow().unwrap(); let has_data = !data.is_empty(); drop(data); @@ -62,6 +90,10 @@ pub fn handler(ctx: Context) -> Result<()> { ctx.accounts.proposal.key(), ctx.accounts.proposal.created_at, ); + msg!( + "Proposals are now {:?}", + ctx.accounts.enrolled_position.recent_proposals + ); } else { ctx .accounts diff --git a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs index 2abf385dc..44b81a7bd 100644 --- a/programs/position-voting-rewards/src/instructions/unenroll_v0.rs +++ b/programs/position-voting-rewards/src/instructions/unenroll_v0.rs @@ -224,6 +224,7 @@ pub fn handler(ctx: Context) -> Result<()> { } ctx.accounts.vsr_epoch_info.vetoken_tracker = ctx.accounts.vetoken_tracker.key(); + ctx.accounts.vsr_epoch_info.registrar = ctx.accounts.registrar.key(); ctx.accounts.vsr_epoch_info.bump_seed = *ctx.bumps.get("vsr_epoch_info").unwrap(); // EDGE CASE: When the closing time epoch infos are the same as the current epoch info, diff --git a/programs/position-voting-rewards/src/state.rs b/programs/position-voting-rewards/src/state.rs index 6c780693e..16c693f5b 100644 --- a/programs/position-voting-rewards/src/state.rs +++ b/programs/position-voting-rewards/src/state.rs @@ -22,7 +22,7 @@ pub struct EnrolledPositionV0 { pub recent_proposals: Vec, } -#[derive(InitSpace, Default, Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Debug, InitSpace, Default, Clone, AnchorSerialize, AnchorDeserialize)] pub struct RecentProposal { pub proposal: Pubkey, pub ts: i64, @@ -161,6 +161,33 @@ macro_rules! vetoken_tracker_seeds { } impl VeTokenTrackerV0 { + pub fn add_recent_proposal(&mut self, proposal: Pubkey, ts: i64) { + let new_proposal = RecentProposal { proposal, ts }; + + // Find the insertion point to maintain descending order by timestamp + let insert_index = self + .recent_proposals + .iter() + .position(|p| p.ts <= ts) + .unwrap_or(self.recent_proposals.len()); + + let cloned_proposals = self.recent_proposals.clone(); + + // Shift elements to make room for the new proposal + if insert_index < self.recent_proposals.len() { + for i in (insert_index + 1..self.recent_proposals.len()).rev() { + self.recent_proposals[i] = cloned_proposals[i - 1].clone(); + } + self.recent_proposals[insert_index] = new_proposal; + } else if ts > self.recent_proposals[self.recent_proposals.len() - 1].ts { + // If the new proposal is more recent than the oldest one, replace the oldest + self.recent_proposals[self.recent_proposals.len() - 1] = new_proposal; + } + + // Re-sort the array to ensure it's in descending order by timestamp + self.recent_proposals.sort_by(|a, b| b.ts.cmp(&a.ts)); + } + pub fn update_vetokens( &mut self, curr_epoch_info: &mut VsrEpochInfoV0, diff --git a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs index 6945d02d6..f4ac9a3e4 100644 --- a/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs +++ b/programs/voter-stake-registry/src/instructions/initialize_registrar_v0.rs @@ -193,7 +193,6 @@ pub fn handler(ctx: Context, args: InitializeRegistrarArg collection_bump_seed: ctx.bumps["collection"], reserved1: [0; 4], reserved2: [0; 3], - recent_proposals: [Pubkey::default(); 4], position_freeze_authorities: args.position_freeze_authorities, voting_mints: Vec::new(), proxy_config: ctx diff --git a/programs/voter-stake-registry/src/state/registrar.rs b/programs/voter-stake-registry/src/state/registrar.rs index 305f361b5..3cae697c2 100644 --- a/programs/voter-stake-registry/src/state/registrar.rs +++ b/programs/voter-stake-registry/src/state/registrar.rs @@ -24,7 +24,6 @@ pub struct Registrar { pub reserved2: [u64; 3], // split because `Default` does not support [u8; 60] pub proxy_config: Pubkey, pub voting_mints: Vec, - pub recent_proposals: [Pubkey; 4], pub position_freeze_authorities: Vec, } diff --git a/tests/position-voting-rewards.ts b/tests/position-voting-rewards.ts index 842ab5069..28f7d0a5d 100644 --- a/tests/position-voting-rewards.ts +++ b/tests/position-voting-rewards.ts @@ -237,6 +237,7 @@ describe("position-voting-rewards", () => { describe("with enrolled vehnt", () => { beforeEach(async () => { + await program.methods .enrollV0() .accounts({ @@ -519,7 +520,7 @@ describe("position-voting-rewards", () => { .signers([positionAuthorityKp]) .rpcAndKeys({ skipPreflight: true }); - await program.methods + console.log("track", await program.methods .trackVoteV0() .accounts({ marker: marker as PublicKey, @@ -527,7 +528,7 @@ describe("position-voting-rewards", () => { proposal: proposal as PublicKey, position, }) - .rpc({ skipPreflight: true }); + .rpc({ skipPreflight: true })); } // Issue rewards @@ -565,10 +566,9 @@ describe("position-voting-rewards", () => { const postAtaBalance = AccountLayout.decode( (await provider.connection.getAccountInfo(enrolledAta!))?.data! ).amount; - expect(Number(preAtaBalance) - Number(postAtaBalance)).to.be.within( - REWARDS - 5000, - REWARDS - ); + expect( + Number(postAtaBalance) - Number(preAtaBalance) + ).to.be.within(toBN(REWARDS, 8).toNumber() - 5000, toBN(REWARDS, 8).toNumber()); }); }); }); From 6f4864b106b0a17bcf3c85d32e9c2bd04f7960ea Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Thu, 17 Oct 2024 15:22:25 -0700 Subject: [PATCH 10/17] WIP: Implement hooks for enrollment --- packages/helium-admin-cli/package.json | 1 + .../src/initialize-vetoken-tracking.ts | 151 ++++++++++++++++++ packages/helium-admin-cli/yarn.deploy.lock | 34 ++++ .../position-voting-rewards-sdk/src/pdas.ts | 4 +- packages/spl-utils/src/constants.ts | 6 +- .../voter-stake-registry-hooks/package.json | 1 + .../src/contexts/heliumVsrContext.tsx | 51 +++++- .../src/hooks/useClaimAllPositionsRewards.ts | 2 +- .../src/hooks/useClaimPositionRewards.ts | 2 +- .../src/hooks/useClosePosition.ts | 19 +++ .../src/hooks/useCreatePosition.ts | 2 +- .../src/hooks/useDao.ts | 6 + .../src/hooks/useEnrolledPositions.ts | 12 ++ .../src/hooks/useExtendPosition.ts | 19 +++ .../src/hooks/useFlipPositionLockupKind.ts | 21 +++ .../src/hooks/useRelinquishVote.ts | 84 ++++++---- .../src/hooks/useSplitPosition.ts | 23 ++- .../src/hooks/useSubDao.ts | 6 + .../src/hooks/useTransferPosition.ts | 33 +++- .../src/hooks/useVote.ts | 113 +++++++++---- .../voter-stake-registry-hooks/src/index.ts | 3 + .../src/sdk/types.ts | 20 +-- .../voter-stake-registry-hooks/tsconfig.json | 3 + .../yarn.deploy.lock | 19 +++ scripts/init-idls.sh | 2 +- scripts/upgrade-idls.sh | 2 +- yarn.lock | 35 ++++ 27 files changed, 583 insertions(+), 91 deletions(-) create mode 100644 packages/helium-admin-cli/src/initialize-vetoken-tracking.ts create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useDao.ts create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useEnrolledPositions.ts create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useSubDao.ts diff --git a/packages/helium-admin-cli/package.json b/packages/helium-admin-cli/package.json index 418a8958d..e1bc7068f 100644 --- a/packages/helium-admin-cli/package.json +++ b/packages/helium-admin-cli/package.json @@ -50,6 +50,7 @@ "@helium/lazy-distributor-sdk": "^0.9.7", "@helium/mobile-entity-manager-sdk": "^0.9.7", "@helium/nft-proxy-sdk": "^0.0.12", + "@helium/organization-sdk": "^0.0.13", "@helium/position-voting-rewards-sdk": "^0.9.7", "@helium/price-oracle-sdk": "^0.9.7", "@helium/spl-utils": "^0.9.7", diff --git a/packages/helium-admin-cli/src/initialize-vetoken-tracking.ts b/packages/helium-admin-cli/src/initialize-vetoken-tracking.ts new file mode 100644 index 000000000..19ae1ad1e --- /dev/null +++ b/packages/helium-admin-cli/src/initialize-vetoken-tracking.ts @@ -0,0 +1,151 @@ +import * as anchor from "@coral-xyz/anchor"; +import { + delegatorRewardsPercent, + init as initHsd, + subDaoKey, +} from "@helium/helium-sub-daos-sdk"; +import { organizationKey } from "@helium/organization-sdk"; +import { init as initPvr } from "@helium/position-voting-rewards-sdk"; +import { init as initVsr } from "@helium/voter-stake-registry-sdk" +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import Squads from "@sqds/sdk"; +import os from "os"; +import yargs from "yargs/yargs"; +import { + loadKeypair, + sendInstructionsOrSquads +} from "./utils"; + +export async function run(args: any = process.argv) { + const yarg = yargs(args).options({ + wallet: { + alias: "k", + describe: "Anchor wallet keypair", + default: `${os.homedir()}/.config/solana/id.json`, + }, + url: { + alias: "u", + default: "http://127.0.0.1:8899", + describe: "The solana url", + }, + dntMint: { + required: true, + type: "string", + describe: "DNT mint of the subdao to be updated", + }, + executeTransaction: { + type: "boolean", + }, + multisig: { + type: "string", + describe: + "Address of the squads multisig to be authority. If not provided, your wallet will be the authority", + }, + authorityIndex: { + type: "number", + describe: "Authority index for squads. Defaults to 1", + default: 1, + }, + votingRewardsPercent: { + type: "number", + describe: "Voting rewards percent", + required: true, + }, + orgName: { + type: "string", + describe: "Organization name", + required: true, + }, + }); + const argv = await yarg.argv; + process.env.ANCHOR_WALLET = argv.wallet; + process.env.ANCHOR_PROVIDER_URL = argv.url; + anchor.setProvider(anchor.AnchorProvider.local(argv.url)); + const provider = anchor.getProvider() as anchor.AnchorProvider; + const walletKP = loadKeypair(argv.wallet); + const wallet = new anchor.Wallet(walletKP); + const program = await initHsd(provider); + const pvrProgram = await initPvr(provider); + const vsrProgram = await initVsr(provider); + + const instructions: TransactionInstruction[] = []; + + const dntMint = new PublicKey(argv.dntMint); + const subDao = subDaoKey(dntMint)[0]; + const subDaoAcc = await program.account.subDaoV0.fetch(subDao); + + let { + instruction, + pubkeys: { vetokenTracker }, + } = await pvrProgram.methods + .initializeVetokenTrackerV0() + .accounts({ + registrar: subDaoAcc.registrar, + proposalNamespace: organizationKey(argv.orgName)[0], + rewardsMint: dntMint, + payer: wallet.publicKey, + rewardsAuthority: subDao, + }) + .prepare(); + + if (!await provider.connection.getAccountInfo(vetokenTracker!)) { + instructions.push(instruction); + } + + instructions.push( + await program.methods + .updateSubDaoV0({ + vetokenTracker: vetokenTracker!, + votingRewardsPercent: delegatorRewardsPercent(argv.votingRewardsPercent), + authority: null, + emissionSchedule: null, + dcBurnAuthority: null, + onboardingDcFee: null, + onboardingDataOnlyDcFee: null, + registrar: null, + delegatorRewardsPercent: null, + activeDeviceAuthority: null, + }) + .accounts({ + subDao, + authority: subDaoAcc.authority, + payer: subDaoAcc.authority, + }) + .instruction() + ); + + const registrarK = subDaoAcc.registrar + const registrar = await vsrProgram.account.registrar.fetch(registrarK) + if ( + !registrar.positionFreezeAuthorities.some((f) => f.equals(vetokenTracker!)) + ) { + instructions.push( + await vsrProgram.methods + .updateRegistrarV0({ + positionFreezeAuthorities: [ + ...registrar.positionFreezeAuthorities, + vetokenTracker!, + ], + positionUpdateAuthority: registrar.positionUpdateAuthority, + }) + .accounts({ + registrar: registrarK, + proxyConfig: registrar.proxyConfig, + }) + .instruction() + ); + } + + const squads = Squads.endpoint(process.env.ANCHOR_PROVIDER_URL, wallet, { + commitmentOrConfig: "finalized", + }); + await sendInstructionsOrSquads({ + provider, + instructions, + executeTransaction: argv.executeTransaction, + squads, + multisig: argv.multisig ? new PublicKey(argv.multisig) : undefined, + authorityIndex: argv.authorityIndex, + signers: [], + }); +} diff --git a/packages/helium-admin-cli/yarn.deploy.lock b/packages/helium-admin-cli/yarn.deploy.lock index 6e8bd7cfe..c4502516b 100644 --- a/packages/helium-admin-cli/yarn.deploy.lock +++ b/packages/helium-admin-cli/yarn.deploy.lock @@ -326,6 +326,7 @@ __metadata: "@helium/lazy-distributor-sdk": ^0.9.7 "@helium/mobile-entity-manager-sdk": ^0.9.7 "@helium/nft-proxy-sdk": ^0.0.12 + "@helium/organization-sdk": ^0.0.13 "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 @@ -461,6 +462,16 @@ __metadata: languageName: node linkType: hard +"@helium/modular-governance-idls@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/modular-governance-idls@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@solana/web3.js": ^1.78.4 + checksum: d210bdbaf3d56c88e96a1cbb4066277629a34da33aff7f49323a376a3df68a9f1a8382edb02a97c61e2336aaec32a8eb0f11699ea1e3e7a07c542d4d3b5edd8f + languageName: node + linkType: hard + "@helium/nft-proxy-sdk@npm:^0.0.12": version: 0.0.12 resolution: "@helium/nft-proxy-sdk@npm:0.0.12" @@ -489,6 +500,18 @@ __metadata: languageName: unknown linkType: soft +"@helium/organization-sdk@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/organization-sdk@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.5.0 + "@helium/modular-governance-idls": ^0.0.13 + "@helium/proposal-sdk": ^0.0.13 + checksum: 771d9b5d768d64276ca5b78911fc265434aee98c7b5d3da535fa6a55e24aa77523177e2d24112c0e83f9b0017d5a67c4d5d0f5315c13979ecc5ddf42662055a4 + languageName: node + linkType: hard + "@helium/position-voting-rewards-sdk@^0.9.7": version: 0.0.0-use.local resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" @@ -522,6 +545,17 @@ __metadata: languageName: unknown linkType: soft +"@helium/proposal-sdk@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/proposal-sdk@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.5.0 + "@helium/modular-governance-idls": ^0.0.13 + checksum: 70b8c02906f0361b1c32308b09492fb5c6b847aceee922c6b1a5b2bfca8eb2f425fa42782a51cb993157b338f7422229c1694a48e6fdfe41040e2ad5b646bff6 + languageName: node + linkType: hard + "@helium/rewards-oracle-sdk@^0.9.7": version: 0.0.0-use.local resolution: "@helium/rewards-oracle-sdk@workspace:packages/rewards-oracle-sdk" diff --git a/packages/position-voting-rewards-sdk/src/pdas.ts b/packages/position-voting-rewards-sdk/src/pdas.ts index 75f99b3c2..afec626d1 100644 --- a/packages/position-voting-rewards-sdk/src/pdas.ts +++ b/packages/position-voting-rewards-sdk/src/pdas.ts @@ -2,12 +2,12 @@ import { PublicKey } from "@solana/web3.js"; import { PROGRAM_ID, currentEpoch } from "./constants"; import BN from "bn.js"; -export const vsrTrackerKey = ( +export const vetokenTrackerKey = ( registrar: PublicKey, programId: PublicKey = PROGRAM_ID ) => PublicKey.findProgramAddressSync( - [Buffer.from("vsr_tracker", "utf-8"), registrar.toBuffer()], + [Buffer.from("vetoken_tracker", "utf-8"), registrar.toBuffer()], programId ); diff --git a/packages/spl-utils/src/constants.ts b/packages/spl-utils/src/constants.ts index ac194f25d..a9d3e84aa 100644 --- a/packages/spl-utils/src/constants.ts +++ b/packages/spl-utils/src/constants.ts @@ -4,13 +4,13 @@ export const DC_MINT = new PublicKey( "dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm" ); -export const HNT_MINT = new PublicKey("hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux"); +export const HNT_MINT = new PublicKey("APqAVo5q9erS8GaXcbJuy3Gx4ikuSzXjzY4SnyppPUm1"); export const MOBILE_MINT = new PublicKey( - "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6" + "CoQciaEADT77zudZQm4atzWjHmHcJKfzZBivbifHScvZ" ); -export const IOT_MINT = new PublicKey("iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns"); +export const IOT_MINT = new PublicKey("g69eEQjY4bv1UvjaeTUcaCNoJ6v8jSsjL3dojN6uC4B"); // TODO: Replace with actual HNT feed export const HNT_PYTH_PRICE_FEED = new PublicKey( diff --git a/packages/voter-stake-registry-hooks/package.json b/packages/voter-stake-registry-hooks/package.json index aac61e625..a4a9f9b94 100644 --- a/packages/voter-stake-registry-hooks/package.json +++ b/packages/voter-stake-registry-hooks/package.json @@ -39,6 +39,7 @@ "@helium/helium-sub-daos-sdk": "^0.9.7", "@helium/modular-governance-hooks": "^0.0.12", "@helium/modular-governance-idls": "^0.0.12", + "@helium/position-voting-rewards-sdk": "^0.9.7", "@helium/spl-utils": "^0.9.7", "@helium/voter-stake-registry-sdk": "^0.9.7", "@solana/wallet-adapter-base": "^0.9.22", diff --git a/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx b/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx index 1aa3570d4..098cc7c72 100644 --- a/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx +++ b/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx @@ -2,7 +2,9 @@ import { AnchorProvider, BN, IdlAccounts, Wallet } from "@coral-xyz/anchor"; import { useSolanaUnixNow } from "@helium/helium-react-hooks"; import { EPOCH_LENGTH, + daoKey, delegatedPositionKey, + subDaoKey, } from "@helium/helium-sub-daos-sdk"; import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; import { init as initNftProxy } from "@helium/nft-proxy-sdk"; @@ -21,6 +23,10 @@ import { usePositions } from "../hooks/usePositions"; import { useRegistrar } from "../hooks/useRegistrar"; import { PositionWithMeta, ProxyAssignmentV0 } from "../sdk/types"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; +import { enrolledPositionKey } from "@helium/position-voting-rewards-sdk"; +import { useEnrolledPositions } from "../hooks/useEnrolledPositions"; +import { useSubDao } from "../hooks/useSubDao"; +import { useDao } from "../hooks/useDao"; type Registrar = IdlAccounts["registrar"]; @@ -84,9 +90,22 @@ export const HeliumVsrStateProvider: React.FC<{ } }, [connection?.rpcEndpoint, wallet?.publicKey?.toBase58()]); + const daoK = useMemo(() => { + if (mint) { + return daoKey(mint); + } + }, [mint?.toBase58()]); + const subDaoK = useMemo(() => { + if (mint) { + return subDaoKey(mint)[0]; + } + }, [mint?.toBase58()]); + const { info: subDao } = useSubDao(subDaoK); + const { info: dao } = useDao(subDaoK); + const registrarKey = useMemo( - () => mint && getRegistrarKey(mint), - [mint?.toBase58()] + () => subDao && subDao.registrar || dao && dao.registrar, + [subDao?.registrar, dao?.registrar] ); const { info: registrar } = useRegistrar(registrarKey); @@ -137,9 +156,17 @@ export const HeliumVsrStateProvider: React.FC<{ return positionKeys?.map((pk) => delegatedPositionKey(pk)[0]); }, [positionKeys]); + const enrolledPositionKeys = useMemo(() => { + return positionKeys?.map((pk) => enrolledPositionKey(pk)[0]); + }, [positionKeys]); + + const { accounts: delegatedAccounts, loading: loadingDel } = useDelegatedPositions(delegatedPositionKeys); + const { accounts: enrolledAccounts, loading: loadingEnrolled } = + useEnrolledPositions(enrolledPositionKeys); + const proxyAccountsByAsset = useMemo(() => { return proxyAccounts?.reduce((acc, prox) => { acc[prox.asset.toBase58()] = prox; @@ -171,7 +198,7 @@ export const HeliumVsrStateProvider: React.FC<{ const { amountLocked, votingPower, positionsWithMeta, amountProxyLocked } = useMemo(() => { - if (positions && registrar && delegatedAccounts && now) { + if (positions && registrar && delegatedAccounts && enrolledAccounts && now) { let amountLocked = new BN(0); let amountProxyLocked = new BN(0); let votingPower = new BN(0); @@ -180,17 +207,27 @@ export const HeliumVsrStateProvider: React.FC<{ .map((position, idx) => { if (position && position.info) { const isDelegated = !!delegatedAccounts?.[idx]?.info; + const isEnrolled = !!enrolledAccounts?.[idx]?.info; const proxy = proxyAccountsByAsset?.[position.info.mint.toBase58()]; const delegatedSubDao = isDelegated ? delegatedAccounts[idx]?.info?.subDao : null; - const hasRewards = isDelegated + const enrollment = isEnrolled + ? enrolledAccounts[idx]?.info + : null; + const delegationRewards = isDelegated ? delegatedAccounts[idx]!.info!.lastClaimedEpoch.add( new BN(1) ).lt(new BN(now).div(new BN(EPOCH_LENGTH))) : false; + const enrollmentRewards = isEnrolled + ? enrollment!.lastClaimedEpoch.add( + new BN(1) + ).lt(new BN(now).div(new BN(EPOCH_LENGTH))) + : false; + const posVotingPower = calcPositionVotingPower({ position: position?.info || null, registrar, @@ -214,8 +251,10 @@ export const HeliumVsrStateProvider: React.FC<{ ...position.info, pubkey: position?.publicKey, isDelegated, + isEnrolled, delegatedSubDao, - hasRewards, + hasDelegationRewards: delegationRewards, + hasEnrollmentRewards: enrollmentRewards, hasGenesisMultiplier: position.info.genesisEnd.gt(new BN(now)), votingPower: posVotingPower, votingMint: mintCfgs[position.info.votingMintConfigIdx], @@ -261,7 +300,7 @@ export const HeliumVsrStateProvider: React.FC<{ const loadingPositions = loadingMyPositions || loadingDelPositions; const ret = useMemo( () => ({ - loading: isLoading || loadingPositions || loadingDel, + loading: isLoading || loadingPositions || loadingDel || loadingEnrolled, error, amountLocked, amountProxyLocked, diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts index bbc05f8b4..33efab7cf 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts @@ -58,7 +58,7 @@ export const useClaimAllPositionsRewards = () => { maxSignatureBatch?: number; }) => { const isInvalid = - !unixNow || !provider || !positions.every((pos) => pos.hasRewards); + !unixNow || !provider || !positions.every((pos) => pos.hasDelegationRewards); const idl = await Program.fetchIdl(programId, provider); const hsdProgram = await init(provider as any, programId, idl); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts index e4191a4c6..9c3439aba 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts @@ -36,7 +36,7 @@ export const useClaimPositionRewards = () => { ) => Promise; maxSignatureBatch?: number; }) => { - const isInvalid = !unixNow || !provider || !position.hasRewards; + const isInvalid = !unixNow || !provider || !position.hasDelegationRewards; const idl = await Program.fetchIdl(programId, provider); const hsdProgram = await init(provider as any, programId, idl); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts index 6c4230240..c015c4797 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts @@ -8,6 +8,10 @@ import { HeliumVsrClient } from "../sdk/client"; import { PositionWithMeta } from "../sdk/types"; import { useQueryClient } from "@tanstack/react-query"; import { INDEXER_WAIT } from "../constants"; +import { + init as initPvr, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; export const useClosePosition = () => { const { provider, unixNow } = useHeliumVsrState(); @@ -76,6 +80,21 @@ export const useClosePosition = () => { ) ); + if (position.isEnrolled) { + const pvrProgram = await initPvr(provider as any); + const [vetokenTracker] = vetokenTrackerKey(position.registrar); + + instructions.push( + await pvrProgram.methods + .unenrollV0() + .accounts({ + position: position.pubkey, + vetokenTracker + }) + .instruction() + ); + } + instructions.push( await client.program.methods .closePositionV0() diff --git a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts index 973a7af8c..a1caa03bb 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts @@ -53,7 +53,7 @@ export const useCreatePosition = () => { ) => Promise; }) => { const isInvalid = !provider || !client; - const registrar = getRegistrarKey(mint); + const registrar = subDao?.registrar || getRegistrarKey(mint); if (isInvalid) { throw new Error("Unable to Create Position, Invalid params"); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useDao.ts b/packages/voter-stake-registry-hooks/src/hooks/useDao.ts new file mode 100644 index 000000000..322fd2ce4 --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useDao.ts @@ -0,0 +1,6 @@ +import { useAnchorAccount } from "@helium/helium-react-hooks"; +import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; +import { PublicKey } from "@solana/web3.js"; + +export const useDao = (daoKey: PublicKey | undefined) => + useAnchorAccount(daoKey, "daoV0"); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPositions.ts b/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPositions.ts new file mode 100644 index 000000000..ff3227cff --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPositions.ts @@ -0,0 +1,12 @@ +import { useAnchorAccounts } from "@helium/helium-react-hooks"; +import { PositionVotingRewards } from "@helium/idls/lib/types/position_voting_rewards"; +import { PublicKey } from "@solana/web3.js"; + +export const useEnrolledPositions = ( + enrolledPositionKeys: PublicKey[] | undefined +) => { + return useAnchorAccounts( + enrolledPositionKeys, + "enrolledPositionV0" + ); +}; diff --git a/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts index 48bd26ead..029f5d855 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts @@ -6,6 +6,10 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { PositionWithMeta } from "../sdk/types"; +import { + init as initPvr, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; export const useExtendPosition = () => { const { provider } = useHeliumVsrState(); @@ -40,6 +44,21 @@ export const useExtendPosition = () => { const [dao] = daoKey(mint); const isDao = Boolean(await provider.connection.getAccountInfo(dao)); + if (position.isEnrolled) { + const [vetokenTracker] = vetokenTrackerKey(position.registrar); + const pvrProgram = await initPvr(provider as any); + + instructions.push( + await pvrProgram.methods + .unenrollV0() + .accounts({ + position: position.pubkey, + vetokenTracker, + }) + .instruction() + ); + } + if (isDao) { instructions.push( await hsdProgram.methods diff --git a/packages/voter-stake-registry-hooks/src/hooks/useFlipPositionLockupKind.ts b/packages/voter-stake-registry-hooks/src/hooks/useFlipPositionLockupKind.ts index 8aacc8976..663394566 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useFlipPositionLockupKind.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useFlipPositionLockupKind.ts @@ -6,6 +6,10 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { PositionWithMeta } from "../sdk/types"; +import { + init as initPvr, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; function secsToDays(secs: number): number { return secs / (60 * 60 * 24); @@ -58,6 +62,23 @@ export const useFlipPositionLockupKind = () => { ) ); + if (position.isEnrolled) { + const [vetokenTracker] = vetokenTrackerKey( + position.registrar + ); + const pvrProgram = await initPvr(provider as any); + + instructions.push( + await pvrProgram.methods + .unenrollV0() + .accounts({ + position: position.pubkey, + vetokenTracker, + }) + .instruction() + ); + } + if (isDao) { instructions.push( await hsdProgram.methods diff --git a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts index 1815ed0cc..442a2892c 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts @@ -10,6 +10,10 @@ import { useSolanaUnixNow } from "@helium/helium-react-hooks"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; import BN from "bn.js"; import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; +import { + init as initPVR, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; export const useRelinquishVote = (proposal: PublicKey) => { const { positions, provider, registrar } = useHeliumVsrState(); @@ -85,6 +89,11 @@ export const useRelinquishVote = (proposal: PublicKey) => { ); } else { const vsrProgram = await init(provider); + const pvrProgram = await initPVR(provider); + const vetokenTrackerK = vetokenTrackerKey(registrar!.pubkey!)[0]; + const vetokenTracker = await pvrProgram.account.veTokenTrackerV0.fetchNullable( + vetokenTrackerK + ); const instructions = ( await Promise.all( sortedPositions.map(async (position, index) => { @@ -95,43 +104,64 @@ export const useRelinquishVote = (proposal: PublicKey) => { const marker = markers?.[index]?.info; if (marker && canRelinquishVote) { + const instructions: TransactionInstruction[] = []; + const markerK = voteMarkerKey(position.mint, proposal)[0]; if (position.isProxiedToMe) { if (marker.proxyIndex < (position.proxy?.index || 0)) { // Do not vote with a position that has been delegated to us, but voting overidden return; } - return await vsrProgram.methods - .proxiedRelinquishVoteV0({ - choice, - }) - .accounts({ - proposal, - voter: provider.wallet.publicKey, - position: position.pubkey, - marker: voteMarkerKey(position.mint, proposal)[0], - proxyAssignment: proxyAssignmentKey( - registrar!.proxyConfig, - position.mint, - provider.wallet.publicKey, - )[0], - }) - .instruction(); + instructions.push( + await vsrProgram.methods + .proxiedRelinquishVoteV0({ + choice, + }) + .accounts({ + proposal, + voter: provider.wallet.publicKey, + position: position.pubkey, + marker: markerK, + proxyAssignment: proxyAssignmentKey( + registrar!.proxyConfig, + position.mint, + provider.wallet.publicKey + )[0], + }) + .instruction() + ); + } else { + instructions.push( + await vsrProgram.methods + .relinquishVoteV1({ + choice, + }) + .accounts({ + proposal, + voter: provider.wallet.publicKey, + position: position.pubkey, + }) + .instruction() + ); + } + + if (vetokenTracker && position.isEnrolled) { + instructions.push( + await pvrProgram.methods + .trackVoteV0() + .accounts({ + marker: markerK, + position: position.pubkey, + }) + .instruction() + ); } - return await vsrProgram.methods - .relinquishVoteV1({ - choice, - }) - .accounts({ - proposal, - voter: provider.wallet.publicKey, - position: position.pubkey, - }) - .instruction(); + + return instructions; } }) ) - ).filter(truthy); + ).filter(truthy).flat(); if (onInstructions) { await onInstructions(instructions); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts index e84e2198b..dadc4173d 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts @@ -1,12 +1,14 @@ import { Program } from "@coral-xyz/anchor"; import { PROGRAM_ID, daoKey, init } from "@helium/helium-sub-daos-sdk"; +import { + init as initPvr, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; import { batchInstructionsToTxsWithPriorityFee, sendAndConfirmWithRetry, - sendInstructions, - sendInstructionsWithPriorityFee, toBN, - toVersionedTx, + toVersionedTx } from "@helium/spl-utils"; import { init as initVsr, positionKey } from "@helium/voter-stake-registry-sdk"; import { @@ -72,6 +74,21 @@ export const useSplitPosition = () => { const mintAcc = await getMint(provider.connection, mint); const amountToTransfer = toBN(amount, mintAcc!.decimals); + if (sourcePosition.isEnrolled) { + const pvrProgram = await initPvr(provider as any); + const [vetokenTracker] = vetokenTrackerKey(sourcePosition.registrar); + + instructions.push( + await pvrProgram.methods + .unenrollV0() + .accounts({ + position: sourcePosition.pubkey, + vetokenTracker, + }) + .instruction() + ); + } + instructions.push( SystemProgram.createAccount({ fromPubkey: provider.wallet!.publicKey!, diff --git a/packages/voter-stake-registry-hooks/src/hooks/useSubDao.ts b/packages/voter-stake-registry-hooks/src/hooks/useSubDao.ts new file mode 100644 index 000000000..5906bcb00 --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useSubDao.ts @@ -0,0 +1,6 @@ +import { useAnchorAccount } from "@helium/helium-react-hooks"; +import { HeliumSubDaos } from "@helium/idls/lib/types/helium_sub_daos"; +import { PublicKey } from "@solana/web3.js"; + +export const useSubDao = (subDaoKey: PublicKey | undefined) => + useAnchorAccount(subDaoKey, "subDaoV0"); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts index 6b6b13970..bc01cb174 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts @@ -1,12 +1,16 @@ import { Program } from "@coral-xyz/anchor"; import { PROGRAM_ID, daoKey, init } from "@helium/helium-sub-daos-sdk"; import { sendInstructions, toBN } from "@helium/spl-utils"; -import { init as initVsr } from "@helium/voter-stake-registry-sdk"; +import { getRegistrarKey, init as initVsr } from "@helium/voter-stake-registry-sdk"; import { getMint } from "@solana/spl-token"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { PositionWithMeta } from "../sdk/types"; +import { + init as initPvr, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; export const useTransferPosition = () => { const { provider } = useHeliumVsrState(); @@ -50,6 +54,33 @@ export const useTransferPosition = () => { const mintAcc = await getMint(provider.connection, mint); const amountToTransfer = toBN(amount, mintAcc!.decimals); + if (sourcePosition.isEnrolled) { + const pvrProgram = await initPvr(provider as any); + const [vetokenTracker] = vetokenTrackerKey(sourcePosition.registrar); + instructions.push( + await pvrProgram.methods + .unenrollV0() + .accounts({ + position: sourcePosition.pubkey, + vetokenTracker, + }) + .instruction() + ); + } + if (targetPosition.isEnrolled) { + const pvrProgram = await initPvr(provider as any); + const [vetokenTracker] = vetokenTrackerKey(targetPosition.registrar); + instructions.push( + await pvrProgram.methods + .enrollV0() + .accounts({ + position: targetPosition.pubkey, + vetokenTracker, + }) + .instruction() + ); + } + if (isDao) { instructions.push( await hsdProgram.methods diff --git a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts index 57ef78eb0..c166c1c95 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts @@ -1,20 +1,22 @@ +import { useSolanaUnixNow } from "@helium/helium-react-hooks"; import { useProposal } from "@helium/modular-governance-hooks"; +import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; +import { + init as initPVR, + vetokenTrackerKey, +} from "@helium/position-voting-rewards-sdk"; import { Status, batchParallelInstructions, truthy } from "@helium/spl-utils"; import { init, voteMarkerKey } from "@helium/voter-stake-registry-sdk"; import { PublicKey, - SYSVAR_CLOCK_PUBKEY, - TransactionInstruction, + TransactionInstruction } from "@solana/web3.js"; import BN from "bn.js"; import { useCallback, useMemo } from "react"; import { useAsyncCallback } from "react-async-hook"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; -import { useVoteMarkers } from "./useVoteMarkers"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; -import { proxyAssignmentKey } from "@helium/nft-proxy-sdk"; -import { useSolanaUnixNow } from "@helium/helium-react-hooks"; -import { PositionWithMeta } from "../sdk/types"; +import { useVoteMarkers } from "./useVoteMarkers"; export const useVote = (proposalKey: PublicKey) => { const { info: proposal } = useProposal(proposalKey); @@ -154,13 +156,20 @@ export const useVote = (proposalKey: PublicKey) => { ); } else { const vsrProgram = await init(provider); + const pvrProgram = await initPVR(provider); + const vetokenTrackerK = vetokenTrackerKey(registrar!.pubkey!)[0]; + const vetokenTracker = await pvrProgram.account.veTokenTrackerV0.fetchNullable( + vetokenTrackerK + ); const instructions = ( await Promise.all( // vote with bigger positions first. sortedPositions.map(async (position, index) => { const marker = markers?.[index]?.info; + const markerK = voteMarkerKey(position.mint, proposalKey)[0]; const canVote = canPositionVote(index, choice); if (canVote) { + const instructions: TransactionInstruction[] = []; if (position.isProxiedToMe) { if ( marker && @@ -172,39 +181,73 @@ export const useVote = (proposalKey: PublicKey) => { return; } - return await vsrProgram.methods - .proxiedVoteV0({ - choice, - }) - .accounts({ - proposal: proposalKey, - voter: provider.wallet.publicKey, - position: position.pubkey, - registrar: registrar?.pubkey, - marker: voteMarkerKey(position.mint, proposalKey)[0], - proxyAssignment: proxyAssignmentKey( - registrar!.proxyConfig, - position.mint, - provider.wallet.publicKey - )[0], - }) - .instruction(); + + instructions.push( + await vsrProgram.methods + .proxiedVoteV0({ + choice, + }) + .accounts({ + proposal: proposalKey, + voter: provider.wallet.publicKey, + position: position.pubkey, + registrar: registrar?.pubkey, + marker: markerK, + proxyAssignment: proxyAssignmentKey( + registrar!.proxyConfig, + position.mint, + provider.wallet.publicKey + )[0], + }) + .instruction() + ); + } else { + instructions.push( + await vsrProgram.methods + .voteV0({ + choice, + }) + .accounts({ + proposal: proposalKey, + voter: provider.wallet.publicKey, + position: position.pubkey, + marker: markerK, + }) + .instruction() + ); + } + + if (vetokenTracker && !position.isEnrolled) { + instructions.push( + await pvrProgram.methods + .enrollV0() + .accounts({ + vetokenTracker: vetokenTrackerK, + position: position.pubkey, + }) + .instruction() + ); } - return await vsrProgram.methods - .voteV0({ - choice, - }) - .accounts({ - proposal: proposalKey, - voter: provider.wallet.publicKey, - position: position.pubkey, - marker: voteMarkerKey(position.mint, proposalKey)[0], - }) - .instruction(); + + if (vetokenTracker) { + instructions.push( + await pvrProgram.methods + .trackVoteV0() + .accounts({ + proposal: proposalKey, + marker: markerK, + position: position.pubkey, + vetokenTracker: vetokenTrackerK, + }) + .instruction() + ); + } + + return instructions; } }) ) - ).filter(truthy); + ).filter(truthy).flat(); if (onInstructions) { await onInstructions(instructions); diff --git a/packages/voter-stake-registry-hooks/src/index.ts b/packages/voter-stake-registry-hooks/src/index.ts index ed4e973b1..bc4bd441c 100644 --- a/packages/voter-stake-registry-hooks/src/index.ts +++ b/packages/voter-stake-registry-hooks/src/index.ts @@ -13,9 +13,12 @@ export { useSplitPosition } from "./hooks/useSplitPosition"; export { useExtendPosition } from "./hooks/useExtendPosition"; export { useFlipPositionLockupKind } from "./hooks/useFlipPositionLockupKind"; export { useSubDaos } from "./hooks/useSubDaos"; +export { useSubDao } from "./hooks/useSubDao"; +export { useDao } from "./hooks/useDao"; export { useTransferPosition } from "./hooks/useTransferPosition"; export { useUndelegatePosition } from "./hooks/useUndelegatePosition"; export { useDelegatedPositions } from "./hooks/useDelegatedPositions"; +export { useEnrolledPositions } from "./hooks/useEnrolledPositions"; export { usePositions } from "./hooks/usePositions"; export { useRegistrar } from "./hooks/useRegistrar"; export { calcLockupMultiplier } from "./utils/calcLockupMultiplier"; diff --git a/packages/voter-stake-registry-hooks/src/sdk/types.ts b/packages/voter-stake-registry-hooks/src/sdk/types.ts index 056927f79..4adf4e55d 100644 --- a/packages/voter-stake-registry-hooks/src/sdk/types.ts +++ b/packages/voter-stake-registry-hooks/src/sdk/types.ts @@ -19,16 +19,18 @@ export interface Position extends Omit { } export type Proxy = ProxyAssignmentV0 & { address: PublicKey }; export interface PositionWithMeta extends Position { - pubkey: PublicKey - isDelegated: boolean + pubkey: PublicKey; + isDelegated: boolean; + isEnrolled: boolean; // This position could by someone elses position, but was delegated to me - isProxiedToMe: boolean - delegatedSubDao: PublicKey | null - hasRewards: boolean - hasGenesisMultiplier: boolean - votingPower: BN - votingMint: VotingMintConfig - proxy: Proxy | null + isProxiedToMe: boolean; + delegatedSubDao: PublicKey | null; + hasDelegationRewards: boolean; + hasEnrollmentRewards: boolean; + hasGenesisMultiplier: boolean; + votingPower: BN; + votingMint: VotingMintConfig; + proxy: Proxy | null; } export type LockupKind = IdlTypes['LockupKind'] /* export type InitializePositionV0Args = IdlTypes['InitializePositionArgsV0'] diff --git a/packages/voter-stake-registry-hooks/tsconfig.json b/packages/voter-stake-registry-hooks/tsconfig.json index ea39fcb05..30ce7b762 100644 --- a/packages/voter-stake-registry-hooks/tsconfig.json +++ b/packages/voter-stake-registry-hooks/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../voter-stake-registry-sdk" }, + { + "path": "../position-voting-rewards-sdk" + }, { "path": "../circuit-breaker-sdk" }, diff --git a/packages/voter-stake-registry-hooks/yarn.deploy.lock b/packages/voter-stake-registry-hooks/yarn.deploy.lock index 4fc69e11b..199899101 100644 --- a/packages/voter-stake-registry-hooks/yarn.deploy.lock +++ b/packages/voter-stake-registry-hooks/yarn.deploy.lock @@ -319,6 +319,24 @@ __metadata: languageName: node linkType: hard +"@helium/position-voting-rewards-sdk@^0.9.7": + version: 0.0.0-use.local + resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.9.7 + "@helium/idls": ^0.9.7 + "@helium/spl-utils": ^0.9.7 + "@solana/spl-token": ^0.3.8 + bn.js: ^5.2.0 + bs58: ^4.0.1 + git-format-staged: ^2.1.3 + ts-loader: ^9.2.3 + ts-node: ^10.9.1 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@helium/proposal-sdk@npm:^0.0.12": version: 0.0.12 resolution: "@helium/proposal-sdk@npm:0.0.12" @@ -381,6 +399,7 @@ __metadata: "@helium/helium-sub-daos-sdk": ^0.9.7 "@helium/modular-governance-hooks": ^0.0.12 "@helium/modular-governance-idls": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 "@helium/voter-stake-registry-sdk": ^0.9.7 "@solana/wallet-adapter-base": ^0.9.22 diff --git a/scripts/init-idls.sh b/scripts/init-idls.sh index 236980427..e6966140c 100755 --- a/scripts/init-idls.sh +++ b/scripts/init-idls.sh @@ -14,7 +14,7 @@ for program in $program_list; do cluster="${1:-localnet}" if [ -n "$id" ]; then - anchor_command="anchor idl init ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" + anchor_command="anchor28 idl init ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" echo "Running command: $anchor_command" # Run the anchor idl init command in the background and store the PID diff --git a/scripts/upgrade-idls.sh b/scripts/upgrade-idls.sh index 5d21ccd02..62f48c6d0 100755 --- a/scripts/upgrade-idls.sh +++ b/scripts/upgrade-idls.sh @@ -14,7 +14,7 @@ for program in $program_list; do cluster="${1:-localnet}" if [ -n "$id" ]; then - anchor_command="anchor idl upgrade ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" + anchor_command="anchor28 idl upgrade ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" echo "Running command: $anchor_command" # Run the anchor idl init command in the background and store the PID diff --git a/yarn.lock b/yarn.lock index 273b2d2fd..32bef6b18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,6 +913,7 @@ __metadata: "@helium/lazy-distributor-sdk": ^0.9.7 "@helium/mobile-entity-manager-sdk": ^0.9.7 "@helium/nft-proxy-sdk": ^0.0.12 + "@helium/organization-sdk": ^0.0.13 "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/price-oracle-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 @@ -1283,6 +1284,16 @@ __metadata: languageName: node linkType: hard +"@helium/modular-governance-idls@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/modular-governance-idls@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@solana/web3.js": ^1.78.4 + checksum: d210bdbaf3d56c88e96a1cbb4066277629a34da33aff7f49323a376a3df68a9f1a8382edb02a97c61e2336aaec32a8eb0f11699ea1e3e7a07c542d4d3b5edd8f + languageName: node + linkType: hard + "@helium/monitor-service@workspace:packages/monitor-service": version: 0.0.0-use.local resolution: "@helium/monitor-service@workspace:packages/monitor-service" @@ -1364,6 +1375,18 @@ __metadata: languageName: node linkType: hard +"@helium/organization-sdk@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/organization-sdk@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.5.0 + "@helium/modular-governance-idls": ^0.0.13 + "@helium/proposal-sdk": ^0.0.13 + checksum: 771d9b5d768d64276ca5b78911fc265434aee98c7b5d3da535fa6a55e24aa77523177e2d24112c0e83f9b0017d5a67c4d5d0f5315c13979ecc5ddf42662055a4 + languageName: node + linkType: hard + "@helium/position-voting-rewards-sdk@^0.9.7, @helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk": version: 0.0.0-use.local resolution: "@helium/position-voting-rewards-sdk@workspace:packages/position-voting-rewards-sdk" @@ -1408,6 +1431,17 @@ __metadata: languageName: node linkType: hard +"@helium/proposal-sdk@npm:^0.0.13": + version: 0.0.13 + resolution: "@helium/proposal-sdk@npm:0.0.13" + dependencies: + "@coral-xyz/anchor": ^0.28.0 + "@helium/anchor-resolvers": ^0.5.0 + "@helium/modular-governance-idls": ^0.0.13 + checksum: 70b8c02906f0361b1c32308b09492fb5c6b847aceee922c6b1a5b2bfca8eb2f425fa42782a51cb993157b338f7422229c1694a48e6fdfe41040e2ad5b646bff6 + languageName: node + linkType: hard + "@helium/proto@npm:^1.5.0": version: 1.6.0 resolution: "@helium/proto@npm:1.6.0" @@ -1570,6 +1604,7 @@ __metadata: "@helium/helium-sub-daos-sdk": ^0.9.7 "@helium/modular-governance-hooks": ^0.0.12 "@helium/modular-governance-idls": ^0.0.12 + "@helium/position-voting-rewards-sdk": ^0.9.7 "@helium/spl-utils": ^0.9.7 "@helium/voter-stake-registry-sdk": ^0.9.7 "@solana/wallet-adapter-base": ^0.9.22 From b33be469593329d36ecad77c866bfaf5405726cb Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Fri, 18 Oct 2024 13:21:04 -0700 Subject: [PATCH 11/17] Write code for claim, fix enroll/unenroll --- .../src/contexts/heliumVsrContext.tsx | 50 ++-- .../src/hooks/useClaimAllPositionsRewards.ts | 260 +++++++++++++----- .../src/hooks/useClaimPositionRewards.ts | 126 ++++++--- .../src/hooks/useClosePosition.ts | 3 +- .../src/hooks/useCreatePosition.ts | 8 +- .../src/hooks/useEnrolledPosition.ts | 12 + .../src/hooks/useExtendPosition.ts | 1 + .../src/hooks/useRegistrarForMint.ts | 32 +++ .../src/hooks/useRelinquishVote.ts | 1 + .../src/hooks/useSplitPosition.ts | 1 + .../src/hooks/useTransferPosition.ts | 1 + .../src/hooks/useVote.ts | 3 +- .../voter-stake-registry-hooks/src/index.ts | 2 + .../src/sdk/types.ts | 4 + 14 files changed, 365 insertions(+), 139 deletions(-) create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useEnrolledPosition.ts create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useRegistrarForMint.ts diff --git a/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx b/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx index 098cc7c72..b9adad4dd 100644 --- a/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx +++ b/packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx @@ -2,31 +2,27 @@ import { AnchorProvider, BN, IdlAccounts, Wallet } from "@coral-xyz/anchor"; import { useSolanaUnixNow } from "@helium/helium-react-hooks"; import { EPOCH_LENGTH, - daoKey, - delegatedPositionKey, - subDaoKey, + delegatedPositionKey } from "@helium/helium-sub-daos-sdk"; import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry"; import { init as initNftProxy } from "@helium/nft-proxy-sdk"; +import { enrolledPositionKey } from "@helium/position-voting-rewards-sdk"; import { truthy } from "@helium/spl-utils"; import { VoteService, - getRegistrarKey, - init, + init } from "@helium/voter-stake-registry-sdk"; import { Connection, PublicKey } from "@solana/web3.js"; import React, { createContext, useContext, useMemo } from "react"; import { useAsync } from "react-async-hook"; import { useDelegatedPositions } from "../hooks/useDelegatedPositions"; +import { useEnrolledPositions } from "../hooks/useEnrolledPositions"; import { usePositionKeysAndProxies } from "../hooks/usePositionKeysAndProxies"; import { usePositions } from "../hooks/usePositions"; import { useRegistrar } from "../hooks/useRegistrar"; +import { useRegistrarForMint } from "../hooks/useRegistrarForMint"; import { PositionWithMeta, ProxyAssignmentV0 } from "../sdk/types"; import { calcPositionVotingPower } from "../utils/calcPositionVotingPower"; -import { enrolledPositionKey } from "@helium/position-voting-rewards-sdk"; -import { useEnrolledPositions } from "../hooks/useEnrolledPositions"; -import { useSubDao } from "../hooks/useSubDao"; -import { useDao } from "../hooks/useDao"; type Registrar = IdlAccounts["registrar"]; @@ -90,23 +86,7 @@ export const HeliumVsrStateProvider: React.FC<{ } }, [connection?.rpcEndpoint, wallet?.publicKey?.toBase58()]); - const daoK = useMemo(() => { - if (mint) { - return daoKey(mint); - } - }, [mint?.toBase58()]); - const subDaoK = useMemo(() => { - if (mint) { - return subDaoKey(mint)[0]; - } - }, [mint?.toBase58()]); - const { info: subDao } = useSubDao(subDaoK); - const { info: dao } = useDao(subDaoK); - - const registrarKey = useMemo( - () => subDao && subDao.registrar || dao && dao.registrar, - [subDao?.registrar, dao?.registrar] - ); + const { registrarKey } = useRegistrarForMint(mint) const { info: registrar } = useRegistrar(registrarKey); @@ -198,7 +178,13 @@ export const HeliumVsrStateProvider: React.FC<{ const { amountLocked, votingPower, positionsWithMeta, amountProxyLocked } = useMemo(() => { - if (positions && registrar && delegatedAccounts && enrolledAccounts && now) { + if ( + positions && + registrar && + delegatedAccounts && + enrolledAccounts && + now + ) { let amountLocked = new BN(0); let amountProxyLocked = new BN(0); let votingPower = new BN(0); @@ -223,10 +209,10 @@ export const HeliumVsrStateProvider: React.FC<{ : false; const enrollmentRewards = isEnrolled - ? enrollment!.lastClaimedEpoch.add( - new BN(1) - ).lt(new BN(now).div(new BN(EPOCH_LENGTH))) - : false; + ? enrollment!.lastClaimedEpoch + .add(new BN(1)) + .lt(new BN(now).div(new BN(EPOCH_LENGTH))) + : false; const posVotingPower = calcPositionVotingPower({ position: position?.info || null, @@ -253,6 +239,7 @@ export const HeliumVsrStateProvider: React.FC<{ isDelegated, isEnrolled, delegatedSubDao, + hasRewards: delegationRewards || enrollmentRewards, hasDelegationRewards: delegationRewards, hasEnrollmentRewards: enrollmentRewards, hasGenesisMultiplier: position.info.genesisEnd.gt(new BN(now)), @@ -281,6 +268,7 @@ export const HeliumVsrStateProvider: React.FC<{ registrar, delegatedAccounts, proxyAccounts, + enrolledAccounts, ]); const sortedPositions = useMemo( diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts index 33efab7cf..2f3850384 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts @@ -16,6 +16,7 @@ import { Status, batchSequentialParallelInstructions, chunks, + truthy, } from "@helium/spl-utils"; import { PROGRAM_ID as VSR_PROGRAM_ID, @@ -34,7 +35,13 @@ import { import { useAsyncCallback } from "react-async-hook"; import { MAX_TRANSACTIONS_PER_SIGNATURE_BATCH } from "../constants"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; -import { PositionWithMeta, SubDao } from "../sdk/types"; +import { PositionWithMeta, SubDao, VeTokenTracker } from "../sdk/types"; +import { + init as initPvr, + enrolledPositionKey, + PROGRAM_ID as PVR_PROGRAM_ID, + vsrEpochInfoKey, +} from "@helium/position-voting-rewards-sdk"; const DAO = daoKey(HNT_MINT)[0]; @@ -44,12 +51,14 @@ export const useClaimAllPositionsRewards = () => { async ({ positions, programId = PROGRAM_ID, + pvrProgramId = PVR_PROGRAM_ID, onProgress, onInstructions, maxSignatureBatch = MAX_TRANSACTIONS_PER_SIGNATURE_BATCH, }: { positions: PositionWithMeta[]; programId?: PublicKey; + pvrProgramId?: PublicKey; onProgress?: (status: Status) => void; // Instead of sending the transaction, let the caller decide onInstructions?: ( @@ -58,10 +67,14 @@ export const useClaimAllPositionsRewards = () => { maxSignatureBatch?: number; }) => { const isInvalid = - !unixNow || !provider || !positions.every((pos) => pos.hasDelegationRewards); + !unixNow || + !provider || + !positions.every((pos) => pos.hasRewards); const idl = await Program.fetchIdl(programId, provider); + const pvrIdl = await Program.fetchIdl(pvrProgramId, provider); const hsdProgram = await init(provider as any, programId, idl); + const pvrProgram = await initPvr(provider as any, pvrProgramId, pvrIdl); if (loading) return; @@ -78,12 +91,24 @@ export const useClaimAllPositionsRewards = () => { const key = delegatedPositionKey(p.pubkey)[0]; return { key, - account: await hsdProgram.account.delegatedPositionV0.fetch(key), + account: await hsdProgram.account.delegatedPositionV0.fetchNullable(key), + }; + }) + ); + const enrolledPositions = await Promise.all( + positions.map(async (p) => { + const key = enrolledPositionKey(p.pubkey)[0]; + return { + key, + account: + await pvrProgram.account.enrolledPositionV0.fetchNullable(key), }; }) ); const subDaoKeys = new Set([ - ...delegatedPositions.map((p) => p.account.subDao.toBase58()), + ...delegatedPositions + .map((p) => p.account?.subDao.toBase58()) + .filter(truthy), ]); const subDaos = ( await Promise.all( @@ -93,85 +118,182 @@ export const useClaimAllPositionsRewards = () => { })) ) ).reduce((acc, { key, account }) => { - acc[key] = account; + if (key) { + acc[key] = account; + } return acc; }, {} as Record); + const vetokenTrackerKeys = new Set([ + ...enrolledPositions + .map((p) => p.account?.vetokenTracker.toBase58()) + .filter(truthy), + ]); + const vetokenTrackers = ( + await Promise.all( + [...vetokenTrackerKeys].map(async (vt) => ({ + key: vt, + account: await pvrProgram.account.veTokenTrackerV0.fetch(vt), + })) + ) + ).reduce((acc, { key, account }) => { + if (key) { + acc[key] = account; + } + return acc; + }, {} as Record); + for (const [idx, position] of positions.entries()) { const { lockup } = position; const lockupKind = Object.keys(lockup.kind)[0] as string; const isConstant = lockupKind === "constant"; const isDecayed = !isConstant && lockup.endTs.lte(new BN(unixNow)); const decayedEpoch = lockup.endTs.div(new BN(EPOCH_LENGTH)); - const delegatedPosition = delegatedPositions[idx]; bucketedEpochsByPosition[position.pubkey.toBase58()] = bucketedEpochsByPosition[position.pubkey.toBase58()] || []; - const { lastClaimedEpoch, claimedEpochsBitmap } = - delegatedPosition.account; - const epoch = lastClaimedEpoch.add(new BN(1)); - const epochsToClaim = Array.from( - { - length: !isDecayed - ? currentEpoch.sub(epoch).toNumber() - : decayedEpoch.sub(epoch).toNumber(), - }, - (_v, k) => epoch.addn(k) - ).filter( - (epoch) => - !isClaimed({ - epoch: epoch.toNumber(), - lastClaimedEpoch: lastClaimedEpoch.toNumber(), - claimedEpochsBitmap, - }) - ); - const subDao = delegatedPosition.account.subDao; - const subDaoStr = subDao.toBase58(); - const subDaoAcc = subDaos[subDaoStr]; + const delegatedPosition = delegatedPositions[idx]; + if (delegatedPosition?.account) { + const { lastClaimedEpoch, claimedEpochsBitmap } = + delegatedPosition.account; + const epoch = lastClaimedEpoch.add(new BN(1)); + const epochsToClaim = Array.from( + { + length: !isDecayed + ? currentEpoch.sub(epoch).toNumber() + : decayedEpoch.sub(epoch).toNumber(), + }, + (_v, k) => epoch.addn(k) + ).filter( + (epoch) => + !isClaimed({ + epoch: epoch.toNumber(), + lastClaimedEpoch: lastClaimedEpoch.toNumber(), + claimedEpochsBitmap, + }) + ); + const subDao = delegatedPosition.account.subDao; + const subDaoStr = subDao.toBase58(); + const subDaoAcc = subDaos[subDaoStr]; - // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap - for (const chunk of chunks(epochsToClaim, 128)) { - bucketedEpochsByPosition[position.pubkey.toBase58()].push( - await Promise.all( - chunk.map((epoch) => - hsdProgram.methods - .claimRewardsV0({ - epoch, - }) - .accountsStrict({ - position: position.pubkey, - mint: position.mint, - positionTokenAccount: getAssociatedTokenAddressSync( - position.mint, - provider.wallet.publicKey - ), - positionAuthority: provider.wallet.publicKey, - registrar: position.registrar, - dao: DAO, - subDao: delegatedPosition.account.subDao, - delegatedPosition: delegatedPosition.key, - dntMint: subDaoAcc.dntMint, - subDaoEpochInfo: subDaoEpochInfoKey( - subDao, - epoch.mul(new BN(EPOCH_LENGTH)) - )[0], - delegatorPool: subDaoAcc.delegatorPool, - delegatorAta: getAssociatedTokenAddressSync( - subDaoAcc.dntMint, - provider.wallet.publicKey - ), - delegatorPoolCircuitBreaker: accountWindowedBreakerKey( - subDaoAcc.delegatorPool - )[0], - vsrProgram: VSR_PROGRAM_ID, - systemProgram: SystemProgram.programId, - circuitBreakerProgram: CIRCUIT_BREAKER_PROGRAM_ID, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - tokenProgram: TOKEN_PROGRAM_ID, - }) - .instruction() + // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap + for (const chunk of chunks(epochsToClaim, 128)) { + bucketedEpochsByPosition[position.pubkey.toBase58()].push( + await Promise.all( + chunk.map((epoch) => + hsdProgram.methods + .claimRewardsV0({ + epoch, + }) + .accountsStrict({ + position: position.pubkey, + mint: position.mint, + positionTokenAccount: getAssociatedTokenAddressSync( + position.mint, + provider.wallet.publicKey + ), + positionAuthority: provider.wallet.publicKey, + registrar: position.registrar, + dao: DAO, + subDao: delegatedPosition.account!.subDao, + delegatedPosition: delegatedPosition.key, + dntMint: subDaoAcc.dntMint, + subDaoEpochInfo: subDaoEpochInfoKey( + subDao, + epoch.mul(new BN(EPOCH_LENGTH)) + )[0], + delegatorPool: subDaoAcc.delegatorPool, + delegatorAta: getAssociatedTokenAddressSync( + subDaoAcc.dntMint, + provider.wallet.publicKey + ), + delegatorPoolCircuitBreaker: accountWindowedBreakerKey( + subDaoAcc.delegatorPool + )[0], + vsrProgram: VSR_PROGRAM_ID, + systemProgram: SystemProgram.programId, + circuitBreakerProgram: CIRCUIT_BREAKER_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction() + ) ) - ) + ); + } + } + + const enrolledPosition = enrolledPositions[idx]; + if (enrolledPosition?.account) { + const { lastClaimedEpoch, claimedEpochsBitmap } = + enrolledPosition.account; + const epoch = lastClaimedEpoch.add(new BN(1)); + const epochsToClaim = Array.from( + { + length: !isDecayed + ? currentEpoch.sub(epoch).toNumber() + : decayedEpoch.sub(epoch).toNumber(), + }, + (_v, k) => epoch.addn(k) + ).filter( + (epoch) => + !isClaimed({ + epoch: epoch.toNumber(), + lastClaimedEpoch: lastClaimedEpoch.toNumber(), + claimedEpochsBitmap, + }) ); + + console.log("epochsToClaim", epochsToClaim) + + const vetokenTracker = enrolledPosition.account.vetokenTracker; + const vetokenTrackerStr = vetokenTracker.toBase58(); + const vetokenTrackerAcc = vetokenTrackers[vetokenTrackerStr]; + + // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap + for (const chunk of chunks(epochsToClaim, 128)) { + const vsrEpochInfo = vsrEpochInfoKey( + vetokenTracker, + epoch.mul(new BN(EPOCH_LENGTH)) + )[0]; + const rewardsPool = getAssociatedTokenAddressSync( + vetokenTrackerAcc.rewardsMint, + vsrEpochInfo + ); + bucketedEpochsByPosition[position.pubkey.toBase58()].push( + await Promise.all( + chunk.map((epoch) => + pvrProgram.methods + .claimRewardsV0({ + epoch, + }) + .accountsStrict({ + position: position.pubkey, + mint: position.mint, + positionTokenAccount: getAssociatedTokenAddressSync( + position.mint, + provider.wallet.publicKey + ), + positionAuthority: provider.wallet.publicKey, + registrar: position.registrar, + vetokenTracker: enrolledPosition.account!.vetokenTracker, + enrolledPosition: enrolledPosition.key, + rewardsMint: vetokenTrackerAcc.rewardsMint, + vsrEpochInfo, + rewardsPool, + enrolledAta: getAssociatedTokenAddressSync( + vetokenTrackerAcc.rewardsMint, + provider.wallet.publicKey + ), + vsrProgram: VSR_PROGRAM_ID, + systemProgram: SystemProgram.programId, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .instruction() + ) + ) + ); + } } } diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts index 9c3439aba..a2df3f17a 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimPositionRewards.ts @@ -16,6 +16,11 @@ import { useAsyncCallback } from "react-async-hook"; import { MAX_TRANSACTIONS_PER_SIGNATURE_BATCH } from "../constants"; import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { PositionWithMeta } from "../sdk/types"; +import { + init as initPvr, + enrolledPositionKey, + PROGRAM_ID as PVR_PROGRAM_ID, +} from "@helium/position-voting-rewards-sdk"; export const useClaimPositionRewards = () => { const { provider, unixNow } = useHeliumVsrState(); @@ -23,12 +28,14 @@ export const useClaimPositionRewards = () => { async ({ position, programId = PROGRAM_ID, + pvrProgramId = PVR_PROGRAM_ID, onProgress, onInstructions, maxSignatureBatch = MAX_TRANSACTIONS_PER_SIGNATURE_BATCH, }: { position: PositionWithMeta; programId?: PublicKey; + pvrProgramId?: PublicKey; onProgress?: (status: Status) => void; // Instead of sending the transaction, let the caller decide onInstructions?: ( @@ -36,10 +43,12 @@ export const useClaimPositionRewards = () => { ) => Promise; maxSignatureBatch?: number; }) => { - const isInvalid = !unixNow || !provider || !position.hasDelegationRewards; + const isInvalid = !unixNow || !provider || !position.hasRewards; const idl = await Program.fetchIdl(programId, provider); + const pvrIdl = await Program.fetchIdl(pvrProgramId, provider); const hsdProgram = await init(provider as any, programId, idl); + const pvrProgram = await initPvr(provider as any, pvrProgramId, pvrIdl); if (loading) return; @@ -54,46 +63,91 @@ export const useClaimPositionRewards = () => { const currentEpoch = new BN(unixNow).div(new BN(EPOCH_LENGTH)); const delegatedPosKey = delegatedPositionKey(position.pubkey)[0]; const delegatedPosAcc = - await hsdProgram.account.delegatedPositionV0.fetch(delegatedPosKey); - - const { lastClaimedEpoch, claimedEpochsBitmap } = delegatedPosAcc; - const epoch = lastClaimedEpoch.add(new BN(1)); - const epochsToClaim = Array.from( - { - length: !isDecayed - ? currentEpoch.sub(epoch).toNumber() - : decayedEpoch.sub(epoch).toNumber(), - }, - (_v, k) => epoch.addn(k) - ).filter( - (epoch) => - !isClaimed({ - epoch: epoch.toNumber(), - lastClaimedEpoch: lastClaimedEpoch.toNumber(), - claimedEpochsBitmap, - }) - ); + await hsdProgram.account.delegatedPositionV0.fetchNullable(delegatedPosKey); const instructions: TransactionInstruction[][] = []; - // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap - for (const chunk of chunks(epochsToClaim, 128)) { - instructions.push( - await Promise.all( - chunk.map( - async (epoch) => - await hsdProgram.methods - .claimRewardsV0({ - epoch, - }) - .accounts({ - position: position.pubkey, - subDao: delegatedPosAcc.subDao, - }) - .instruction() + if (delegatedPosAcc) { + const { lastClaimedEpoch, claimedEpochsBitmap } = delegatedPosAcc; + const epoch = lastClaimedEpoch.add(new BN(1)); + const epochsToClaim = Array.from( + { + length: !isDecayed + ? currentEpoch.sub(epoch).toNumber() + : decayedEpoch.sub(epoch).toNumber(), + }, + (_v, k) => epoch.addn(k) + ).filter( + (epoch) => + !isClaimed({ + epoch: epoch.toNumber(), + lastClaimedEpoch: lastClaimedEpoch.toNumber(), + claimedEpochsBitmap, + }) + ); + // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap + for (const chunk of chunks(epochsToClaim, 128)) { + instructions.push( + await Promise.all( + chunk.map( + async (epoch) => + await hsdProgram.methods + .claimRewardsV0({ + epoch, + }) + .accounts({ + position: position.pubkey, + subDao: delegatedPosAcc.subDao, + }) + .instruction() + ) ) - ) + ); + } + } + + const enrolledPosKey = enrolledPositionKey(position.pubkey)[0]; + const enrolledPosAcc = + await pvrProgram.account.enrolledPositionV0.fetchNullable(enrolledPosKey); + + if (enrolledPosAcc) { + const { lastClaimedEpoch, claimedEpochsBitmap } = enrolledPosAcc; + const epoch = lastClaimedEpoch.add(new BN(1)); + const epochsToClaim = Array.from( + { + length: !isDecayed + ? currentEpoch.sub(epoch).toNumber() + : decayedEpoch.sub(epoch).toNumber(), + }, + (_v, k) => epoch.addn(k) + ).filter( + (epoch) => + !isClaimed({ + epoch: epoch.toNumber(), + lastClaimedEpoch: lastClaimedEpoch.toNumber(), + claimedEpochsBitmap, + }) ); + + // Chunk size is 128 because we want each chunk to correspond to the 128 bits in bitmap + for (const chunk of chunks(epochsToClaim, 128)) { + instructions.push( + await Promise.all( + chunk.map( + async (epoch) => + await pvrProgram.methods + .claimRewardsV0({ + epoch, + }) + .accounts({ + position: position.pubkey, + enrolledPosition: enrolledPosKey, + }) + .instruction() + ) + ) + ); + } } if (onInstructions) { diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts index c015c4797..0fa1b1178 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClosePosition.ts @@ -89,7 +89,8 @@ export const useClosePosition = () => { .unenrollV0() .accounts({ position: position.pubkey, - vetokenTracker + vetokenTracker, + rentRefund: provider.wallet.publicKey, }) .instruction() ); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts index a1caa03bb..c0fa2bee9 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useCreatePosition.ts @@ -18,8 +18,10 @@ import { useHeliumVsrState } from "../contexts/heliumVsrContext"; import { HeliumVsrClient } from "../sdk/client"; import { SubDaoWithMeta } from "../sdk/types"; import { + daoKey, init as initHsd, subDaoEpochInfoKey, + subDaoKey, } from "@helium/helium-sub-daos-sdk"; import { useQueryClient } from "@tanstack/react-query"; import { INDEXER_WAIT } from "../constants"; @@ -53,12 +55,16 @@ export const useCreatePosition = () => { ) => Promise; }) => { const isInvalid = !provider || !client; - const registrar = subDao?.registrar || getRegistrarKey(mint); if (isInvalid) { throw new Error("Unable to Create Position, Invalid params"); } else { const hsdProgram = await initHsd(provider); + const [daoK] = daoKey(mint); + const [subDaoK] = subDaoKey(mint); + const myDao = await hsdProgram.account.daoV0.fetchNullable(daoK); + const mySubDao = await hsdProgram.account.subDaoV0.fetchNullable(subDaoK); + const registrar = (mySubDao?.registrar || myDao?.registrar)!; const mintKeypair = Keypair.generate(); const position = positionKey(mintKeypair.publicKey)[0]; const instructions: TransactionInstruction[] = []; diff --git a/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPosition.ts new file mode 100644 index 000000000..e85050236 --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useEnrolledPosition.ts @@ -0,0 +1,12 @@ +import { useAnchorAccount } from "@helium/helium-react-hooks"; +import { PositionVotingRewards } from "@helium/idls/lib/types/position_voting_rewards"; +import { PublicKey } from "@solana/web3.js"; + +export const useEnrolledPosition = ( + enrolledPositionKey: PublicKey | undefined +) => { + return useAnchorAccount( + enrolledPositionKey, + "enrolledPositionV0" + ); +}; diff --git a/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts index 029f5d855..cddbdb6e1 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useExtendPosition.ts @@ -54,6 +54,7 @@ export const useExtendPosition = () => { .accounts({ position: position.pubkey, vetokenTracker, + rentRefund: provider.wallet.publicKey, }) .instruction() ); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useRegistrarForMint.ts b/packages/voter-stake-registry-hooks/src/hooks/useRegistrarForMint.ts new file mode 100644 index 000000000..c1b7ef56b --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useRegistrarForMint.ts @@ -0,0 +1,32 @@ +import { daoKey, subDaoKey } from "@helium/helium-sub-daos-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { useMemo } from "react"; +import { useSubDao } from "./useSubDao"; +import { useDao } from "./useDao"; + +export function useRegistrarForMint(mint: PublicKey | undefined) { + const daoK = useMemo(() => { + if (mint) { + return daoKey(mint)[0]; + } + }, [mint?.toBase58()]); + const subDaoK = useMemo(() => { + if (mint) { + return subDaoKey(mint)[0]; + } + }, [mint?.toBase58()]); + const { info: subDao, loading: loadingSubdao } = useSubDao(subDaoK); + const { info: dao, loading: loadingDao } = useDao(daoK); + + const registrarKey = useMemo( + () => (subDao && subDao.registrar) || (dao && dao.registrar), + [subDao?.registrar, dao?.registrar] + ); + + return { + registrarKey, + subDao, + dao, + loading: loadingDao || loadingSubdao + } +} \ No newline at end of file diff --git a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts index 442a2892c..68a79bddc 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useRelinquishVote.ts @@ -152,6 +152,7 @@ export const useRelinquishVote = (proposal: PublicKey) => { .accounts({ marker: markerK, position: position.pubkey, + proposal }) .instruction() ); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts index dadc4173d..485205796 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useSplitPosition.ts @@ -84,6 +84,7 @@ export const useSplitPosition = () => { .accounts({ position: sourcePosition.pubkey, vetokenTracker, + rentRefund: provider.wallet.publicKey, }) .instruction() ); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts b/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts index bc01cb174..330fe2e04 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useTransferPosition.ts @@ -63,6 +63,7 @@ export const useTransferPosition = () => { .accounts({ position: sourcePosition.pubkey, vetokenTracker, + rentRefund: provider.wallet.publicKey, }) .instruction() ); diff --git a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts index c166c1c95..9437bd3de 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useVote.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useVote.ts @@ -124,7 +124,7 @@ export const useVote = (proposalKey: PublicKey) => { !earlierDelegateVoted)); return canVote; }, - [registrar, unixNow] + [registrar, unixNow, markers, sortedPositions] ); const canVote = useCallback( (choice: number) => { @@ -272,6 +272,7 @@ export const useVote = (proposalKey: PublicKey) => { markers, voteWeights, canVote, + canPositionVote, voters, }; }; diff --git a/packages/voter-stake-registry-hooks/src/index.ts b/packages/voter-stake-registry-hooks/src/index.ts index bc4bd441c..3c2587bcd 100644 --- a/packages/voter-stake-registry-hooks/src/index.ts +++ b/packages/voter-stake-registry-hooks/src/index.ts @@ -12,6 +12,7 @@ export { useDelegatePosition } from "./hooks/useDelegatePosition"; export { useSplitPosition } from "./hooks/useSplitPosition"; export { useExtendPosition } from "./hooks/useExtendPosition"; export { useFlipPositionLockupKind } from "./hooks/useFlipPositionLockupKind"; +export { useRegistrarForMint } from "./hooks/useRegistrarForMint" export { useSubDaos } from "./hooks/useSubDaos"; export { useSubDao } from "./hooks/useSubDao"; export { useDao } from "./hooks/useDao"; @@ -19,6 +20,7 @@ export { useTransferPosition } from "./hooks/useTransferPosition"; export { useUndelegatePosition } from "./hooks/useUndelegatePosition"; export { useDelegatedPositions } from "./hooks/useDelegatedPositions"; export { useEnrolledPositions } from "./hooks/useEnrolledPositions"; +export { useEnrolledPosition } from "./hooks/useEnrolledPosition"; export { usePositions } from "./hooks/usePositions"; export { useRegistrar } from "./hooks/useRegistrar"; export { calcLockupMultiplier } from "./utils/calcLockupMultiplier"; diff --git a/packages/voter-stake-registry-hooks/src/sdk/types.ts b/packages/voter-stake-registry-hooks/src/sdk/types.ts index 4adf4e55d..ab6c4336c 100644 --- a/packages/voter-stake-registry-hooks/src/sdk/types.ts +++ b/packages/voter-stake-registry-hooks/src/sdk/types.ts @@ -1,5 +1,6 @@ import { BN, IdlAccounts, IdlTypes } from '@coral-xyz/anchor' import { HeliumSubDaos } from '@helium/idls/lib/types/helium_sub_daos' +import { PositionVotingRewards } from '@helium/idls/lib/types/position_voting_rewards' import { VoterStakeRegistry as HeliumVoterStakeRegistry } from '@helium/idls/lib/types/voter_stake_registry' import { NftProxy } from '@helium/modular-governance-idls/lib/types/nft_proxy' import { PublicKey } from '@solana/web3.js' @@ -25,6 +26,7 @@ export interface PositionWithMeta extends Position { // This position could by someone elses position, but was delegated to me isProxiedToMe: boolean; delegatedSubDao: PublicKey | null; + hasRewards: boolean; hasDelegationRewards: boolean; hasEnrollmentRewards: boolean; hasGenesisMultiplier: boolean; @@ -45,3 +47,5 @@ export interface SubDaoWithMeta extends Omit { json: any; } } + +export type VeTokenTracker = IdlAccounts["veTokenTrackerV0"]; From 5ad965c844fe526ccacb9449202acb0663ba7443 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 21 Oct 2024 08:39:07 -0700 Subject: [PATCH 12/17] Undo anchor28 --- packages/spl-utils/src/constants.ts | 6 +++--- .../src/hooks/useClaimAllPositionsRewards.ts | 5 ++--- scripts/init-idls.sh | 2 +- scripts/upgrade-idls.sh | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/spl-utils/src/constants.ts b/packages/spl-utils/src/constants.ts index a9d3e84aa..ac194f25d 100644 --- a/packages/spl-utils/src/constants.ts +++ b/packages/spl-utils/src/constants.ts @@ -4,13 +4,13 @@ export const DC_MINT = new PublicKey( "dcuc8Amr83Wz27ZkQ2K9NS6r8zRpf1J6cvArEBDZDmm" ); -export const HNT_MINT = new PublicKey("APqAVo5q9erS8GaXcbJuy3Gx4ikuSzXjzY4SnyppPUm1"); +export const HNT_MINT = new PublicKey("hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux"); export const MOBILE_MINT = new PublicKey( - "CoQciaEADT77zudZQm4atzWjHmHcJKfzZBivbifHScvZ" + "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6" ); -export const IOT_MINT = new PublicKey("g69eEQjY4bv1UvjaeTUcaCNoJ6v8jSsjL3dojN6uC4B"); +export const IOT_MINT = new PublicKey("iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns"); // TODO: Replace with actual HNT feed export const HNT_PYTH_PRICE_FEED = new PublicKey( diff --git a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts index 2f3850384..b94592ab4 100644 --- a/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts +++ b/packages/voter-stake-registry-hooks/src/hooks/useClaimAllPositionsRewards.ts @@ -243,8 +243,6 @@ export const useClaimAllPositionsRewards = () => { }) ); - console.log("epochsToClaim", epochsToClaim) - const vetokenTracker = enrolledPosition.account.vetokenTracker; const vetokenTrackerStr = vetokenTracker.toBase58(); const vetokenTrackerAcc = vetokenTrackers[vetokenTrackerStr]; @@ -257,7 +255,8 @@ export const useClaimAllPositionsRewards = () => { )[0]; const rewardsPool = getAssociatedTokenAddressSync( vetokenTrackerAcc.rewardsMint, - vsrEpochInfo + vsrEpochInfo, + true ); bucketedEpochsByPosition[position.pubkey.toBase58()].push( await Promise.all( diff --git a/scripts/init-idls.sh b/scripts/init-idls.sh index e6966140c..236980427 100755 --- a/scripts/init-idls.sh +++ b/scripts/init-idls.sh @@ -14,7 +14,7 @@ for program in $program_list; do cluster="${1:-localnet}" if [ -n "$id" ]; then - anchor_command="anchor28 idl init ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" + anchor_command="anchor idl init ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" echo "Running command: $anchor_command" # Run the anchor idl init command in the background and store the PID diff --git a/scripts/upgrade-idls.sh b/scripts/upgrade-idls.sh index 62f48c6d0..5d21ccd02 100755 --- a/scripts/upgrade-idls.sh +++ b/scripts/upgrade-idls.sh @@ -14,7 +14,7 @@ for program in $program_list; do cluster="${1:-localnet}" if [ -n "$id" ]; then - anchor_command="anchor28 idl upgrade ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" + anchor_command="anchor idl upgrade ${id} --filepath ${filepath} --provider.cluster ${cluster} --provider.wallet $HOME/.config/solana/id.json" echo "Running command: $anchor_command" # Run the anchor idl init command in the background and store the PID From 39c2008f4e3d9bd30eef98de3d985b95d6f93489 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 21 Oct 2024 08:55:13 -0700 Subject: [PATCH 13/17] Fix to give rewards when there haven't been proposals --- .../src/instructions/claim_rewards_v0.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs index 1676a449a..f79f734e5 100644 --- a/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs +++ b/programs/position-voting-rewards/src/instructions/claim_rewards_v0.rs @@ -178,7 +178,17 @@ pub fn handler(ctx: Context, args: ClaimRewardsArgsV0) -> Result .filter(|&proposal| proposal_set.contains(&proposal.proposal)) .count(); - if eligible_count >= 2 { + let not_two_proposals = ctx.accounts.vsr_epoch_info.recent_proposals.len() < 2 + || ctx + .accounts + .vsr_epoch_info + .recent_proposals + .iter() + .filter(|p| p.proposal == Pubkey::default()) + .count() + < 2; + + if not_two_proposals || eligible_count >= 2 { msg!("Position is eligible, transferring"); transfer( ctx From d35570dc37c715c49c3b0e80a311408989821e21 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 21 Oct 2024 16:01:03 -0500 Subject: [PATCH 14/17] Fix broken test --- tests/voter-stake-registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/voter-stake-registry.ts b/tests/voter-stake-registry.ts index 7afc568b9..c76517866 100644 --- a/tests/voter-stake-registry.ts +++ b/tests/voter-stake-registry.ts @@ -733,7 +733,7 @@ describe("voter-stake-registry", () => { targetPosition: newPos, depositMint: hntMint, }) - .rpc({ skipPreflight: true }) + .rpc() ).to.eventually.be.rejectedWith( "AnchorError caused by account: source_position. Error Code: ActiveVotesExist. Error Number: 6055. Error Message: Cannot change a position while active votes exist." ); From a541e22673fde7a7b760d7d81c0e178e9060a83f Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 21 Oct 2024 16:23:24 -0500 Subject: [PATCH 15/17] Fix mut for oracle --- .../lazy-distributor/src/instructions/set_current_rewards_v0.rs | 1 - .../src/instructions/set_current_rewards_wrapper_v1.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/lazy-distributor/src/instructions/set_current_rewards_v0.rs b/programs/lazy-distributor/src/instructions/set_current_rewards_v0.rs index 5b0132906..05201c187 100644 --- a/programs/lazy-distributor/src/instructions/set_current_rewards_v0.rs +++ b/programs/lazy-distributor/src/instructions/set_current_rewards_v0.rs @@ -21,7 +21,6 @@ pub struct SetCurrentRewardsV0<'info> { )] pub recipient: Box>, #[account( - mut, constraint = args.oracle_index < lazy_distributor.oracles.len() as u16 @ ErrorCode::InvalidOracleIndex, constraint = oracle.key() == lazy_distributor.oracles[usize::from(args.oracle_index)].oracle )] diff --git a/programs/rewards-oracle/src/instructions/set_current_rewards_wrapper_v1.rs b/programs/rewards-oracle/src/instructions/set_current_rewards_wrapper_v1.rs index 7e175a1d9..93756757f 100644 --- a/programs/rewards-oracle/src/instructions/set_current_rewards_wrapper_v1.rs +++ b/programs/rewards-oracle/src/instructions/set_current_rewards_wrapper_v1.rs @@ -17,6 +17,7 @@ pub struct SetCurrentRewardsWrapperArgsV1 { #[instruction(args: SetCurrentRewardsWrapperArgsV1)] pub struct SetCurrentRewardsWrapperV1<'info> { // the oracle EOA that gets wrapped + #[account(mut)] pub oracle: Signer<'info>, pub lazy_distributor: Box>, #[account( From 642fdf559beb85d5fcbab40e8b85014305e3b29c Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Mon, 21 Oct 2024 16:56:55 -0500 Subject: [PATCH 16/17] Fix tests --- tests/helium-entity-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helium-entity-manager.ts b/tests/helium-entity-manager.ts index 5aea03392..f5da17a01 100644 --- a/tests/helium-entity-manager.ts +++ b/tests/helium-entity-manager.ts @@ -145,7 +145,7 @@ describe("helium-entity-manager", () => { dao, activeDeviceAuthority: activeDeviceAuthority.publicKey, // Add some padding for onboards - numTokens: MAKER_STAKING_FEE.mul(new BN(2)).add(new BN(10000000000)), + numTokens: MAKER_STAKING_FEE.mul(new BN(2)).add(new BN(100000000000)), })); }); From aa08460fec9cdc57642327891be5997d24885f8e Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Wed, 23 Oct 2024 16:38:58 -0500 Subject: [PATCH 17/17] Bump cargo version --- Cargo.lock | 4 ++-- programs/mobile-entity-manager/Cargo.toml | 2 +- programs/voter-stake-registry/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73ffcdca8..88bc474ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2529,7 +2529,7 @@ dependencies = [ [[package]] name = "mobile-entity-manager" -version = "0.1.3" +version = "0.1.4" dependencies = [ "account-compression-cpi", "anchor-lang", @@ -5687,7 +5687,7 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "voter-stake-registry" -version = "0.3.3" +version = "0.3.4" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/programs/mobile-entity-manager/Cargo.toml b/programs/mobile-entity-manager/Cargo.toml index e46e4fa1c..249f00bb3 100644 --- a/programs/mobile-entity-manager/Cargo.toml +++ b/programs/mobile-entity-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mobile-entity-manager" -version = "0.1.3" +version = "0.1.4" description = "Created with Anchor" edition = "2021" diff --git a/programs/voter-stake-registry/Cargo.toml b/programs/voter-stake-registry/Cargo.toml index e636579ef..90bd354c7 100644 --- a/programs/voter-stake-registry/Cargo.toml +++ b/programs/voter-stake-registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "voter-stake-registry" -version = "0.3.3" +version = "0.3.4" description = "Heliums voter weight plugin for spl-governance" license = "GPL-3.0-or-later" homepage = "https://github.com/helium/helium-program-library"