-
Notifications
You must be signed in to change notification settings - Fork 557
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
582 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import {Client} from "@solana/governance-program-library"; | ||
import {PublicKey, TransactionInstruction} from "@solana/web3.js"; | ||
import BN from "bn.js"; | ||
import {StakeAccount, StakeConnection} from "@parcl-oss/staking"; | ||
import {Provider, Wallet} from "@coral-xyz/anchor"; | ||
import {VoterWeightAction} from "@solana/spl-governance"; | ||
import {convertVoterWeightActionToType} from "../VoterWeightPlugins/lib/utils"; | ||
import queryClient from "@hooks/queries/queryClient"; | ||
|
||
// A wrapper for the StakeConnection from @parcl-oss/staking, that implements the generic plugin client interface | ||
export class ParclVoterWeightPluginClient extends Client<any> { | ||
readonly requiresInputVoterWeight = false; | ||
// The parcl plugin does not have a registrar account | ||
async getRegistrarAccount(): Promise<null> { | ||
return null; | ||
} | ||
|
||
async getMaxVoterWeightRecordPDA() { | ||
const maxVoterWeightPk = (await this.client.program.methods.updateMaxVoterWeight().pubkeys()).maxVoterRecord | ||
|
||
if (!maxVoterWeightPk) return null; | ||
|
||
return { | ||
maxVoterWeightPk, | ||
maxVoterWeightRecordBump: 0 // This is wrong for Parcl - but it doesn't matter as it is not used | ||
} | ||
} | ||
|
||
async getVoterWeightRecordPDA(realm: PublicKey, mint: PublicKey, voter: PublicKey) { | ||
const { voterWeightAccount } = await this.getUpdateVoterWeightPks([], voter, VoterWeightAction.CastVote, PublicKey.default); | ||
|
||
return { | ||
voterWeightPk: voterWeightAccount, | ||
voterWeightRecordBump: 0 // This is wrong for Parcl - but it doesn't matter as it is not used | ||
}; | ||
} | ||
|
||
// NO-OP Parcl records are created through the Parcl dApp. | ||
async createVoterWeightRecord(): Promise<TransactionInstruction | null> { | ||
return null; | ||
} | ||
|
||
// NO-OP | ||
async createMaxVoterWeightRecord(): Promise<TransactionInstruction | null> { | ||
return null; | ||
} | ||
|
||
private async getStakeAccount(voter: PublicKey): Promise<StakeAccount> { | ||
return queryClient.fetchQuery({ | ||
queryKey: ['parcl getStakeAccount', voter], | ||
queryFn: () => this.client.getMainAccount(voter), | ||
}) | ||
} | ||
|
||
private async getUpdateVoterWeightPks(instructions: TransactionInstruction[], voter: PublicKey, action: VoterWeightAction, target?: PublicKey) { | ||
const stakeAccount = await this.getStakeAccount(voter) | ||
|
||
if (!stakeAccount) throw new Error("Stake account not found for voter " + voter.toString()); | ||
return this.client.withUpdateVoterWeight( | ||
instructions, | ||
stakeAccount, | ||
{ [convertVoterWeightActionToType(action)]: {} } as any, | ||
target | ||
); | ||
} | ||
|
||
async updateVoterWeightRecord(voter: PublicKey, realm: PublicKey, mint: PublicKey, action: VoterWeightAction, inputRecordCallback?: () => Promise<PublicKey>, target?: PublicKey) { | ||
const instructions: TransactionInstruction[] = []; | ||
await this.getUpdateVoterWeightPks(instructions, voter, action, target); | ||
|
||
return { pre: instructions }; | ||
} | ||
// NO-OP | ||
async updateMaxVoterWeightRecord(): Promise<TransactionInstruction | null> { | ||
return null; | ||
} | ||
async calculateVoterWeight(voter: PublicKey): Promise<BN | null> { | ||
const stakeAccount = await this.getStakeAccount(voter) | ||
|
||
if (stakeAccount) { | ||
return stakeAccount.getVoterWeight(await this.client.getTime()).toBN() | ||
} else { | ||
return new BN(0) | ||
} | ||
} | ||
constructor(program: typeof StakeConnection.prototype.program, private client: StakeConnection, devnet:boolean) { | ||
super(program, devnet); | ||
} | ||
|
||
static async connect(provider: Provider, devnet = false, wallet: Wallet): Promise<ParclVoterWeightPluginClient> { | ||
const parclClient = await StakeConnection.connect( | ||
provider.connection, | ||
wallet | ||
) | ||
|
||
return new ParclVoterWeightPluginClient(parclClient.program, parclClient, devnet); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { MintInfo } from '@solana/spl-token' | ||
import BN from 'bn.js' | ||
import { GoverningTokenType } from '@solana/spl-governance' | ||
import { fmtMintAmount } from '@tools/sdk/units' | ||
import { useEffect } from 'react' | ||
import useWalletOnePointOh from '@hooks/useWalletOnePointOh' | ||
import { useUserCommunityTokenOwnerRecord } from '@hooks/queries/tokenOwnerRecord' | ||
import { useRealmConfigQuery } from '@hooks/queries/realmConfig' | ||
import { useRealmCommunityMintInfoQuery, useRealmCouncilMintInfoQuery } from '@hooks/queries/mintInfo' | ||
import ParclVotingPower from './ParclVotingPower' | ||
import { useUserTokenAccountsQuery } from '@hooks/queries/tokenAccount' | ||
import { useRealmQuery } from '@hooks/queries/realm' | ||
import { PublicKey } from '@solana/web3.js' | ||
import VanillaWithdrawTokensButton from '@components/TokenBalance/VanillaWithdrawTokensButton' | ||
import { Deposit } from '@components/GovernancePower/Power/Vanilla/Deposit' | ||
|
||
export const PARCL_INSTRUCTIONS = | ||
'You can deposit PRCL tokens at https://app.parcl.co/staking' | ||
|
||
const TokenDeposit = ({ | ||
mintInfo, | ||
mintAddress, | ||
inAccountDetails, | ||
setHasGovPower, | ||
role | ||
}: { | ||
mintInfo: MintInfo | undefined, | ||
mintAddress: PublicKey, | ||
inAccountDetails?: boolean, | ||
role: 'council' | 'community', | ||
setHasGovPower?: (hasGovPower: boolean) => void | ||
}) => { | ||
const wallet = useWalletOnePointOh() | ||
const { data: tokenAccounts } = useUserTokenAccountsQuery() | ||
const connected = !!wallet?.connected | ||
|
||
const ownTokenRecord = useUserCommunityTokenOwnerRecord().data?.result | ||
const config = useRealmConfigQuery().data?.result | ||
|
||
const relevantTokenConfig = role === "community" | ||
? config?.account.communityTokenConfig | ||
: config?.account.councilTokenConfig; | ||
|
||
const isMembership = | ||
relevantTokenConfig?.tokenType === GoverningTokenType.Membership | ||
const isDormant = | ||
relevantTokenConfig?.tokenType === GoverningTokenType.Dormant; | ||
|
||
const depositTokenRecord = ownTokenRecord | ||
const depositTokenAccount = tokenAccounts?.find((a) => | ||
a.account.mint.equals(mintAddress) | ||
); | ||
|
||
const hasTokensInWallet = | ||
depositTokenAccount && depositTokenAccount.account.amount.gt(new BN(0)) | ||
|
||
const hasTokensDeposited = | ||
depositTokenRecord && | ||
depositTokenRecord.account.governingTokenDepositAmount.gt(new BN(0)) | ||
|
||
const availableTokens = | ||
depositTokenRecord && mintInfo | ||
? fmtMintAmount( | ||
mintInfo, | ||
depositTokenRecord.account.governingTokenDepositAmount | ||
) | ||
: '0' | ||
|
||
useEffect(() => { | ||
if (availableTokens != '0' || hasTokensDeposited || hasTokensInWallet) { | ||
if (setHasGovPower) setHasGovPower(true) | ||
} | ||
}, [availableTokens, hasTokensDeposited, hasTokensInWallet, setHasGovPower]) | ||
|
||
const canShowAvailableTokensMessage = hasTokensInWallet && connected | ||
const tokensToShow = | ||
hasTokensInWallet && depositTokenAccount | ||
? fmtMintAmount(mintInfo, depositTokenAccount.account.amount) | ||
: hasTokensInWallet | ||
? availableTokens | ||
: 0 | ||
|
||
// Do not show deposits for mints with zero supply because nobody can deposit anyway | ||
if (!mintInfo || mintInfo.supply.isZero()) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div className="w-full"> | ||
{(availableTokens != '0' || inAccountDetails) && ( | ||
<div className="flex items-center space-x-4"> | ||
<ParclVotingPower className="w-full" role={role} /> | ||
</div> | ||
)} | ||
<div | ||
className={`my-4 opacity-70 text-xs ${ | ||
canShowAvailableTokensMessage ? 'block' : 'hidden' | ||
}`} | ||
> | ||
You have {tokensToShow} {hasTokensDeposited ? `more ` : ``} tokens | ||
available to deposit. | ||
</div> | ||
{ | ||
role === "community" | ||
? <div className={`my-4 opacity-70 text-xs`}>{PARCL_INSTRUCTIONS}</div> | ||
: null | ||
} | ||
{ | ||
!isDormant | ||
? <Deposit role="council" /> | ||
: null | ||
} | ||
<div className="flex flex-col mt-6 space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"> | ||
{!isMembership && // Membership tokens can't be withdrawn (that is their whole point, actually) | ||
!isDormant && | ||
inAccountDetails && ( | ||
<VanillaWithdrawTokensButton | ||
role={ | ||
'council' | ||
} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
const ParclAccountDetails = () => { | ||
const realm = useRealmQuery().data?.result | ||
const communityMint = useRealmCommunityMintInfoQuery().data?.result | ||
const councilMint = useRealmCouncilMintInfoQuery().data?.result; | ||
const wallet = useWalletOnePointOh() | ||
const connected = !!wallet?.connected | ||
const councilMintAddress = realm?.account.config.councilMint; | ||
const hasLoaded = communityMint && councilMint && realm && councilMintAddress; | ||
|
||
return ( | ||
<> | ||
{hasLoaded ? ( | ||
<div className={`${`flex flex-col w-full`}`}> | ||
{!connected ? ( | ||
<div className={'text-xs text-white/50 mt-8'}> | ||
Connect your wallet to see governance power | ||
</div> | ||
) : | ||
( | ||
<> | ||
<TokenDeposit mintInfo={communityMint} mintAddress={realm.account.communityMint} role={"community"} inAccountDetails={true} /> | ||
<TokenDeposit mintInfo={councilMint} mintAddress={councilMintAddress} role={"council"} inAccountDetails={true} /> | ||
</> | ||
)} | ||
</div> | ||
) : ( | ||
<> | ||
<div className="h-12 mb-4 rounded-lg animate-pulse bg-bkg-3" /> | ||
<div className="h-10 rounded-lg animate-pulse bg-bkg-3" /> | ||
</> | ||
)} | ||
</> | ||
) | ||
} | ||
|
||
export default ParclAccountDetails |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { BigNumber } from 'bignumber.js' | ||
import { useMemo } from 'react' | ||
import { useRealmQuery } from '@hooks/queries/realm' | ||
import { useMintInfoByPubkeyQuery } from '@hooks/queries/mintInfo' | ||
import { useConnection } from '@solana/wallet-adapter-react' | ||
import { getParclGovPower } from '@hooks/queries/governancePower' | ||
import { useAsync } from 'react-async-hook' | ||
import BN from 'bn.js' | ||
import { getMintMetadata } from '@components/instructions/programs/splToken' | ||
import VotingPowerPct from '@components/ProposalVotingPower/VotingPowerPct' | ||
import clsx from 'clsx' | ||
import { useRealmConfigQuery } from '@hooks/queries/realmConfig' | ||
import { GoverningTokenType } from '@solana/spl-governance' | ||
import useParclScalingFactor from '@hooks/parcl/useScalingFactor' | ||
import useWalletOnePointOh from '@hooks/useWalletOnePointOh' | ||
import { useUserTokenAccountsQuery } from '@hooks/queries/tokenAccount' | ||
|
||
interface Props { | ||
className?: string | ||
role: 'community' | 'council' | ||
hideIfZero?: boolean | ||
children?: React.ReactNode | ||
} | ||
|
||
export default function ParclVotingPower({ | ||
role, | ||
hideIfZero, | ||
children, | ||
...props | ||
}: Props) { | ||
const realm = useRealmQuery().data?.result | ||
const realmConfig = useRealmConfigQuery().data?.result | ||
const { data: tokenAccounts } = useUserTokenAccountsQuery() | ||
const wallet = useWalletOnePointOh() | ||
|
||
const { connection } = useConnection() | ||
|
||
const relevantMint = | ||
role === 'community' | ||
? realm?.account.communityMint | ||
: realm?.account.config.councilMint | ||
|
||
const mintInfo = useMintInfoByPubkeyQuery(relevantMint).data?.result | ||
|
||
const { result: personalAmount } = useAsync( | ||
async () => | ||
wallet?.publicKey && role === 'community' | ||
? getParclGovPower(connection, wallet.publicKey) | ||
: tokenAccounts?.find( | ||
(a) => relevantMint && a.account.mint.equals(relevantMint) | ||
)?.account.amount as BN, | ||
[connection, wallet, role, relevantMint, tokenAccounts] | ||
) | ||
|
||
const parclScalingFactor = useParclScalingFactor() ?? 1; | ||
|
||
const totalAmount = personalAmount ?? new BN(0) | ||
|
||
const formattedTotal = useMemo( | ||
() => | ||
mintInfo && totalAmount !== undefined | ||
? new BigNumber(totalAmount.toString()) | ||
.multipliedBy( | ||
role === 'community' | ||
? parclScalingFactor | ||
: 1 | ||
) | ||
.shiftedBy(-mintInfo.decimals) | ||
.integerValue() | ||
.toString() | ||
: undefined, | ||
[totalAmount, mintInfo] | ||
) | ||
|
||
const tokenName = | ||
getMintMetadata(relevantMint)?.name ?? realm?.account.name ?? '' | ||
|
||
const disabled = | ||
realmConfig?.account.councilTokenConfig.tokenType === | ||
GoverningTokenType.Dormant | ||
|
||
return ( | ||
<div | ||
className={clsx( | ||
props.className, | ||
hideIfZero && totalAmount.isZero() && 'hidden', | ||
disabled && 'hidden' | ||
)} | ||
> | ||
<div className={'p-3 rounded-md bg-bkg-1'}> | ||
<div className="text-fgd-3 text-xs"> | ||
{tokenName} | ||
{role === 'council' ? ' Council' : ''} Votes | ||
</div> | ||
<div className="flex items-center justify-between mt-1"> | ||
<div className=" flex flex-row gap-x-2"> | ||
<div className="text-xl font-bold text-fgd-1 hero-text"> | ||
{formattedTotal ?? 0} | ||
</div> | ||
</div> | ||
|
||
{mintInfo && ( | ||
<VotingPowerPct | ||
amount={new BigNumber(totalAmount.toString())} | ||
total={new BigNumber(mintInfo.supply.toString())} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
{children} | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.