mirror of https://github.com/certusone/oyster.git
Add RPC-based governance record hook up to the front end visual display
This commit is contained in:
parent
c928151461
commit
12ea2ae3bd
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}%
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}>
|
||||
|
|
Loading…
Reference in New Issue