Skip to content

Commit c2a75d2

Browse files
authored
Add ability to renew expired/expiring delegations/proxies (#968)
* Add ability to renew proxies and delegations * WIP * Done
1 parent 08a7323 commit c2a75d2

File tree

6 files changed

+174
-2
lines changed

6 files changed

+174
-2
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as anchor from "@coral-xyz/anchor";
2+
import { init as initVsr } from "@helium/voter-stake-registry-sdk";
3+
import os from "os";
4+
import yargs from "yargs";
5+
import { Connection, PublicKey } from "@solana/web3.js";
6+
import fs from "fs";
7+
import { AccountLayout } from "@solana/spl-token";
8+
9+
export async function run(args: any = process.argv) {
10+
const yarg = yargs(args).options({
11+
wallet: {
12+
alias: "k",
13+
describe: "Anchor wallet keypair",
14+
default: `${os.homedir()}/.config/solana/id.json`,
15+
},
16+
url: {
17+
alias: "u",
18+
default: "http://127.0.0.1:8899",
19+
describe: "The solana url",
20+
},
21+
output: {
22+
alias: "o",
23+
describe: "Output file path for staked wallets",
24+
type: "string",
25+
demandOption: true,
26+
},
27+
});
28+
const argv = await yarg.argv;
29+
process.env.ANCHOR_WALLET = argv.wallet;
30+
process.env.ANCHOR_PROVIDER_URL = argv.url;
31+
anchor.setProvider(anchor.AnchorProvider.local(argv.url));
32+
33+
const provider = anchor.getProvider() as anchor.AnchorProvider;
34+
const connection = new Connection(argv.url);
35+
36+
console.log("Initializing VSR program...");
37+
const vsrProgram = await initVsr(provider);
38+
39+
console.log("Fetching all positions...");
40+
const positions = await vsrProgram.account.positionV0.all();
41+
console.log(`Found ${positions.length} total positions`);
42+
43+
// Get unique mints from positions
44+
const mints = [...new Set(positions.filter(p => p.account.lockup.kind.constant || p.account.lockup.endTs.gte(new anchor.BN(Date.now() / 1000))).map(p => p.account.mint.toString()))];
45+
console.log(`Found ${mints.length} unique mints to process`);
46+
47+
// Get token accounts for each mint and extract owner addresses
48+
const stakedWallets = new Set<string>();
49+
50+
const batchSize = 10;
51+
// Process mints in batches of batchSize
52+
for (let i = 0; i < mints.length; i += batchSize) {
53+
const mintBatch = mints.slice(i, i + batchSize);
54+
const tokenAccountsPromises = mintBatch.map(mint =>
55+
connection.getTokenLargestAccounts(new PublicKey(mint))
56+
);
57+
58+
const tokenAccountsResults = await Promise.all(tokenAccountsPromises);
59+
60+
// Collect all token account addresses
61+
const tokenAccountAddresses: PublicKey[] = [];
62+
tokenAccountsResults.forEach(result => {
63+
result.value.forEach(account => {
64+
tokenAccountAddresses.push(account.address);
65+
});
66+
});
67+
68+
// Fetch all token accounts in one batch
69+
const tokenAccountsInfo = await connection.getMultipleAccountsInfo(tokenAccountAddresses);
70+
71+
// Parse token accounts and extract owners
72+
tokenAccountsInfo.forEach((accountInfo, index) => {
73+
if (accountInfo) {
74+
try {
75+
const tokenAccount = AccountLayout.decode(accountInfo.data);
76+
stakedWallets.add(tokenAccount.owner.toString());
77+
} catch (e) {
78+
console.error(`Failed to parse token account at index ${index}`);
79+
}
80+
}
81+
});
82+
83+
console.log(`Processed ${i / batchSize} of ${mints.length / batchSize} batches`);
84+
}
85+
86+
// Convert Set to Array and write to file
87+
const stakedWalletsArray = Array.from(stakedWallets);
88+
console.log(`\nWriting ${stakedWalletsArray.length} unique staked wallets to ${argv.output}`);
89+
fs.writeFileSync(argv.output, JSON.stringify(stakedWalletsArray, null, 2));
90+
91+
console.log("Done!");
92+
}

packages/voter-stake-registry-hooks/src/contexts/heliumVsrContext.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
delegatedPositionKey,
66
} from "@helium/helium-sub-daos-sdk";
77
import { VoterStakeRegistry } from "@helium/idls/lib/types/voter_stake_registry";
8-
import { init as initNftProxy } from "@helium/nft-proxy-sdk";
8+
import { init as initNftProxy, proxyConfigKey } from "@helium/nft-proxy-sdk";
99
import { truthy } from "@helium/spl-utils";
1010
import { VoteService, init } from "@helium/voter-stake-registry-sdk";
1111
import { Connection, PublicKey } from "@solana/web3.js";
@@ -18,6 +18,7 @@ import { useRegistrar } from "../hooks/useRegistrar";
1818
import { PositionWithMeta, ProxyAssignmentV0 } from "../sdk/types";
1919
import { calcPositionVotingPower } from "../utils/calcPositionVotingPower";
2020
import { useRegistrarForMint } from "../hooks/useRegistrarForMint";
21+
import { useProxyConfig } from "../hooks/useProxyConfig";
2122

