mirror of https://github.com/certusone/oyster.git
fix: Do not show Yes/No buttons when voter record can't be loaded
This commit is contained in:
parent
e05ecbc9a2
commit
94ff7880ff
|
@ -12,12 +12,13 @@ import { ParsedAccount } from '@oyster/common';
|
|||
import { MemcmpFilter, getGovernanceAccounts } from '../models/api';
|
||||
import { useAccountChangeTracker } from '../contexts/GovernanceContext';
|
||||
import { useRpcContext } from './useRpcContext';
|
||||
import { none, Option, some } from '../tools/option';
|
||||
|
||||
// Fetches Governance program account using the given key and subscribes to updates
|
||||
export function useGovernanceAccountByPubkey<
|
||||
TAccount extends GovernanceAccount
|
||||
>(accountClass: GovernanceAccountClass, pubkey: PublicKey | undefined) {
|
||||
const [account, setAccount] = useState<ParsedAccount<TAccount>>();
|
||||
const [account, setAccount] = useState<Option<ParsedAccount<TAccount>>>();
|
||||
|
||||
const { connection, endpoint, programId } = useRpcContext();
|
||||
|
||||
|
@ -37,13 +38,13 @@ export function useGovernanceAccountByPubkey<
|
|||
pubkey,
|
||||
accountInfo!,
|
||||
);
|
||||
setAccount(loadedAccount);
|
||||
setAccount(some(loadedAccount));
|
||||
} else {
|
||||
setAccount(undefined);
|
||||
setAccount(none());
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(`Can't load ${pubkey.toBase58()} account`, ex);
|
||||
setAccount(undefined);
|
||||
setAccount(none());
|
||||
}
|
||||
|
||||
return connection.onProgramAccountChange(programId, info => {
|
||||
|
@ -53,7 +54,7 @@ export function useGovernanceAccountByPubkey<
|
|||
info.accountInfo,
|
||||
) as ParsedAccount<TAccount>;
|
||||
|
||||
setAccount(account);
|
||||
setAccount(some(account));
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -124,7 +125,7 @@ export function useGovernanceAccountsByFilter<
|
|||
);
|
||||
setAccounts(loadedAccounts);
|
||||
} catch (ex) {
|
||||
console.error(`Can't load ${accountClass}`, ex);
|
||||
console.error(`Can't load ${accountClass.name}`, ex);
|
||||
setAccounts({});
|
||||
}
|
||||
|
||||
|
@ -214,6 +215,6 @@ export function useGovernanceAccountByFilter<
|
|||
}
|
||||
|
||||
throw new Error(
|
||||
`Filters ${filters} returned multiple accounts ${accounts} for ${accountClass} while a single result was expected`,
|
||||
`Filters ${filters} returned multiple accounts ${accounts} for ${accountClass.name} while a single result was expected`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { PublicKey } from '@solana/web3.js';
|
|||
import {
|
||||
getSignatoryRecordAddress,
|
||||
getTokenOwnerAddress,
|
||||
getVoteRecordAddress,
|
||||
Governance,
|
||||
Proposal,
|
||||
ProposalInstruction,
|
||||
|
@ -12,7 +13,6 @@ import {
|
|||
} from '../models/accounts';
|
||||
import { pubkeyFilter } from '../models/api';
|
||||
import {
|
||||
useGovernanceAccountByFilter,
|
||||
useGovernanceAccountByPda,
|
||||
useGovernanceAccountByPubkey,
|
||||
useGovernanceAccountsByFilter,
|
||||
|
@ -24,7 +24,10 @@ import { useRpcContext } from './useRpcContext';
|
|||
// ----- Governance -----
|
||||
|
||||
export function useGovernance(governance: PublicKey | undefined) {
|
||||
return useGovernanceAccountByPubkey<Governance>(Governance, governance);
|
||||
return useGovernanceAccountByPubkey<Governance>(
|
||||
Governance,
|
||||
governance,
|
||||
)?.tryUnwrap();
|
||||
}
|
||||
|
||||
export function useGovernancesByRealm(realm: PublicKey | undefined) {
|
||||
|
@ -36,7 +39,10 @@ export function useGovernancesByRealm(realm: PublicKey | undefined) {
|
|||
// ----- Proposal -----
|
||||
|
||||
export function useProposal(proposal: PublicKey | undefined) {
|
||||
return useGovernanceAccountByPubkey<Proposal>(Proposal, proposal);
|
||||
return useGovernanceAccountByPubkey<Proposal>(
|
||||
Proposal,
|
||||
proposal,
|
||||
)?.tryUnwrap();
|
||||
}
|
||||
|
||||
export function useProposalsByGovernance(governance: PublicKey | undefined) {
|
||||
|
@ -85,7 +91,7 @@ export function useWalletTokenOwnerRecord(
|
|||
);
|
||||
},
|
||||
[wallet?.publicKey, governingTokenMint, realm],
|
||||
);
|
||||
)?.tryUnwrap();
|
||||
}
|
||||
|
||||
/// Returns all TokenOwnerRecords for the current wallet
|
||||
|
@ -102,12 +108,12 @@ export function useProposalAuthority(proposalOwner: PublicKey | undefined) {
|
|||
const tokenOwnerRecord = useTokenOwnerRecord(proposalOwner);
|
||||
|
||||
return connected &&
|
||||
tokenOwnerRecord &&
|
||||
(tokenOwnerRecord.info.governingTokenOwner.toBase58() ===
|
||||
tokenOwnerRecord?.isSome() &&
|
||||
(tokenOwnerRecord.value.info.governingTokenOwner.toBase58() ===
|
||||
wallet?.publicKey?.toBase58() ||
|
||||
tokenOwnerRecord.info.governanceDelegate?.toBase58() ===
|
||||
tokenOwnerRecord.value.info.governanceDelegate?.toBase58() ===
|
||||
wallet?.publicKey?.toBase58())
|
||||
? tokenOwnerRecord
|
||||
? tokenOwnerRecord?.tryUnwrap()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
|
@ -130,7 +136,7 @@ export function useWalletSignatoryRecord(proposal: PublicKey) {
|
|||
);
|
||||
},
|
||||
[wallet?.publicKey, proposal],
|
||||
);
|
||||
)?.tryUnwrap();
|
||||
}
|
||||
|
||||
export function useSignatoriesByProposal(proposal: PublicKey | undefined) {
|
||||
|
@ -156,11 +162,21 @@ export const useVoteRecordsByProposal = (proposal: PublicKey | undefined) => {
|
|||
]);
|
||||
};
|
||||
|
||||
export const useWalletVoteRecord = (proposal: PublicKey) => {
|
||||
const { wallet } = useWallet();
|
||||
export const useTokenOwnerVoteRecord = (
|
||||
proposal: PublicKey,
|
||||
tokenOwnerRecord: PublicKey,
|
||||
) => {
|
||||
const { programId } = useRpcContext();
|
||||
|
||||
return useGovernanceAccountByFilter<VoteRecord>(VoteRecord, [
|
||||
pubkeyFilter(1, proposal),
|
||||
pubkeyFilter(1 + 32, wallet?.publicKey),
|
||||
]);
|
||||
return useGovernanceAccountByPda<VoteRecord>(
|
||||
VoteRecord,
|
||||
async () => {
|
||||
if (!proposal || !tokenOwnerRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await getVoteRecordAddress(programId, proposal, tokenOwnerRecord);
|
||||
},
|
||||
[tokenOwnerRecord.toBase58(), proposal],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -595,6 +595,23 @@ export class VoteRecord {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getVoteRecordAddress(
|
||||
programId: PublicKey,
|
||||
proposal: PublicKey,
|
||||
tokenOwnerRecord: PublicKey,
|
||||
) {
|
||||
const [voteRecordAddress] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from(GOVERNANCE_PROGRAM_SEED),
|
||||
proposal.toBuffer(),
|
||||
tokenOwnerRecord.toBuffer(),
|
||||
],
|
||||
programId,
|
||||
);
|
||||
|
||||
return voteRecordAddress;
|
||||
}
|
||||
|
||||
export class AccountMetaData {
|
||||
pubkey: PublicKey;
|
||||
isSigner: boolean;
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { GOVERNANCE_SCHEMA } from './serialisation';
|
||||
import { serialize } from 'borsh';
|
||||
import { CastVoteArgs, Vote } from './instructions';
|
||||
import { GOVERNANCE_PROGRAM_SEED } from './accounts';
|
||||
import { getVoteRecordAddress } from './accounts';
|
||||
|
||||
export const withCastVote = async (
|
||||
instructions: TransactionInstruction[],
|
||||
|
@ -27,13 +27,10 @@ export const withCastVote = async (
|
|||
const args = new CastVoteArgs({ vote });
|
||||
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args));
|
||||
|
||||
const [voteRecordAddress] = await PublicKey.findProgramAddress(
|
||||
[
|
||||
Buffer.from(GOVERNANCE_PROGRAM_SEED),
|
||||
proposal.toBuffer(),
|
||||
tokenOwnerRecord.toBuffer(),
|
||||
],
|
||||
const voteRecordAddress = await getVoteRecordAddress(
|
||||
programId,
|
||||
proposal,
|
||||
tokenOwnerRecord,
|
||||
);
|
||||
|
||||
const keys = [
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
// Rust style Option with typeScript twist inspired by https://gist.github.com/s-panferov/575da5a7131c285c0539
|
||||
|
||||
export interface Option<T> {
|
||||
map<U>(fn: (a: T) => U): Option<U>;
|
||||
isSome(): this is Some<T>;
|
||||
isNone(): this is None<T>;
|
||||
unwrap(): T;
|
||||
tryUnwrap(): T | undefined;
|
||||
}
|
||||
|
||||
export class Some<T> implements Option<T> {
|
||||
value: T;
|
||||
|
||||
map<U>(fn: (a: T) => U): Option<U> {
|
||||
return new Some(fn(this.value));
|
||||
}
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
isSome(): this is Some<T> {
|
||||
return true;
|
||||
}
|
||||
|
||||
isNone(): this is None<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
tryUnwrap(): T | undefined {
|
||||
return this.unwrap();
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Some(${this.value})`;
|
||||
}
|
||||
}
|
||||
|
||||
export function some<T>(value: T) {
|
||||
return new Some<T>(value);
|
||||
}
|
||||
|
||||
export function none<T>() {
|
||||
return new None<T>();
|
||||
}
|
||||
|
||||
export class None<T> implements Option<T> {
|
||||
map<U>(fn: (a: T) => U): Option<U> {
|
||||
return new None<U>();
|
||||
}
|
||||
|
||||
isSome(): this is Some<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
isNone(): this is None<T> {
|
||||
return true;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
throw new Error('None has no value');
|
||||
}
|
||||
|
||||
tryUnwrap(): T | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'None';
|
||||
}
|
||||
}
|
|
@ -45,9 +45,7 @@ export const GovernanceView = () => {
|
|||
const token = tokenMap.get(
|
||||
realm?.info.communityMint?.toBase58() || '',
|
||||
) as any;
|
||||
const tokenBackground =
|
||||
token?.extensions?.background ||
|
||||
'https://solana.com/static/8c151e179d2d7e80255bdae6563209f2/6833b/validators.webp';
|
||||
const tokenBackground = token?.extensions?.background;
|
||||
|
||||
const communityMint = realm?.info.communityMint?.toBase58() || '';
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Vote } from '../../../../models/instructions';
|
|||
|
||||
import { castVote } from '../../../../actions/castVote';
|
||||
import { useHasVoteTimeExpired } from '../../../../hooks/useHasVoteTimeExpired';
|
||||
import { useWalletVoteRecord } from '../../../../hooks/apiHooks';
|
||||
import { useTokenOwnerVoteRecord } from '../../../../hooks/apiHooks';
|
||||
import { useRpcContext } from '../../../../hooks/useRpcContext';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
@ -35,12 +35,15 @@ export function CastVoteButton({
|
|||
vote: Vote;
|
||||
}) {
|
||||
const rpcContext = useRpcContext();
|
||||
const voteRecord = useWalletVoteRecord(proposal.pubkey);
|
||||
const voteRecord = useTokenOwnerVoteRecord(
|
||||
proposal.pubkey,
|
||||
tokenOwnerRecord.pubkey,
|
||||
);
|
||||
const hasVoteTimeExpired = useHasVoteTimeExpired(governance, proposal);
|
||||
|
||||
const isVisible =
|
||||
hasVoteTimeExpired === false &&
|
||||
!voteRecord &&
|
||||
voteRecord?.isNone() &&
|
||||
tokenOwnerRecord &&
|
||||
!tokenOwnerRecord.info.governingTokenDepositAmount.isZero() &&
|
||||
proposal.info.state === ProposalState.Voting;
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from '../../../../models/accounts';
|
||||
import { useAccountChangeTracker } from '../../../../contexts/GovernanceContext';
|
||||
import { relinquishVote } from '../../../../actions/relinquishVote';
|
||||
import { useWalletVoteRecord } from '../../../../hooks/apiHooks';
|
||||
import { useTokenOwnerVoteRecord } from '../../../../hooks/apiHooks';
|
||||
import { useRpcContext } from '../../../../hooks/useRpcContext';
|
||||
|
||||
const { useWallet } = contexts.Wallet;
|
||||
|
@ -30,7 +30,10 @@ export function RelinquishVoteButton({
|
|||
const { connected } = useWallet();
|
||||
const rpcContext = useRpcContext();
|
||||
|
||||
const voteRecord = useWalletVoteRecord(proposal.pubkey);
|
||||
const voteRecord = useTokenOwnerVoteRecord(
|
||||
proposal.pubkey,
|
||||
tokenOwnerRecord.pubkey,
|
||||
)?.tryUnwrap();
|
||||
|
||||
const accountChangeTracker = useAccountChangeTracker();
|
||||
|
||||
|
|
Loading…
Reference in New Issue