From 430f154c2461aec9453b4fe550f840a8edd3e324 Mon Sep 17 00:00:00 2001 From: Noah Prince Date: Tue, 7 Jan 2025 12:23:04 -0800 Subject: [PATCH] Add the ability to extend delegation --- .../src/add-expiration-to-delegations.ts | 11 ++- packages/helium-sub-daos-sdk/src/resolvers.ts | 3 +- .../src/hooks/useExtendDelegation.ts | 98 +++++++++++++++++++ .../voter-stake-registry-hooks/src/index.ts | 1 + ...ation_ts.rs => extend_expiration_ts_v0.rs} | 33 ++++--- .../src/instructions/delegation/mod.rs | 4 +- programs/helium-sub-daos/src/lib.rs | 4 +- tests/helium-sub-daos.ts | 5 +- 8 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 packages/voter-stake-registry-hooks/src/hooks/useExtendDelegation.ts rename programs/helium-sub-daos/src/instructions/delegation/{add_expiration_ts.rs => extend_expiration_ts_v0.rs} (90%) diff --git a/packages/helium-admin-cli/src/add-expiration-to-delegations.ts b/packages/helium-admin-cli/src/add-expiration-to-delegations.ts index 5d8bb8669..1749738f9 100644 --- a/packages/helium-admin-cli/src/add-expiration-to-delegations.ts +++ b/packages/helium-admin-cli/src/add-expiration-to-delegations.ts @@ -8,6 +8,7 @@ import { min } from "bn.js"; import os from "os"; import yargs from "yargs/yargs"; import { loadKeypair } from "./utils"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; export async function run(args: any = process.argv) { const yarg = yargs(args).options({ @@ -58,17 +59,23 @@ export async function run(args: any = process.argv) { const currTs = await getSolanaUnixTimestamp(provider); const currTsBN = new anchor.BN(currTs.toString()); - const proxyEndTs = proxyConfig.seasons.find(s => currTsBN.gt(s.start))?.end; + const proxyEndTs = proxyConfig.seasons.reverse().find(s => currTsBN.gte(s.start))?.end; for (const [delegation, position] of zip(needsMigration, positionAccs)) { const subDao = delegation.account.subDao; + const positionTokenAccount = ( + await provider.connection.getTokenLargestAccounts(position.mint) + ).value[0].address; instructions.push( await hsdProgram.methods - .addExpirationTs() + .extendExpirationTsV0() .accountsStrict({ payer: wallet.publicKey, position: delegation.account.position, delegatedPosition: delegation.publicKey, registrar: registrarK, + mint: position.mint, + authority: wallet.publicKey, + positionTokenAccount, dao, subDao: delegation.account.subDao, oldClosingTimeSubDaoEpochInfo: subDaoEpochInfoKey( diff --git a/packages/helium-sub-daos-sdk/src/resolvers.ts b/packages/helium-sub-daos-sdk/src/resolvers.ts index 572e45c17..dae783631 100644 --- a/packages/helium-sub-daos-sdk/src/resolvers.ts +++ b/packages/helium-sub-daos-sdk/src/resolvers.ts @@ -262,9 +262,10 @@ export const heliumSubDaosResolvers = combineResolvers( owner: "positionAuthority", }), ataResolver({ + instruction: "extendExpirationTsV0", account: "positionTokenAccount", mint: "mint", - owner: "positionAuthority", + owner: "authority", }), resolveIndividual(async ({ args, path, accounts }) => { if (path[path.length - 1] == "clockwork") { diff --git a/packages/voter-stake-registry-hooks/src/hooks/useExtendDelegation.ts b/packages/voter-stake-registry-hooks/src/hooks/useExtendDelegation.ts new file mode 100644 index 000000000..71cc32be6 --- /dev/null +++ b/packages/voter-stake-registry-hooks/src/hooks/useExtendDelegation.ts @@ -0,0 +1,98 @@ +import { BN, Program } from "@coral-xyz/anchor"; +import { + PROGRAM_ID, + delegatedPositionKey, + init, + subDaoEpochInfoKey, +} from "@helium/helium-sub-daos-sdk"; +import { sendInstructions } from "@helium/spl-utils"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { useAsyncCallback } from "react-async-hook"; +import { useHeliumVsrState } from "../contexts/heliumVsrContext"; +import { PositionWithMeta, SubDaoWithMeta } from "../sdk/types"; +import { fetchBackwardsCompatibleIdl } from "@helium/spl-utils"; +import { PROGRAM_ID as PROXY_PROGRAM_ID, init as initProxy } from "@helium/nft-proxy-sdk"; +import { PROGRAM_ID as VSR_PROGRAM_ID, init as initVsr } from "@helium/voter-stake-registry-sdk"; +import { useSolanaUnixNow } from "@helium/helium-react-hooks"; +export const useExtendDelegation = () => { + const { provider } = useHeliumVsrState(); + const now = useSolanaUnixNow(60 * 5 * 1000) + const { error, loading, execute } = useAsyncCallback( + async ({ + position, + programId = PROGRAM_ID, + onInstructions, + }: { + position: PositionWithMeta; + programId?: PublicKey; + // Instead of sending the transaction, let the caller decide + onInstructions?: ( + instructions: TransactionInstruction[] + ) => Promise; + }) => { + const isInvalid = + !now || !provider || !provider.wallet || !position.isDelegated; + const idl = await fetchBackwardsCompatibleIdl(programId, provider as any); + const hsdProgram = await init(provider as any, programId, idl); + const proxyProgram = await initProxy(provider as any, PROXY_PROGRAM_ID, idl); + const vsrProgram = await initVsr(provider as any, VSR_PROGRAM_ID, idl); + + if (loading) return; + + if (isInvalid || !hsdProgram) { + throw new Error("Unable to extend delegation, Invalid params"); + } else { + const instructions: TransactionInstruction[] = []; + + const delegatedPosKey = delegatedPositionKey(position.pubkey)[0]; + const delegatedPosAcc = + await hsdProgram.account.delegatedPositionV0.fetch(delegatedPosKey); + const registrarAcc = await vsrProgram.account.registrar.fetch( + position.registrar + ); + const proxyConfigAcc = await proxyProgram.account.proxyConfigV0.fetch( + registrarAcc.proxyConfig + ); + const newExpirationTs = proxyConfigAcc.seasons.reverse().find( + (season) => new BN(now!).gte(season.start) + )?.end; + if (!newExpirationTs) { + throw new Error("No new valid expiration ts found"); + } + const oldExpirationTs = delegatedPosAcc.expirationTs; + + const oldSubDaoEpochInfo = subDaoEpochInfoKey( + delegatedPosAcc.subDao, + oldExpirationTs + )[0]; + const newSubDaoEpochInfo = subDaoEpochInfoKey( + delegatedPosAcc.subDao, + newExpirationTs + )[0]; + instructions.push( + await hsdProgram.methods + .extendExpirationTsV0() + .accounts({ + position: position.pubkey, + subDao: delegatedPosAcc.subDao, + oldClosingTimeSubDaoEpochInfo: oldSubDaoEpochInfo, + closingTimeSubDaoEpochInfo: newSubDaoEpochInfo, + }) + .instruction() + ); + + if (onInstructions) { + await onInstructions(instructions); + } else { + await sendInstructions(provider, instructions); + } + } + } + ); + + return { + error, + loading, + delegatePosition: execute, + }; +}; diff --git a/packages/voter-stake-registry-hooks/src/index.ts b/packages/voter-stake-registry-hooks/src/index.ts index c5b8752b4..d8b82581c 100644 --- a/packages/voter-stake-registry-hooks/src/index.ts +++ b/packages/voter-stake-registry-hooks/src/index.ts @@ -10,6 +10,7 @@ export { useClaimPositionRewards } from "./hooks/useClaimPositionRewards"; export { useClosePosition } from "./hooks/useClosePosition"; export { useCreatePosition } from "./hooks/useCreatePosition"; export { useDao } from "./hooks/useDao"; +export { useExtendDelegation } from "./hooks/useExtendDelegation"; export { useDelegatePosition } from "./hooks/useDelegatePosition"; export { useDelegatedPositions } from "./hooks/useDelegatedPositions"; export { useExtendPosition } from "./hooks/useExtendPosition"; diff --git a/programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs b/programs/helium-sub-daos/src/instructions/delegation/extend_expiration_ts_v0.rs similarity index 90% rename from programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs rename to programs/helium-sub-daos/src/instructions/delegation/extend_expiration_ts_v0.rs index a2087e66d..cc90beab3 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/add_expiration_ts.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/extend_expiration_ts_v0.rs @@ -1,27 +1,33 @@ use std::{cmp::min, str::FromStr}; use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, TokenAccount}; use nft_proxy::ProxyConfigV0; use voter_stake_registry::state::{PositionV0, Registrar}; use crate::{ caclulate_vhnt_info, current_epoch, id, DaoV0, DelegatedPositionV0, SubDaoEpochInfoV0, SubDaoV0, - TESTING, }; #[derive(Accounts)] -pub struct AddExpirationTs<'info> { +pub struct ExtendExpirationTsV0<'info> { + #[account(mut)] + pub payer: Signer<'info>, #[account( mut, - address = if TESTING { - payer.key() - } else { - Pubkey::from_str("hprdnjkbziK8NqhThmAn5Gu4XqrBbctX8du4PfJdgvW").unwrap() - } + has_one = mint, )] - pub payer: Signer<'info>, - #[account(mut)] pub position: Account<'info, PositionV0>, + pub mint: Box>, + #[account( + token::mint = mint, + constraint = position_token_account.amount > 0 + )] + pub position_token_account: Box>, + #[account( + constraint = authority.key() == position_token_account.owner || authority.key() == Pubkey::from_str("hprdnjkbziK8NqhThmAn5Gu4XqrBbctX8du4PfJdgvW").unwrap() + )] + pub authority: Signer<'info>, #[account( has_one = proxy_config )] @@ -41,13 +47,16 @@ pub struct AddExpirationTs<'info> { has_one = position, has_one = sub_dao, bump = delegated_position.bump_seed, - constraint = TESTING || delegated_position.expiration_ts == 0 )] pub delegated_position: Account<'info, DelegatedPositionV0>, #[account( mut, seeds = ["sub_dao_epoch_info".as_bytes(), sub_dao.key().as_ref(), ¤t_epoch( - position.lockup.end_ts + if delegated_position.expiration_ts == 0 { + position.lockup.end_ts + } else { + delegated_position.expiration_ts + } ).to_le_bytes()], bump, )] @@ -85,7 +94,7 @@ pub struct AddExpirationTs<'info> { pub system_program: Program<'info, System>, } -pub fn handler(ctx: Context) -> Result<()> { +pub fn handler(ctx: Context) -> Result<()> { 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]; diff --git a/programs/helium-sub-daos/src/instructions/delegation/mod.rs b/programs/helium-sub-daos/src/instructions/delegation/mod.rs index 5c4eefe2f..88327db95 100644 --- a/programs/helium-sub-daos/src/instructions/delegation/mod.rs +++ b/programs/helium-sub-daos/src/instructions/delegation/mod.rs @@ -1,17 +1,17 @@ -pub mod add_expiration_ts; pub mod claim_rewards_v0; pub mod claim_rewards_v1; pub mod close_delegation_v0; pub mod delegate_v0; +pub mod extend_expiration_ts_v0; pub mod reset_lockup_v0; pub mod track_vote_v0; pub mod transfer_v0; -pub use add_expiration_ts::*; pub use claim_rewards_v0::*; pub use claim_rewards_v1::*; pub use close_delegation_v0::*; pub use delegate_v0::*; +pub use extend_expiration_ts_v0::*; pub use reset_lockup_v0::*; pub use track_vote_v0::*; pub use transfer_v0::*; diff --git a/programs/helium-sub-daos/src/lib.rs b/programs/helium-sub-daos/src/lib.rs index 35f300312..1d97b22b6 100644 --- a/programs/helium-sub-daos/src/lib.rs +++ b/programs/helium-sub-daos/src/lib.rs @@ -136,8 +136,8 @@ pub mod helium_sub_daos { initialize_hnt_delegator_pool::handler(ctx) } - pub fn add_expiration_ts(ctx: Context) -> Result<()> { - add_expiration_ts::handler(ctx) + pub fn extend_expiration_ts_v0(ctx: Context) -> Result<()> { + extend_expiration_ts_v0::handler(ctx) } pub fn temp_resize_account(ctx: Context) -> Result<()> { diff --git a/tests/helium-sub-daos.ts b/tests/helium-sub-daos.ts index 5f6cd2ec9..1eac07d21 100644 --- a/tests/helium-sub-daos.ts +++ b/tests/helium-sub-daos.ts @@ -1277,19 +1277,20 @@ describe("helium-sub-daos", () => { const expectedFallRates = subDaoEpochInfo.fallRatesFromClosingPositions.toString(); const expectedVehntInClosingPositions = subDaoEpochInfo.vehntInClosingPositions.toString(); - console.log("dat old one is ", closingTimeSubDaoEpochInfo!.toBase58()); const newClosingTimeSubDaoEpochInfo = subDaoEpochInfoKey( subDao, seasonEnd )[0] await program.methods - .addExpirationTs() + .extendExpirationTsV0() .accounts({ position, subDao, oldClosingTimeSubDaoEpochInfo: closingTimeSubDaoEpochInfo, closingTimeSubDaoEpochInfo: newClosingTimeSubDaoEpochInfo, + authority: positionAuthorityKp.publicKey, }) + .signers([positionAuthorityKp]) .rpc({ skipPreflight: true }); const oldSubDaoEpochInfo = await program.account.subDaoEpochInfoV0.fetch(closingTimeSubDaoEpochInfo!);