Add RPC-based governance record hook up to the front end visual display

This commit is contained in:
Jordan Prince 2021-03-27 21:08:31 -05:00
parent c928151461
commit 12ea2ae3bd
13 changed files with 393 additions and 277 deletions

View File

@ -16,6 +16,7 @@ import { TimelockSet } from '../models/timelock';
import { AccountLayout } from '@solana/spl-token';
import { depositSourceTokensInstruction } from '../models/depositSourceTokens';
import { LABELS } from '../constants';
import { createEmptyGovernanceVotingRecordInstruction } from '../models/createEmptyGovernanceVotingRecord';
const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection;
const { notify } = utils;
@ -40,6 +41,7 @@ export const depositSourceTokens = async (
AccountLayout.span,
);
let needToCreateGovAccountToo = !existingVoteAccount;
if (!existingVoteAccount) {
existingVoteAccount = createTokenAccount(
instructions,
@ -51,6 +53,26 @@ export const depositSourceTokens = async (
);
}
const [governanceVotingRecord] = await PublicKey.findProgramAddress(
[
PROGRAM_IDS.timelock.programAccountId.toBuffer(),
proposal.pubkey.toBuffer(),
existingVoteAccount.toBuffer(),
],
PROGRAM_IDS.timelock.programId,
);
if (needToCreateGovAccountToo) {
instructions.push(
createEmptyGovernanceVotingRecordInstruction(
governanceVotingRecord,
proposal.pubkey,
existingVoteAccount,
wallet.publicKey,
),
);
}
if (!existingYesVoteAccount) {
createTokenAccount(
instructions,
@ -90,6 +112,7 @@ export const depositSourceTokens = async (
instructions.push(
depositSourceTokensInstruction(
governanceVotingRecord,
existingVoteAccount,
sourceAccount,
proposal.info.sourceHolding,

View File

@ -42,6 +42,15 @@ export const vote = async (
PROGRAM_IDS.timelock.programId,
);
const [governanceVotingRecord] = await PublicKey.findProgramAddress(
[
PROGRAM_IDS.timelock.programAccountId.toBuffer(),
proposal.pubkey.toBuffer(),
votingAccount.toBuffer(),
],
PROGRAM_IDS.timelock.programId,
);
const transferAuthority = approve(
instructions,
[],
@ -54,6 +63,7 @@ export const vote = async (
instructions.push(
voteInstruction(
governanceVotingRecord,
state.pubkey,
votingAccount,
yesVotingAccount,

View File

@ -105,12 +105,22 @@ export const withdrawVotingTokens = async (
votingTokenAmount,
);
const [governanceVotingRecord] = await PublicKey.findProgramAddress(
[
PROGRAM_IDS.timelock.programAccountId.toBuffer(),
proposal.pubkey.toBuffer(),
existingVoteAccount.toBuffer(),
],
PROGRAM_IDS.timelock.programId,
);
signers.push(transferAuthority);
signers.push(yesTransferAuthority);
signers.push(noTransferAuthority);
instructions.push(
withdrawVotingTokensInstruction(
governanceVotingRecord,
existingVoteAccount,
existingYesVoteAccount,
existingNoVoteAccount,

View File

@ -1,12 +1,10 @@
import BN from 'bn.js';
import * as d3 from 'd3';
import React, { useEffect, useState } from 'react';
import { VoteType } from '../../views/proposal';
import { VoterDisplayData, VoteType } from '../../views/proposal';
//https://observablehq.com/d/86d91b23534992ff
interface IVoterBubbleGraph {
votingAccounts: Record<string, { amount: BN }>;
yesVotingAccounts: Record<string, { amount: BN }>;
noVotingAccounts: Record<string, { amount: BN }>;
data: Array<VoterDisplayData>;
width: number;
height: number;
endpoint: string;
@ -15,52 +13,31 @@ interface IVoterBubbleGraph {
const MAX_BUBBLE_AMOUNT = 50;
export function VoterBubbleGraph(props: IVoterBubbleGraph) {
const {
votingAccounts,
yesVotingAccounts,
noVotingAccounts,
width,
height,
endpoint,
} = props;
const { data, width, height, endpoint } = props;
const subdomain = endpoint
.replace('http://', '')
.replace('https://', '')
.split('.')[0];
const mapper = (key: string, account: { amount: BN }, label: string) => ({
name: key.slice(0, 3) + '...' + key.slice(key.length - 3, key.length),
title: key,
group: label,
value: account.amount.toNumber(),
});
// For some reason giving this a type causes an issue where setRef
// cant be used with ref={} prop...not sure why. SetStateAction nonsense.
const [ref, setRef] = useState<any>();
const data = [
...Object.keys(votingAccounts).map(key =>
mapper(key, votingAccounts[key], VoteType.Undecided),
),
...Object.keys(yesVotingAccounts).map(key =>
mapper(key, yesVotingAccounts[key], VoteType.Yes),
),
...Object.keys(noVotingAccounts).map(key =>
mapper(key, noVotingAccounts[key], VoteType.No),
),
]
.sort((a, b) => b.value - a.value)
.slice(0, MAX_BUBBLE_AMOUNT);
const limitedData = data.slice(0, MAX_BUBBLE_AMOUNT).map(d => ({
...d,
name:
d.name.slice(0, 3) +
'...' +
d.name.slice(d.name.length - 3, d.name.length),
}));
console.log('Data', limitedData);
const format = d3.format(',d');
const color = d3
.scaleOrdinal()
.domain([VoteType.Undecided, VoteType.Yes, VoteType.No])
.range(['grey', 'green', 'red']);
const pack = (
data: Array<{ name: string; title: string; group: string; value: number }>,
) => {
const pack = (data: Array<VoterDisplayData>) => {
return d3
.pack()
.size([width - 2, height - 2])
@ -73,7 +50,7 @@ export function VoterBubbleGraph(props: IVoterBubbleGraph) {
useEffect(() => {
if (ref) {
ref.innerHTML = '';
const root = pack(data);
const root = pack(limitedData);
console.log('Re-rendered');
const newSvg = d3
.select(ref)
@ -150,7 +127,7 @@ export function VoterBubbleGraph(props: IVoterBubbleGraph) {
}${format(d.value)}`,
);
}
}, [ref, votingAccounts, yesVotingAccounts, noVotingAccounts, height, width]);
}, [ref, limitedData, height, width]);
return (
<div

View File

@ -2,9 +2,7 @@ import React from 'react';
import { LABELS } from '../../constants';
import { Table, Grid } from 'antd';
import BN from 'bn.js';
import { VoteType } from '../../views/proposal';
import { Breakpoint } from 'antd/lib/_util/responsiveObserve';
import { VoterDisplayData, VoteType } from '../../views/proposal';
function shortNumber(num: number) {
if (Math.abs(num) < 1000) {
@ -40,56 +38,24 @@ function shortNumber(num: number) {
const { useBreakpoint } = Grid;
interface IVoterTable {
votingAccounts: Record<string, { amount: BN }>;
yesVotingAccounts: Record<string, { amount: BN }>;
noVotingAccounts: Record<string, { amount: BN }>;
data: Array<VoterDisplayData>;
total: number;
endpoint: string;
}
const MAX_TABLE_AMOUNT = 5000;
export const VoterTable = (props: IVoterTable) => {
const {
votingAccounts,
yesVotingAccounts,
noVotingAccounts,
endpoint,
} = props;
const { data, total, endpoint } = props;
const breakpoint = useBreakpoint();
const subdomain = endpoint
.replace('http://', '')
.replace('https://', '')
.split('.')[0];
let total = 0;
const mapper = (key: string, account: { amount: BN }, label: string) => {
total += account.amount.toNumber();
return {
key: key,
type: label,
count: account.amount.toNumber(),
};
};
const data = [
...Object.keys(votingAccounts).map(key =>
mapper(key, votingAccounts[key], VoteType.Undecided),
),
...Object.keys(yesVotingAccounts).map(key =>
mapper(key, yesVotingAccounts[key], VoteType.Yes),
),
...Object.keys(noVotingAccounts).map(key =>
mapper(key, noVotingAccounts[key], VoteType.No),
),
]
.sort((a, b) => b.count - a.count)
.slice(0, MAX_TABLE_AMOUNT);
const columns = [
{
title: LABELS.ACCOUNT,
dataIndex: 'key',
key: 'key',
dataIndex: 'name',
key: 'name',
align: 'center',
render: (key: string) => (
<a
@ -117,18 +83,15 @@ export const VoterTable = (props: IVoterTable) => {
},
{
title: LABELS.COUNT,
dataIndex: 'count',
key: 'count',
dataIndex: 'value',
key: 'value',
align: 'center',
render: (
count: number,
record: { key: string; count: number; type: VoteType },
) => (
render: (count: number, record: VoterDisplayData) => (
<span
style={
record.type == VoteType.Undecided
record.group == VoteType.Undecided
? { color: 'grey' }
: { color: record.type === VoteType.Yes ? 'green' : 'red' }
: { color: record.group === VoteType.Yes ? 'green' : 'red' }
}
>
{shortNumber(count)}
@ -137,18 +100,15 @@ export const VoterTable = (props: IVoterTable) => {
},
{
title: LABELS.PERCENTAGE,
dataIndex: 'count',
key: 'count',
dataIndex: 'value',
key: 'value',
align: 'center',
render: (
count: number,
record: { key: string; count: number; type: VoteType },
) => (
render: (count: number, record: VoterDisplayData) => (
<span
style={
record.type == VoteType.Undecided
record.group == VoteType.Undecided
? { color: 'grey' }
: { color: record.type === VoteType.Yes ? 'green' : 'red' }
: { color: record.group === VoteType.Yes ? 'green' : 'red' }
}
>
{Math.round((count * 100) / total)}%

View File

@ -0,0 +1,56 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
/// 0. `[]` Governance voting record key. Needs to be set with pubkey set to PDA with seeds of the
/// program account key, proposal key, your voting account key.
/// 1. `[]` Proposal key
/// 2. `[]` Your voting account
/// 3. `[]` Payer
/// 4. `[]` Timelock program account pub key.
/// 5. `[]` Timelock program pub key. Different from program account - is the actual id of the executable.
/// 6. `[]` System account.
export const createEmptyGovernanceVotingRecordInstruction = (
governanceRecordAccount: PublicKey,
proposalAccount: PublicKey,
votingAccount: PublicKey,
payer: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.CreateGovernanceVotingRecord,
},
data,
);
const keys = [
{ pubkey: governanceRecordAccount, isSigner: false, isWritable: false },
{ pubkey: proposalAccount, isSigner: false, isWritable: false },
{ pubkey: votingAccount, isSigner: false, isWritable: false },
{ pubkey: payer, isSigner: true, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{
pubkey: PROGRAM_IDS.timelock.programId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.system, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -9,7 +9,7 @@ import { CONFIG_NAME_LENGTH, TimelockInstruction } from './timelock';
import BN from 'bn.js';
import * as Layout from '../utils/layout';
/// 0. `[writable]` Timelock config key. Needs to be set with pubkey set to PDA with seeds of the
/// 0. `[]` Timelock config key. Needs to be set with pubkey set to PDA with seeds of the
/// program account key, governance mint key, council mint key, and timelock program account key.
/// 1. `[]` Program account to tie this config to.
/// 2. `[]` Governance mint to tie this config to

View File

@ -11,16 +11,18 @@ import BN from 'bn.js';
/// These tokens are removed from your account and can be returned by withdrawing
/// them from the timelock (but then you will miss the vote.)
///
/// 0. `[writable]` Initialized Voting account to hold your received voting tokens.
/// 1. `[writable]` User token account to deposit tokens from.
/// 2. `[writable]` Source holding account for timelock that will accept the tokens in escrow.
/// 3. `[writable]` Voting mint account.
/// 4. `[]` Timelock set account.
/// 5. `[]` Transfer authority
/// 6. `[]` Timelock program mint authority
/// 7. `[]` Timelock program account pub key.
/// 8. `[]` Token program account.
/// 0. `[writable]` Governance voting record account. See Vote docs for more detail.
/// 1. `[writable]` Initialized Voting account to hold your received voting tokens.
/// 2. `[writable]` User token account to deposit tokens from.
/// 3. `[writable]` Source holding account for timelock that will accept the tokens in escrow.
/// 4. `[writable]` Voting mint account.
/// 5. `[]` Timelock set account.
/// 6. `[]` Transfer authority
/// 7. `[]` Timelock program mint authority
/// 8. `[]` Timelock program account pub key.
/// 9. `[]` Token program account.
export const depositSourceTokensInstruction = (
governanceVotingRecord: PublicKey,
votingAccount: PublicKey,
sourceAccount: PublicKey,
sourceHoldingAccount: PublicKey,
@ -48,6 +50,7 @@ export const depositSourceTokensInstruction = (
);
const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: sourceAccount, isSigner: false, isWritable: true },
{ pubkey: sourceHoldingAccount, isSigner: false, isWritable: true },

View File

@ -26,8 +26,35 @@ export enum TimelockInstruction {
DepositGovernanceTokens = 13,
WithdrawVotingTokens = 14,
CreateEmptyTimelockConfig = 15,
CreateGovernanceVotingRecord = 16,
}
export interface GovernanceVotingRecord {
/// proposal
proposal: PublicKey;
/// owner
owner: PublicKey;
///version
version: number;
/// How many votes were unspent
undecidedCount: BN;
/// How many votes were spent yes
yesCount: BN;
/// How many votes were spent no
noCount: BN;
}
export const GovernanceVotingRecordLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
Layout.publicKey('proposal'),
Layout.publicKey('owner'),
BufferLayout.u8('version'),
Layout.uint64('undecidedCount'),
Layout.uint64('yesCount'),
Layout.uint64('noCount'),
BufferLayout.seq(BufferLayout.u8(), 100, 'padding'),
],
);
export interface TimelockConfig {
///version
version: number;
@ -289,6 +316,31 @@ export const TimelockSetParser = (
return details;
};
export const GovernanceVotingRecordParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = GovernanceVotingRecordLayout.decode(buffer);
console.log('Data', data);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: {
proposal: data.proposal,
owner: data.owner,
version: data.version,
undecidedCount: data.undecidedCount,
yesCount: data.yesCount,
noCount: data.noCount,
},
};
return details;
};
export const TimelockStateParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
@ -307,6 +359,7 @@ export const TimelockStateParser = (
...info,
},
info: {
version: data.version,
timelockSet: data.timelockSet,
status: data.timelockStateStatus,
totalSigningTokensMinted: data.totalSigningTokensMinted,

View File

@ -14,22 +14,26 @@ import BN from 'bn.js';
/// Burns voting tokens, indicating you approve and/or disapprove of running this set of transactions. If you tip the consensus,
/// then the transactions can begin to be run at their time slots when people click execute.
///
/// 0. `[writable]` Timelock state account.
/// 1. `[writable]` Your Voting account.
/// 2. `[writable]` Your Yes-Voting account.
/// 3. `[writable]` Your No-Voting account.
/// 4. `[writable]` Voting mint account.
/// 5. `[writable]` Yes Voting mint account.
/// 6. `[writable]` No Voting mint account.
/// 7. `[]` Source mint account
/// 8. `[]` Timelock set account.
/// 9. `[]` Timelock config account.
/// 10. `[]` Transfer authority
/// 11. `[]` Timelock program mint authority
/// 12. `[]` Timelock program account pub key.
/// 13. `[]` Token program account.
/// 14. `[]` Clock sysvar.
/// 0. `[writable]` Governance voting record account.
/// Can be uninitialized or initialized(if already used once in this proposal)
/// Must have address with PDA having seed tuple [timelock acct key, proposal key, your voting account key]
/// 1. `[writable]` Timelock state account.
/// 2. `[writable]` Your Voting account.
/// 3. `[writable]` Your Yes-Voting account.
/// 4. `[writable]` Your No-Voting account.
/// 5. `[writable]` Voting mint account.
/// 6. `[writable]` Yes Voting mint account.
/// 7. `[writable]` No Voting mint account.
/// 8. `[]` Source mint account
/// 9. `[]` Timelock set account.
/// 10. `[]` Timelock config account.
/// 12. `[]` Transfer authority
/// 13. `[]` Timelock program mint authority
/// 14. `[]` Timelock program account pub key.
/// 15. `[]` Token program account.
/// 16. `[]` Clock sysvar.
export const voteInstruction = (
governanceVotingRecord: PublicKey,
timelockStateAccount: PublicKey,
votingAccount: PublicKey,
yesVotingAccount: PublicKey,
@ -65,6 +69,7 @@ export const voteInstruction = (
);
const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: timelockStateAccount, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, isSigner: false, isWritable: true },

View File

@ -6,25 +6,27 @@ import * as BufferLayout from 'buffer-layout';
import { TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// 0. `[writable]` Initialized Voting account from which to remove your voting tokens.
/// 1. `[writable]` Initialized Yes Voting account from which to remove your voting tokens.
/// 2. `[writable]` Initialized No Voting account from which to remove your voting tokens.
/// 3. `[writable]` User token account that you wish your actual tokens to be returned to.
/// 4. `[writable]` Source holding account owned by the timelock that will has the actual tokens in escrow.
/// 5. `[writable]` Initialized Yes Voting dump account owned by timelock set to which to send your voting tokens.
/// 6. `[writable]` Initialized No Voting dump account owned by timelock set to which to send your voting tokens.
/// 7. `[writable]` Voting mint account.
/// 8. `[writable]` Yes Voting mint account.
/// 9. `[writable]` No Voting mint account.
/// 10. `[]` Timelock state account.
/// 11. `[]` Timelock set account.
/// 12. `[]` Transfer authority
/// 13. `[]` Yes Transfer authority
/// 14. `[]` No Transfer authority
/// 15. `[]` Timelock program mint authority
/// 16. `[]` Timelock program account pub key.
/// 17. `[]` Token program account.
/// 0. `[writable]` Governance voting record account. See Vote docs for more detail.
/// 1. `[writable]` Initialized Voting account from which to remove your voting tokens.
/// 2. `[writable]` Initialized Yes Voting account from which to remove your voting tokens.
/// 3. `[writable]` Initialized No Voting account from which to remove your voting tokens.
/// 4. `[writable]` User token account that you wish your actual tokens to be returned to.
/// 5. `[writable]` Source holding account owned by the timelock that will has the actual tokens in escrow.
/// 6. `[writable]` Initialized Yes Voting dump account owned by timelock set to which to send your voting tokens.
/// 7. `[writable]` Initialized No Voting dump account owned by timelock set to which to send your voting tokens.
/// 8. `[writable]` Voting mint account.
/// 9. `[writable]` Yes Voting mint account.
/// 10. `[writable]` No Voting mint account.
/// 11. `[]` Timelock state account.
/// 12. `[]` Timelock set account.
/// 13. `[]` Transfer authority
/// 14. `[]` Yes Transfer authority
/// 15. `[]` No Transfer authority
/// 16. `[]` Timelock program mint authority
/// 17. `[]` Timelock program account pub key.
/// 18. `[]` Token program account.
export const withdrawVotingTokensInstruction = (
governanceVotingRecord: PublicKey,
votingAccount: PublicKey,
yesVotingAccount: PublicKey,
noVotingAccount: PublicKey,
@ -61,6 +63,7 @@ export const withdrawVotingTokensInstruction = (
);
const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingAccount, isSigner: false, isWritable: true },

View File

@ -1,21 +1,19 @@
import { Account, PublicKey } from '@solana/web3.js';
import { contexts, utils } from '@oyster/common';
import { AccountLayout } from '@solana/spl-token';
import { PublicKey } from '@solana/web3.js';
import * as bs58 from 'bs58';
import { ParsedAccount, utils } from '@oyster/common';
import {
GovernanceVotingRecord,
GovernanceVotingRecordLayout,
GovernanceVotingRecordParser,
} from '../models/timelock';
const { deserializeAccount } = contexts.Accounts;
export async function getVoteAccountHolders(
mint?: PublicKey,
const MAX_LOOKUPS = 5000;
export async function getGovernanceVotingRecords(
proposal?: PublicKey,
endpoint?: string,
): Promise<Record<string, Account>> {
): Promise<Record<string, ParsedAccount<GovernanceVotingRecord>>> {
const PROGRAM_IDS = utils.programIds();
if (!mint || !endpoint) return {};
const [authority] = await PublicKey.findProgramAddress(
[PROGRAM_IDS.timelock.programAccountId.toBuffer()],
PROGRAM_IDS.timelock.programId,
);
if (!proposal || !endpoint) return {};
let accountRes = await fetch(endpoint, {
method: 'POST',
@ -27,16 +25,16 @@ export async function getVoteAccountHolders(
id: 1,
method: 'getProgramAccounts',
params: [
PROGRAM_IDS.token.toBase58(),
PROGRAM_IDS.timelock.programId.toBase58(),
{
commitment: 'single',
filters: [
{ dataSize: AccountLayout.span },
{ dataSize: GovernanceVotingRecordLayout.span },
{
memcmp: {
// Mint is first thing in the account data
// Proposal key is first thing in the account data
offset: 0,
bytes: mint.toString(),
bytes: proposal.toString(),
},
},
],
@ -46,12 +44,16 @@ export async function getVoteAccountHolders(
});
let raw = (await accountRes.json())['result'];
if (!raw) return {};
let accounts: Record<string, Account> = {};
const authorityBase58 = authority.toBase58();
let accounts: Record<string, ParsedAccount<GovernanceVotingRecord>> = {};
let i = 0;
for (let acc of raw) {
const account = deserializeAccount(bs58.decode(acc.account.data));
if (account.owner.toBase58() !== authorityBase58)
accounts[account.owner.toBase58()] = account;
const account = GovernanceVotingRecordParser(acc.pubkey, {
...acc.account,
data: bs58.decode(acc.account.data),
}) as ParsedAccount<GovernanceVotingRecord>;
if (i > MAX_LOOKUPS) break;
accounts[account.info.owner.toBase58()] = account;
i++;
}
return accounts;

View File

@ -4,6 +4,7 @@ import { LABELS } from '../../constants';
import { ParsedAccount, TokenIcon } from '@oyster/common';
import {
ConsensusAlgorithm,
GovernanceVotingRecord,
INSTRUCTION_LIMIT,
TimelockConfig,
TimelockSet,
@ -26,7 +27,7 @@ import { Vote } from '../../components/Proposal/Vote';
import { RegisterToVote } from '../../components/Proposal/RegisterToVote';
import { WithdrawTokens } from '../../components/Proposal/WithdrawTokens';
import './style.less';
import { getVoteAccountHolders } from '../../utils/lookups';
import { getGovernanceVotingRecords } from '../../utils/lookups';
import BN from 'bn.js';
import { VoterBubbleGraph } from '../../components/Proposal/VoterBubbleGraph';
import { VoterTable } from '../../components/Proposal/VoterTable';
@ -56,23 +57,22 @@ export const ProposalView = () => {
const sourceMint = useMint(proposal?.info.sourceMint);
const yesVotingMint = useMint(proposal?.info.yesVotingMint);
const [votingAccounts, setVotingAccounts] = useState<any>({});
const [yesVotingAccounts, setYesVotingAccounts] = useState<any>({});
const [noVotingAccounts, setNoVotingAccounts] = useState<any>({});
const noVotingMint = useMint(proposal?.info.noVotingMint);
const [votingDisplayData, setVotingDisplayData] = useState<any>({});
useEffect(() => {
getVoteAccountHolders(proposal?.info.votingMint, endpoint).then(
setVotingAccounts,
);
getVoteAccountHolders(proposal?.info.yesVotingMint, endpoint).then(
setYesVotingAccounts,
);
getVoteAccountHolders(proposal?.info.noVotingMint, endpoint).then(
setNoVotingAccounts,
getGovernanceVotingRecords(proposal?.pubkey, endpoint).then(records =>
setVotingDisplayData(voterDisplayData(records)),
);
}, [proposal]);
return (
<div className="flexColumn">
{proposal && sigMint && votingMint && sourceMint && yesVotingMint ? (
{proposal &&
sigMint &&
votingMint &&
sourceMint &&
yesVotingMint &&
noVotingMint ? (
<InnerProposalView
proposal={proposal}
timelockState={timelockState}
@ -80,9 +80,8 @@ export const ProposalView = () => {
sourceMint={sourceMint}
votingMint={votingMint}
yesVotingMint={yesVotingMint}
votingAccounts={votingAccounts}
yesVotingAccounts={yesVotingAccounts}
noVotingAccounts={noVotingAccounts}
noVotingMint={noVotingMint}
votingDisplayData={votingDisplayData}
sigMint={sigMint}
instructions={context.transactions}
endpoint={endpoint}
@ -144,6 +143,70 @@ function useLoadGist({
}
}, [loading]);
}
interface PartialGovernanceRecord {
info: { yesCount: BN; noCount: BN; undecidedCount: BN };
}
export interface VoterDisplayData {
name: string;
title: string;
group: string;
value: number;
}
function voterDisplayData(
governanceVotingRecords: Record<string, PartialGovernanceRecord>,
): Array<VoterDisplayData> {
const mapper = (key: string, amount: number, label: string) => ({
name: key,
title: key,
group: label,
value: amount,
});
const undecidedData = [
...Object.keys(governanceVotingRecords)
.filter(
key => governanceVotingRecords[key].info.undecidedCount.toNumber() > 0,
)
.map(key =>
mapper(
key,
governanceVotingRecords[key].info.undecidedCount.toNumber(),
VoteType.Undecided,
),
),
];
const noData = [
...Object.keys(governanceVotingRecords)
.filter(key => governanceVotingRecords[key].info.noCount.toNumber() > 0)
.map(key =>
mapper(
key,
governanceVotingRecords[key].info.noCount.toNumber(),
VoteType.No,
),
),
];
const yesData = [
...Object.keys(governanceVotingRecords)
.filter(key => governanceVotingRecords[key].info.yesCount.toNumber() > 0)
.map(key =>
mapper(
key,
governanceVotingRecords[key].info.yesCount.toNumber(),
VoteType.Yes,
),
),
];
const data = [...undecidedData, ...yesData, ...noData].sort(
(a, b) => b.value - a.value,
);
return data;
}
function InnerProposalView({
proposal,
@ -151,12 +214,11 @@ function InnerProposalView({
sigMint,
votingMint,
yesVotingMint,
noVotingMint,
instructions,
timelockConfig,
sourceMint,
votingAccounts,
yesVotingAccounts,
noVotingAccounts,
votingDisplayData,
endpoint,
}: {
proposal: ParsedAccount<TimelockSet>;
@ -165,11 +227,10 @@ function InnerProposalView({
sigMint: MintInfo;
votingMint: MintInfo;
yesVotingMint: MintInfo;
noVotingMint: MintInfo;
sourceMint: MintInfo;
instructions: Record<string, ParsedAccount<TimelockTransaction>>;
votingAccounts: Record<string, { amount: BN }>;
yesVotingAccounts: Record<string, { amount: BN }>;
noVotingAccounts: Record<string, { amount: BN }>;
votingDisplayData: Array<VoterDisplayData>;
endpoint: string;
}) {
const sigAccount = useAccountByMint(proposal.info.signatoryMint);
@ -254,103 +315,56 @@ function InnerProposalView({
</Col>
</Row>
<Row
gutter={[
{ xs: 8, sm: 16, md: 24, lg: 32 },
{ xs: 8, sm: 16, md: 24, lg: 32 },
]}
className="proposals-visual"
>
<Col md={12} sm={24} xs={24}>
<Card
style={{ height: '100%' }}
title={LABELS.LARGEST_VOTERS_BUBBLE}
>
{width && height && (
<VoterBubbleGraph
endpoint={endpoint}
width={width}
height={height}
noVotingAccounts={{
zU3YkmiaCgYHVebfdNq1U09DiNVHf1kxWuY5InWHv: {
amount: new BN(1),
},
bGarqsCCUzDBzHjjeehZIMknIMj5zJ6O9R5tK: {
amount: new BN(5),
},
UTj29mKAAAAAAan1RcYx3TJKFZjmGkdXraLXrijm0ttX: {
amount: new BN(3),
},
rGArIdkGp9UXGSxcUSGMyUw9SvF: { amount: new BN(8) },
}}
yesVotingAccounts={{
'9qR84VknBPtVyRw9XwCYRP6B1GiBtZohNo6TqETzw9Jv': {
amount: new BN(50),
},
CpJTMoYFzhVn94TypvR3oNZukeo82C64urf2Pdcwewv2: {
amount: new BN(20),
},
}}
votingAccounts={{
ArfPb6WNcGc9kUar2qiukS57g5M2x5o8kfa65SQvrCMn: {
amount: new BN(100),
},
ArfPb6WNc9kUar2qiukS57g5M2x5o8kfa65SQvrCMn: {
amount: new BN(302),
},
}}
/>
)}
</Card>
</Col>
<Col md={12} sm={24} xs={24}>
<Card
style={{ height: '100%' }}
title={LABELS.LARGEST_VOTERS_TABLE}
>
<div
ref={r => {
if (r) {
setHeight(r.clientHeight);
setWidth(r.clientWidth);
}
}}
{votingDisplayData.length > 0 && (
<Row
gutter={[
{ xs: 8, sm: 16, md: 24, lg: 32 },
{ xs: 8, sm: 16, md: 24, lg: 32 },
]}
className="proposals-visual"
>
<Col md={12} sm={24} xs={24}>
<Card
style={{ height: '100%' }}
title={LABELS.LARGEST_VOTERS_BUBBLE}
>
<VoterTable
endpoint={endpoint}
noVotingAccounts={{
zU3YkmiaCgYHVebfdNq1U09DiNVHf1kxWuY5InWHv: {
amount: new BN(1),
},
bGarqsCCUzDBzHjjeehZIMknIMj5zJ6O9R5tK: {
amount: new BN(5),
},
UTj29mKAAAAAAan1RcYx3TJKFZjmGkdXraLXrijm0ttX: {
amount: new BN(3),
},
rGArIdkGp9UXGSxcUSGMyUw9SvF: { amount: new BN(8) },
{width && height && (
<VoterBubbleGraph
endpoint={endpoint}
width={width}
height={height}
data={votingDisplayData}
/>
)}
</Card>
</Col>
<Col md={12} sm={24} xs={24}>
<Card
style={{ height: '100%' }}
title={LABELS.LARGEST_VOTERS_TABLE}
>
<div
ref={r => {
if (r) {
setHeight(r.clientHeight);
setWidth(r.clientWidth);
}
}}
yesVotingAccounts={{
'9qR84VknBPtVyRw9XwCYRP6B1GiBtZohNo6TqETzw9Jv': {
amount: new BN(50),
},
CpJTMoYFzhVn94TypvR3oNZukeo82C64urf2Pdcwewv2: {
amount: new BN(20),
},
}}
votingAccounts={{
ArfPb6WNcGc9kUar2qiukS57g5M2x5o8kfa65SQvrCMn: {
amount: new BN(100),
},
ArfPb6WNc9kUar2qiukS57g5M2x5o8kfa65SQvrCMn: {
amount: new BN(302),
},
}}
/>
</div>
</Card>
</Col>
</Row>
>
<VoterTable
endpoint={endpoint}
total={
votingMint.supply.toNumber() +
yesVotingMint.supply.toNumber() +
noVotingMint.supply.toNumber()
}
data={votingDisplayData}
/>
</div>
</Card>
</Col>
</Row>
)}
<Row className="proposals-stats">
<Col md={7} xs={24}>