2223
type Registrar = IdlAccounts<VoterStakeRegistry>["registrar"];
2324

@@ -138,6 +139,8 @@ export const HeliumVsrStateProvider: React.FC<{
138139
const { accounts: delegatedAccounts, loading: loadingDel } =
139140
useDelegatedPositions(delegatedPositionKeys);
140141

142+
const { info: proxyConfig } = useProxyConfig(registrar?.proxyConfig);
143+
141144
const proxyAccountsByAsset = useMemo(() => {
142145
return proxyAccounts?.reduce((acc, prox) => {
143146
acc[prox.asset.toBase58()] = prox;
@@ -236,6 +239,23 @@ export const HeliumVsrStateProvider: React.FC<{
236239

237240
votingPower = votingPower.add(posVotingPower);
238241

242+
const proxyExpiration = proxy?.expirationTs;
243+
const delegationExpiration = delegatedAccounts?.[
244+
idx
245+
]?.info?.expirationTs;
246+
const isProxyExpired = proxyExpiration?.lt(new BN(now));
247+
const isDelegationExpired = delegationExpiration?.lt(new BN(now));
248+
const currentSeason = proxyConfig?.seasons
249+
?.reverse()
250+
?.find((season) => season.start.lte(new BN(now)));
251+
252+
const isProxyRenewable =
253+
proxyExpiration && !isProxyExpired && currentSeason?.end.gt(proxyExpiration);
254+
const isDelegationRenewable =
255+
delegationExpiration &&
256+
!isDelegationExpired &&
257+
currentSeason?.end.gt(delegationExpiration);
258+
239259
return {
240260
...position.info,
241261
pubkey: position?.publicKey,
@@ -247,6 +267,10 @@ export const HeliumVsrStateProvider: React.FC<{
247267
votingMint: mintCfgs[position.info.votingMintConfigIdx],
248268
isProxiedToMe,
249269
proxy,
270+
isProxyExpired,
271+
isDelegationExpired,
272+
isProxyRenewable,
273+
isDelegationRenewable,
250274
} as PositionWithMeta;
251275
}
252276
})

packages/voter-stake-registry-hooks/src/hooks/useAssignProxies.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ export const useAssignProxies = () => {
127127
const subInstructions: TransactionInstruction[] = [];
128128
if (
129129
proxyAssignment &&
130-
!proxyAssignment.nextVoter?.equals(PublicKey.default)
130+
!proxyAssignment.nextVoter?.equals(PublicKey.default) &&
131+
// Only unassign if the proxy is actually changing
132+
!proxyAssignment.nextVoter.equals(recipient)
131133
) {
132134
const toUndelegate =
133135
await voteService.getProxyAssignmentsForPosition(

packages/voter-stake-registry-hooks/src/hooks/useDelegatePosition.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
PROGRAM_ID,
44
delegatedPositionKey,
55
init,
6+
subDaoEpochInfoKey,
67
} from "@helium/helium-sub-daos-sdk";
78
import { HNT_MINT, sendInstructions } from "@helium/spl-utils";
89
import { init as initHplCrons } from "@helium/hpl-crons-sdk";
@@ -26,6 +27,9 @@ import {
2627
import { useDelegatedPosition } from "./useDelegatedPosition";
2728
import { nextAvailableTaskIds, taskKey } from "@helium/tuktuk-sdk";
2829
import { PREPAID_TX_FEES, usePositionFees } from "./usePositionFees";
30+
import { useRegistrar } from "./useRegistrar";
31+
import { useProxyConfig } from "./useProxyConfig";
32+
import { BN } from "@coral-xyz/anchor";
2933

3034
export const useDelegatePosition = ({
3135
automationEnabled = false,
@@ -48,6 +52,8 @@ export const useDelegatePosition = ({
4852
useDelegationClaimBot(delegationClaimBotK);
4953
const { info: delegatedPositionAcc } = useDelegatedPosition(delegatedPosKey);
5054
const { info: taskQueue } = useTaskQueue(TASK_QUEUE);
55+
const { info: registrar } = useRegistrar(position.registrar);
56+
const { info: proxyConfig } = useProxyConfig(registrar?.proxyConfig);
5157

5258
const { rentFee, prepaidTxFees, insufficientBalance } = usePositionFees({
5359
automationEnabled,
@@ -106,6 +112,38 @@ export const useDelegatePosition = ({
106112
})
107113
.instruction()
108114
);
115+
} else if (position.isDelegated && position.isDelegationRenewable) {
116+
const delegatedPosKey = delegatedPositionKey(position.pubkey)[0];
117+
const delegatedPosAcc =
118+
await hsdProgram.account.delegatedPositionV0.fetch(delegatedPosKey);
119+
const now = new BN(Date.now() / 1000);
120+
const newExpirationTs = proxyConfig?.seasons.reverse().find(
121+
(season) => now.gte(season.start)
122+
)?.end;
123+
if (!newExpirationTs) {
124+
throw new Error("No new valid expiration ts found");
125+
}
126+
const oldExpirationTs = delegatedPosAcc.expirationTs;
127+
128+
const oldSubDaoEpochInfo = subDaoEpochInfoKey(
129+
delegatedPosAcc.subDao,
130+
oldExpirationTs
131+
)[0];
132+
const newSubDaoEpochInfo = subDaoEpochInfoKey(
133+
delegatedPosAcc.subDao,
134+
newExpirationTs
135+
)[0];
136+
instructions.push(
137+
await hsdProgram.methods
138+
.extendExpirationTsV0()
139+
.accountsPartial({
140+
position: position.pubkey,
141+
subDao: delegatedPosAcc.subDao,
142+
oldClosingTimeSubDaoEpochInfo: oldSubDaoEpochInfo,
143+
closingTimeSubDaoEpochInfo: newSubDaoEpochInfo,
144+
})
145+
.instruction()
146+
);
109147
}
110148
}
111149

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useAnchorAccount, useAnchorAccounts } from "@helium/helium-react-hooks";
2+
import { PublicKey } from "@solana/web3.js";
3+
import { NftProxy } from "@helium/modular-governance-idls/lib/types/nft_proxy"
4+
5+
export const useProxyConfig = (proxyConfigKey: PublicKey | undefined) => {
6+
return useAnchorAccount<NftProxy, "proxyConfigV0">(
7+
proxyConfigKey,
8+
"proxyConfigV0"
9+
);
10+
};

packages/voter-stake-registry-hooks/src/sdk/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export interface PositionWithMeta extends Position {
2929
votingPower: BN
3030
votingMint: VotingMintConfig
3131
proxy: Proxy | null
32+
isProxyExpired: boolean,
33+
isDelegationExpired: boolean,
34+
/// Whether the proxy is within the 30 day renewal period
35+
isProxyRenewable: boolean,
36+
/// Whether the delegation is within the 30 day renewal period
37+
isDelegationRenewable: boolean,
3238
}
3339
export type LockupKind = IdlTypes<HeliumVoterStakeRegistry>['lockupKind']
3440
/* export type InitializePositionV0Args = IdlTypes<HeliumVoterStakeRegistry>['InitializePositionArgsV0']

0 commit comments

Comments
 (0)