fix: Do not show Yes/No buttons when voter record can't be loaded

This commit is contained in:
Sebastian.Bor 2021-08-05 23:11:51 +01:00
parent e05ecbc9a2
commit 94ff7880ff
8 changed files with 147 additions and 37 deletions

View File

@ -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`,
);
}

View File

@ -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],
);
};

View File

@ -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;

View File

@ -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 = [

View File

@ -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';
}
}

View File

@ -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() || '';

View File

@ -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;

View File

@ -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();