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 { AccountLayout } from '@solana/spl-token';
import { depositSourceTokensInstruction } from '../models/depositSourceTokens'; import { depositSourceTokensInstruction } from '../models/depositSourceTokens';
import { LABELS } from '../constants'; import { LABELS } from '../constants';
import { createEmptyGovernanceVotingRecordInstruction } from '../models/createEmptyGovernanceVotingRecord';
const { createTokenAccount } = actions; const { createTokenAccount } = actions;
const { sendTransaction } = contexts.Connection; const { sendTransaction } = contexts.Connection;
const { notify } = utils; const { notify } = utils;
@ -40,6 +41,7 @@ export const depositSourceTokens = async (
AccountLayout.span, AccountLayout.span,
); );
let needToCreateGovAccountToo = !existingVoteAccount;
if (!existingVoteAccount) { if (!existingVoteAccount) {
existingVoteAccount = createTokenAccount( existingVoteAccount = createTokenAccount(
instructions, 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) { if (!existingYesVoteAccount) {
createTokenAccount( createTokenAccount(
instructions, instructions,
@ -90,6 +112,7 @@ export const depositSourceTokens = async (
instructions.push( instructions.push(
depositSourceTokensInstruction( depositSourceTokensInstruction(
governanceVotingRecord,
existingVoteAccount, existingVoteAccount,
sourceAccount, sourceAccount,
proposal.info.sourceHolding, proposal.info.sourceHolding,

View File

@ -42,6 +42,15 @@ export const vote = async (
PROGRAM_IDS.timelock.programId, 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( const transferAuthority = approve(
instructions, instructions,
[], [],
@ -54,6 +63,7 @@ export const vote = async (
instructions.push( instructions.push(
voteInstruction( voteInstruction(
governanceVotingRecord,
state.pubkey, state.pubkey,
votingAccount, votingAccount,
yesVotingAccount, yesVotingAccount,

View File

@ -105,12 +105,22 @@ export const withdrawVotingTokens = async (
votingTokenAmount, 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(transferAuthority);
signers.push(yesTransferAuthority); signers.push(yesTransferAuthority);
signers.push(noTransferAuthority); signers.push(noTransferAuthority);
instructions.push( instructions.push(
withdrawVotingTokensInstruction( withdrawVotingTokensInstruction(
governanceVotingRecord,
existingVoteAccount, existingVoteAccount,
existingYesVoteAccount, existingYesVoteAccount,
existingNoVoteAccount, existingNoVoteAccount,

View File

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

View File

@ -2,9 +2,7 @@ import React from 'react';
import { LABELS } from '../../constants'; import { LABELS } from '../../constants';
import { Table, Grid } from 'antd'; import { Table, Grid } from 'antd';
import BN from 'bn.js'; import { VoterDisplayData, VoteType } from '../../views/proposal';
import { VoteType } from '../../views/proposal';
import { Breakpoint } from 'antd/lib/_util/responsiveObserve';
function shortNumber(num: number) { function shortNumber(num: number) {
if (Math.abs(num) < 1000) { if (Math.abs(num) < 1000) {
@ -40,56 +38,24 @@ function shortNumber(num: number) {
const { useBreakpoint } = Grid; const { useBreakpoint } = Grid;
interface IVoterTable { interface IVoterTable {
votingAccounts: Record<string, { amount: BN }>; data: Array<VoterDisplayData>;
yesVotingAccounts: Record<string, { amount: BN }>; total: number;
noVotingAccounts: Record<string, { amount: BN }>;
endpoint: string; endpoint: string;
} }
const MAX_TABLE_AMOUNT = 5000;
export const VoterTable = (props: IVoterTable) => { export const VoterTable = (props: IVoterTable) => {
const { const { data, total, endpoint } = props;
votingAccounts,
yesVotingAccounts,
noVotingAccounts,
endpoint,
} = props;
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const subdomain = endpoint const subdomain = endpoint
.replace('http://', '') .replace('http://', '')
.replace('https://', '') .replace('https://', '')
.split('.')[0]; .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 = [ const columns = [
{ {
title: LABELS.ACCOUNT, title: LABELS.ACCOUNT,
dataIndex: 'key', dataIndex: 'name',
key: 'key', key: 'name',
align: 'center', align: 'center',
render: (key: string) => ( render: (key: string) => (
<a <a
@ -117,18 +83,15 @@ export const VoterTable = (props: IVoterTable) => {
}, },
{ {
title: LABELS.COUNT, title: LABELS.COUNT,
dataIndex: 'count', dataIndex: 'value',
key: 'count', key: 'value',
align: 'center', align: 'center',
render: ( render: (count: number, record: VoterDisplayData) => (
count: number,
record: { key: string; count: number; type: VoteType },
) => (
<span <span
style={ style={
record.type == VoteType.Undecided record.group == VoteType.Undecided
? { color: 'grey' } ? { color: 'grey' }
: { color: record.type === VoteType.Yes ? 'green' : 'red' } : { color: record.group === VoteType.Yes ? 'green' : 'red' }
} }
> >
{shortNumber(count)} {shortNumber(count)}
@ -137,18 +100,15 @@ export const VoterTable = (props: IVoterTable) => {
}, },
{ {
title: LABELS.PERCENTAGE, title: LABELS.PERCENTAGE,
dataIndex: 'count', dataIndex: 'value',
key: 'count', key: 'value',
align: 'center', align: 'center',
render: ( render: (count: number, record: VoterDisplayData) => (
count: number,
record: { key: string; count: number; type: VoteType },
) => (
<span <span
style={ style={
record.type == VoteType.Undecided record.group == VoteType.Undecided
? { color: 'grey' } ? { color: 'grey' }
: { color: record.type === VoteType.Yes ? 'green' : 'red' } : { color: record.group === VoteType.Yes ? 'green' : 'red' }
} }
> >
{Math.round((count * 100) / total)}% {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 BN from 'bn.js';
import * as Layout from '../utils/layout'; 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. /// program account key, governance mint key, council mint key, and timelock program account key.
/// 1. `[]` Program account to tie this config to. /// 1. `[]` Program account to tie this config to.
/// 2. `[]` Governance mint 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 /// These tokens are removed from your account and can be returned by withdrawing
/// them from the timelock (but then you will miss the vote.) /// them from the timelock (but then you will miss the vote.)
/// ///
/// 0. `[writable]` Initialized Voting account to hold your received voting tokens. /// 0. `[writable]` Governance voting record account. See Vote docs for more detail.
/// 1. `[writable]` User token account to deposit tokens from. /// 1. `[writable]` Initialized Voting account to hold your received voting tokens.
/// 2. `[writable]` Source holding account for timelock that will accept the tokens in escrow. /// 2. `[writable]` User token account to deposit tokens from.
/// 3. `[writable]` Voting mint account. /// 3. `[writable]` Source holding account for timelock that will accept the tokens in escrow.
/// 4. `[]` Timelock set account. /// 4. `[writable]` Voting mint account.
/// 5. `[]` Transfer authority /// 5. `[]` Timelock set account.
/// 6. `[]` Timelock program mint authority /// 6. `[]` Transfer authority
/// 7. `[]` Timelock program account pub key. /// 7. `[]` Timelock program mint authority
/// 8. `[]` Token program account. /// 8. `[]` Timelock program account pub key.
/// 9. `[]` Token program account.
export const depositSourceTokensInstruction = ( export const depositSourceTokensInstruction = (
governanceVotingRecord: PublicKey,
votingAccount: PublicKey, votingAccount: PublicKey,
sourceAccount: PublicKey, sourceAccount: PublicKey,
sourceHoldingAccount: PublicKey, sourceHoldingAccount: PublicKey,
@ -48,6 +50,7 @@ export const depositSourceTokensInstruction = (
); );
const keys = [ const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true }, { pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: sourceAccount, isSigner: false, isWritable: true }, { pubkey: sourceAccount, isSigner: false, isWritable: true },
{ pubkey: sourceHoldingAccount, isSigner: false, isWritable: true }, { pubkey: sourceHoldingAccount, isSigner: false, isWritable: true },

View File

@ -26,8 +26,35 @@ export enum TimelockInstruction {
DepositGovernanceTokens = 13, DepositGovernanceTokens = 13,
WithdrawVotingTokens = 14, WithdrawVotingTokens = 14,
CreateEmptyTimelockConfig = 15, 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 { export interface TimelockConfig {
///version ///version
version: number; version: number;
@ -289,6 +316,31 @@ export const TimelockSetParser = (
return details; 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 = ( export const TimelockStateParser = (
pubKey: PublicKey, pubKey: PublicKey,
info: AccountInfo<Buffer>, info: AccountInfo<Buffer>,
@ -307,6 +359,7 @@ export const TimelockStateParser = (
...info, ...info,
}, },
info: { info: {
version: data.version,
timelockSet: data.timelockSet, timelockSet: data.timelockSet,
status: data.timelockStateStatus, status: data.timelockStateStatus,
totalSigningTokensMinted: data.totalSigningTokensMinted, 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, /// 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. /// then the transactions can begin to be run at their time slots when people click execute.
/// ///
/// 0. `[writable]` Timelock state account. /// 0. `[writable]` Governance voting record account.
/// 1. `[writable]` Your Voting account. /// Can be uninitialized or initialized(if already used once in this proposal)
/// 2. `[writable]` Your Yes-Voting account. /// Must have address with PDA having seed tuple [timelock acct key, proposal key, your voting account key]
/// 3. `[writable]` Your No-Voting account. /// 1. `[writable]` Timelock state account.
/// 4. `[writable]` Voting mint account. /// 2. `[writable]` Your Voting account.
/// 5. `[writable]` Yes Voting mint account. /// 3. `[writable]` Your Yes-Voting account.
/// 6. `[writable]` No Voting mint account. /// 4. `[writable]` Your No-Voting account.
/// 7. `[]` Source mint account /// 5. `[writable]` Voting mint account.
/// 8. `[]` Timelock set account. /// 6. `[writable]` Yes Voting mint account.
/// 9. `[]` Timelock config account. /// 7. `[writable]` No Voting mint account.
/// 10. `[]` Transfer authority /// 8. `[]` Source mint account
/// 11. `[]` Timelock program mint authority /// 9. `[]` Timelock set account.
/// 12. `[]` Timelock program account pub key. /// 10. `[]` Timelock config account.
/// 13. `[]` Token program account. /// 12. `[]` Transfer authority
/// 14. `[]` Clock sysvar. /// 13. `[]` Timelock program mint authority
/// 14. `[]` Timelock program account pub key.
/// 15. `[]` Token program account.
/// 16. `[]` Clock sysvar.
export const voteInstruction = ( export const voteInstruction = (
governanceVotingRecord: PublicKey,
timelockStateAccount: PublicKey, timelockStateAccount: PublicKey,
votingAccount: PublicKey, votingAccount: PublicKey,
yesVotingAccount: PublicKey, yesVotingAccount: PublicKey,
@ -65,6 +69,7 @@ export const voteInstruction = (
); );
const keys = [ const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: timelockStateAccount, isSigner: false, isWritable: true }, { pubkey: timelockStateAccount, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true }, { pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, 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 { TimelockInstruction } from './timelock';
import BN from 'bn.js'; import BN from 'bn.js';
/// 0. `[writable]` Initialized Voting account from which to remove your voting tokens. /// 0. `[writable]` Governance voting record account. See Vote docs for more detail.
/// 1. `[writable]` Initialized Yes Voting account from which to remove your voting tokens. /// 1. `[writable]` Initialized Voting account from which to remove your voting tokens.
/// 2. `[writable]` Initialized No Voting account from which to remove your voting tokens. /// 2. `[writable]` Initialized Yes Voting account from which to remove your voting tokens.
/// 3. `[writable]` User token account that you wish your actual tokens to be returned to. /// 3. `[writable]` Initialized No Voting account from which to remove your voting tokens.
/// 4. `[writable]` Source holding account owned by the timelock that will has the actual tokens in escrow. /// 4. `[writable]` User token account that you wish your actual tokens to be returned to.
/// 5. `[writable]` Initialized Yes Voting dump account owned by timelock set to which to send your voting tokens. /// 5. `[writable]` Source holding account owned by the timelock that will has the actual tokens in escrow.
/// 6. `[writable]` Initialized No Voting dump account owned by timelock set to which to send your voting tokens. /// 6. `[writable]` Initialized Yes Voting dump account owned by timelock set to which to send your voting tokens.
/// 7. `[writable]` Voting mint account. /// 7. `[writable]` Initialized No Voting dump account owned by timelock set to which to send your voting tokens.
/// 8. `[writable]` Yes Voting mint account. /// 8. `[writable]` Voting mint account.
/// 9. `[writable]` No Voting mint account. /// 9. `[writable]` Yes Voting mint account.
/// 10. `[]` Timelock state account. /// 10. `[writable]` No Voting mint account.
/// 11. `[]` Timelock set account. /// 11. `[]` Timelock state account.
/// 12. `[]` Transfer authority /// 12. `[]` Timelock set account.
/// 13. `[]` Yes Transfer authority /// 13. `[]` Transfer authority
/// 14. `[]` No Transfer authority /// 14. `[]` Yes Transfer authority
/// 15. `[]` Timelock program mint authority /// 15. `[]` No Transfer authority
/// 16. `[]` Timelock program account pub key. /// 16. `[]` Timelock program mint authority
/// 17. `[]` Token program account. /// 17. `[]` Timelock program account pub key.
/// 18. `[]` Token program account.
export const withdrawVotingTokensInstruction = ( export const withdrawVotingTokensInstruction = (
governanceVotingRecord: PublicKey,
votingAccount: PublicKey, votingAccount: PublicKey,
yesVotingAccount: PublicKey, yesVotingAccount: PublicKey,
noVotingAccount: PublicKey, noVotingAccount: PublicKey,
@ -61,6 +63,7 @@ export const withdrawVotingTokensInstruction = (
); );
const keys = [ const keys = [
{ pubkey: governanceVotingRecord, isSigner: false, isWritable: true },
{ pubkey: votingAccount, isSigner: false, isWritable: true }, { pubkey: votingAccount, isSigner: false, isWritable: true },
{ pubkey: yesVotingAccount, isSigner: false, isWritable: true }, { pubkey: yesVotingAccount, isSigner: false, isWritable: true },
{ pubkey: noVotingAccount, isSigner: false, isWritable: true }, { pubkey: noVotingAccount, isSigner: false, isWritable: true },

View File

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

View File

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