Merge branch 'main' into lending/xcl

This commit is contained in:
jordansexton 2021-07-01 21:00:09 -05:00
commit 10a6a5d5cb
33 changed files with 838 additions and 320 deletions

View File

@ -63,7 +63,7 @@ export const PROGRAM_IDS = [
{
name: 'mainnet-beta',
governance: () => ({
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
@ -85,7 +85,7 @@ export const PROGRAM_IDS = [
{
name: 'testnet',
governance: () => ({
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
}),
wormhole: () => ({
pubkey: new PublicKey('5gQf5AUhAgWYgUCt9ouShm9H7dzzXUsLdssYwe5krKhg'),
@ -104,7 +104,7 @@ export const PROGRAM_IDS = [
{
name: 'devnet',
governance: () => ({
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),
@ -123,7 +123,7 @@ export const PROGRAM_IDS = [
{
name: 'localnet',
governance: () => ({
programId: new PublicKey('GovergMfhoNZePj4v86rLXZSN4DeFSLmvKEgWCch1Zuu'),
programId: new PublicKey('GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

View File

@ -15,9 +15,6 @@ import {
} from '@oyster/common';
import { AccountLayout, MintLayout, Token, u64 } from '@solana/spl-token';
import { setAuthority } from '@project-serum/serum/lib/token-instructions';
import { GOVERNANCE_PROGRAM_SEED } from '../../models/accounts';
import { serializeInstructionToBase64 } from '../../models/serialisation';
const { notify } = utils;
export interface SourceEntryInterface {
@ -29,27 +26,25 @@ export const generateGovernanceArtifacts = async (
connection: Connection,
wallet: any,
) => {
const PROGRAM_IDS = utils.programIds();
let communityMintSigners: Account[] = [];
let communityMintInstruction: TransactionInstruction[] = [];
// Setup community mint
const [communityMintAddress, otherOwner] = await withMint(
const { mintAddress: communityMintAddress } = await withMint(
communityMintInstruction,
communityMintSigners,
connection,
wallet,
9,
new u64('4205522598596271000'),
new u64('6007889426566101064'),
0,
new u64('7000'),
new u64('10000'),
);
let councilMinSigners: Account[] = [];
let councilMintInstructions: TransactionInstruction[] = [];
// Setup council mint
const [councilMintAddress] = await withMint(
const { mintAddress: councilMintAddress } = await withMint(
councilMintInstructions,
councilMinSigners,
connection,
@ -63,52 +58,17 @@ export const generateGovernanceArtifacts = async (
let governanceSigners: Account[] = [];
let governanceInstructions: TransactionInstruction[] = [];
// Token governance artifacts
const tokenGovernance = await withTokenGovernance(
governanceInstructions,
governanceSigners,
connection,
wallet,
0,
new u64(200),
);
let realmName = `Realm-${communityMintAddress.toBase58().substring(0, 5)}`;
let governedAccount = communityMintAddress;
const [realmAddress] = await PublicKey.findProgramAddress(
[Buffer.from(GOVERNANCE_PROGRAM_SEED), Buffer.from(realmName)],
PROGRAM_IDS.governance.programId,
);
const [governanceAddress] = await PublicKey.findProgramAddress(
[
Buffer.from('account-governance'),
realmAddress.toBuffer(),
governedAccount.toBuffer(),
],
PROGRAM_IDS.governance.programId,
);
// Use setAuthority from Serum because I couldn't get Token.createSetAuthorityInstruction to work
// It looks like version mismatch because the function in the SDK takes authorityType params
// This step will be uneccesery once we have CreateMintAuthority instruction
let ix = setAuthority({
target: communityMintAddress,
currentAuthority: wallet.publicKey,
newAuthority: governanceAddress,
authorityType: 'MintTokens',
});
governanceInstructions.push(ix);
const mintToInstruction: TransactionInstruction = Token.createMintToInstruction(
PROGRAM_IDS.token,
communityMintAddress,
otherOwner,
governanceAddress,
[],
1,
);
const instructionBase64 = serializeInstructionToBase64(mintToInstruction);
// const upgrade = await createUpgradeInstruction(
// new PublicKey('Hita5Lun87S4MADAF4vGoWEgFm5DyuVqxoWzzqYxS3AD'),
// new PublicKey('EUn3VY7uiAVvi3X72Pfe8DcXbeLHMu5mVbavdQDKTViK'),
// new PublicKey('FqSReK9R8QxvFZgdrAwGT3gsYp1ZGfiFjS8xrzyyadn3'),
// );
// const instructionBase64 = serializeInstructionToBase64(upgrade);
notify({
message: 'Creating Governance artifacts...',
@ -139,7 +99,7 @@ export const generateGovernanceArtifacts = async (
realmName,
communityMintAddress,
councilMintAddress,
instructionBase64,
tokenGovernance,
};
} catch (ex) {
console.error(ex);
@ -147,6 +107,69 @@ export const generateGovernanceArtifacts = async (
}
};
const withTokenGovernance = async (
instructions: TransactionInstruction[],
signers: Account[],
connection: Connection,
wallet: any,
decimals: number,
amount: u64,
) => {
const PROGRAM_IDS = utils.programIds();
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
MintLayout.span,
);
const tokenAccountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
const mintAddress = createMint(
instructions,
wallet.publicKey,
mintRentExempt,
decimals,
wallet.publicKey,
wallet.publicKey,
signers,
);
const tokenAccountAddress = createTokenAccount(
instructions,
wallet.publicKey,
tokenAccountRentExempt,
mintAddress,
wallet.publicKey,
signers,
);
instructions.push(
Token.createMintToInstruction(
PROGRAM_IDS.token,
mintAddress,
tokenAccountAddress,
wallet.publicKey,
[],
new u64(amount),
),
);
const beneficiaryTokenAccountAddress = createTokenAccount(
instructions,
wallet.publicKey,
tokenAccountRentExempt,
mintAddress,
wallet.publicKey,
signers,
);
return {
tokenAccountAddress: tokenAccountAddress.toBase58(),
beneficiaryTokenAccountAddress: beneficiaryTokenAccountAddress.toBase58(),
};
};
const withMint = async (
instructions: TransactionInstruction[],
signers: Account[],
@ -189,18 +212,16 @@ const withMint = async (
signers,
);
if (amount) {
instructions.push(
Token.createMintToInstruction(
PROGRAM_IDS.token,
mintAddress,
tokenAccountAddress,
wallet.publicKey,
[],
new u64(amount),
),
);
}
instructions.push(
Token.createMintToInstruction(
PROGRAM_IDS.token,
mintAddress,
tokenAccountAddress,
wallet.publicKey,
[],
new u64(amount),
),
);
const otherOwner = new Account();
instructions.push(
@ -240,5 +261,5 @@ const withMint = async (
),
);
return [mintAddress, otherOwnerTokenAccount];
return { mintAddress, otherOwnerTokenAccount };
};

View File

@ -6,6 +6,7 @@ import { GovernanceConfig } from '../models/accounts';
import { withCreateProgramGovernance } from '../models/withCreateProgramGovernance';
import { sendTransactionWithNotifications } from '../tools/transactions';
import { withCreateMintGovernance } from '../models/withCreateMintGovernance';
import { withCreateTokenGovernance } from '../models/withCreateTokenGovernance';
export const registerGovernance = async (
connection: Connection,
@ -57,6 +58,19 @@ export const registerGovernance = async (
).governanceAddress;
break;
}
case GovernanceType.Token: {
governanceAddress = (
await withCreateTokenGovernance(
instructions,
realm,
config,
transferAuthority!,
wallet.publicKey,
wallet.publicKey,
)
).governanceAddress;
break;
}
default: {
throw new Error(
`Governance type ${governanceType} is not supported yet.`,

View File

@ -0,0 +1,77 @@
import { ParsedAccount, TokenIcon } from '@oyster/common';
import { Avatar, Badge } from 'antd';
import React from 'react';
import { Governance, ProposalState } from '../../models/accounts';
import { useProposalsByGovernance } from '../../hooks/apiHooks';
import './style.less';
export function GovernanceBadge({
governance,
size = 40,
showVotingCount = true,
}: {
governance: ParsedAccount<Governance>;
size?: number;
showVotingCount?: boolean;
}) {
const proposals = useProposalsByGovernance(governance?.pubkey);
const color = governance.info.isProgramGovernance() ? 'green' : 'gray';
const useAvatar =
governance.info.isProgramGovernance() ||
governance.info.isAccountGovernance();
return (
<Badge
count={
showVotingCount
? proposals.filter(p => p.info.state === ProposalState.Voting).length
: 0
}
>
<div style={{ width: size * 1.3, height: size }}>
{governance.info.isMintGovernance() && (
<TokenIcon
mintAddress={governance.info.config.governedAccount}
size={size}
/>
)}
{governance.info.isTokenGovernance() && (
<div
style={{ position: 'relative' }}
className="token-icon-container"
>
<TokenIcon
style={{ position: 'absolute', left: size * 0.5 }}
mintAddress={governance.info.config.governedAccount}
size={size * 0.6}
/>
<TokenIcon
mintAddress={governance.info.config.governedAccount}
size={size * 0.6}
/>
<TokenIcon
style={{
position: 'absolute',
left: size * 0.25,
top: size * 0.3,
}}
mintAddress={governance.info.config.governedAccount}
size={size * 0.6}
/>
</div>
)}
{useAvatar && (
<Avatar
size={size}
gap={2}
style={{ background: color, marginRight: 5 }}
>
{governance.info.config.governedAccount.toBase58().slice(0, 5)}
</Avatar>
)}
</div>
</Badge>
);
}

View File

@ -0,0 +1,6 @@
@import '~antd/dist/antd.dark.less';
// add transparent shadow to token icons
.token-icon-container > div > div {
box-shadow: 0px 0px 0px 2px rgba(0, 0, 0, 0.3);
}

View File

@ -0,0 +1,47 @@
import React from 'react';
import { contexts, ParsedAccount } from '@oyster/common';
import { TokenOwnerRecord } from '../../models/accounts';
import { formatTokenAmount } from '../../tools/text';
const { useMint } = contexts.Accounts;
export function RealmDepositBadge({
councilTokenOwnerRecord,
communityTokenOwnerRecord,
}: {
councilTokenOwnerRecord: ParsedAccount<TokenOwnerRecord> | undefined;
communityTokenOwnerRecord: ParsedAccount<TokenOwnerRecord> | undefined;
}) {
const communityMint = useMint(
communityTokenOwnerRecord?.info.governingTokenMint,
);
const councilMint = useMint(councilTokenOwnerRecord?.info.governingTokenMint);
if (!councilTokenOwnerRecord && !communityTokenOwnerRecord) {
return null;
}
return (
<>
<span>deposited </span>
{communityTokenOwnerRecord && (
<span>
{`tokens: ${formatTokenAmount(
communityMint,
communityTokenOwnerRecord.info.governingTokenDepositAmount,
)}`}
</span>
)}
{communityTokenOwnerRecord && councilTokenOwnerRecord && ' | '}
{councilTokenOwnerRecord && (
<span>
{`council tokens: ${formatTokenAmount(
councilMint,
councilTokenOwnerRecord.info.governingTokenDepositAmount,
)}`}
</span>
)}
</>
);
}

View File

@ -110,6 +110,7 @@ export const LABELS = {
GOVERNANCE_OVER: 'governance over',
PROGRAM: 'Program',
MINT: 'Mint',
TOKEN_ACCOUNT: 'Token Account',
REGISTER: 'Register',
REGISTERING: 'Registering',
@ -117,13 +118,15 @@ export const LABELS = {
PROGRAM_ID_LABEL: 'program id',
MINT_ADDRESS_LABEL: 'mint address',
ACCOUNT_ADDRESS: 'account address',
TOKEN_ACCOUNT_ADDRESS: 'token account address',
MIN_TOKENS_TO_CREATE_PROPOSAL: 'min tokens to create proposal',
MIN_INSTRUCTION_HOLD_UP_TIME: 'min instruction hold up time (slots)',
MAX_VOTING_TIME: 'max voting time (slots)',
MIN_INSTRUCTION_HOLD_UP_TIME_DAYS: 'min instruction hold up time (days)',
MAX_VOTING_TIME_DAYS: 'max voting time (days)',
TRANSFER_UPGRADE_AUTHORITY: 'transfer upgrade authority',
TRANSFER_MINT_AUTHORITY: 'transfer mint authority',
UPGRADE_AUTHORITY: 'upgrade authority',
MINT_AUTHORITY: 'mint authority',
TOKEN_OWNER: 'token owner',
PROGRAM_ID: 'Program ID',
INSTRUCTION: 'Instruction',
@ -145,7 +148,7 @@ export const LABELS = {
MINIMUM_SLOT_WAITING_PERIOD: 'Minimum slots between proposal and vote',
SELECT_CONFIG: 'Select Governed Program',
CONFIG: 'Governed Program',
GIST_PLACEHOLDER: 'Github Gist link',
GIST_PLACEHOLDER: 'Github Gist link (optional)',
NAME: 'Name',
PUBLIC_KEY: 'Public Key',
@ -155,7 +158,7 @@ export const LABELS = {
' Please note that during voting, if you withdraw your tokens, your vote will not count towards the voting total. You must wait for the vote to complete in order for your withdrawal to not affect the voting.',
SLOT_MUST_BE_NUMERIC: 'Slot can only be numeric',
SLOT_MUST_BE_GREATER_THAN: 'Slot must be greater than or equal to ',
HOLD_UP_TIME: 'hold up time',
HOLD_UP_TIME_DAYS: 'hold up time (days)',
MIN_SLOT_MUST_BE_NUMERIC: 'Minimum Slot Waiting Period can only be numeric',
TIME_LIMIT_MUST_BE_NUMERIC: 'Time Limit can only be numeric',

View File

@ -86,6 +86,15 @@ export function useWalletTokenOwnerRecord(
);
}
/// Returns all TokenOwnerRecords for the current wallet
export function useWalletTokenOwnerRecords() {
const { wallet } = useWallet();
return useGovernanceAccountsByFilter<TokenOwnerRecord>(TokenOwnerRecord, [
pubkeyFilter(1 + 32 + 32, wallet?.publicKey),
]);
}
export function useProposalAuthority(proposalOwner: PublicKey | undefined) {
const { wallet, connected } = useWallet();
const tokenOwnerRecord = useTokenOwnerRecord(proposalOwner);

View File

@ -1,15 +1,14 @@
import { ParsedAccount } from '@oyster/common';
import { Governance, Proposal } from '../models/accounts';
import { useIsBeyondSlot } from './useIsBeyondSlot';
import { useIsBeyondTimestamp } from './useIsBeyondTimestamp';
export const useHasVotingTimeExpired = (
governance: ParsedAccount<Governance>,
proposal: ParsedAccount<Proposal>,
) => {
return useIsBeyondSlot(
return useIsBeyondTimestamp(
proposal.info.votingAt
? proposal.info.votingAt.toNumber() +
governance.info.config.maxVotingTime.toNumber()
? proposal.info.votingAt.toNumber() + governance.info.config.maxVotingTime
: undefined,
);
};

View File

@ -1,41 +0,0 @@
import { useConnection } from '@oyster/common';
import { useEffect, useState } from 'react';
export const useIsBeyondSlot = (slot: number | undefined) => {
const connection = useConnection();
const [isBeyondSlot, setIsBeyondSlot] = useState<boolean | undefined>();
useEffect(() => {
if (!slot) {
return;
}
const sub = (async () => {
const currentSlot = await connection.getSlot();
if (currentSlot > slot) {
setIsBeyondSlot(true);
return;
}
setIsBeyondSlot(false);
const id = setInterval(() => {
connection.getSlot().then(currentSlot => {
if (currentSlot > slot) {
setIsBeyondSlot(true);
clearInterval(id!);
}
});
}, 5000); // TODO: How to estimate the slot distance to avoid uneccesery checks?
return id;
})();
return () => {
sub.then(id => id && clearInterval(id));
};
}, [connection, slot]);
return isBeyondSlot;
};

View File

@ -0,0 +1,43 @@
import { useConnection } from '@oyster/common';
import moment from 'moment';
import { useEffect, useState } from 'react';
export const useIsBeyondTimestamp = (timestamp: number | undefined) => {
const connection = useConnection();
const [isBeyondTimestamp, setIsBeyondTimestamp] = useState<
boolean | undefined
>();
useEffect(() => {
if (!timestamp) {
return;
}
const sub = (async () => {
const now = moment().unix();
if (now > timestamp) {
setIsBeyondTimestamp(true);
return;
}
setIsBeyondTimestamp(false);
const id = setInterval(() => {
const now = moment().unix();
if (now > timestamp) {
setIsBeyondTimestamp(true);
clearInterval(id!);
}
}, 5000); // TODO: Use actual timestamp to calculate the interval
return id;
})();
return () => {
sub.then(id => id && clearInterval(id));
};
}, [connection, timestamp]);
return isBeyondTimestamp;
};

View File

@ -1,6 +1,8 @@
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { utils } from '@oyster/common';
import { utils, constants } from '@oyster/common';
const { ZERO } = constants;
/// Seed prefix for Governance Program PDAs
export const GOVERNANCE_PROGRAM_SEED = 'governance';
@ -16,6 +18,7 @@ export enum GovernanceAccountType {
VoteRecord = 7,
ProposalInstruction = 8,
MintGovernance = 9,
TokenGovernance = 10,
}
export interface GovernanceAccount {
@ -50,27 +53,52 @@ export function getAccountTypes(accountClass: GovernanceAccountClass) {
GovernanceAccountType.AccountGovernance,
GovernanceAccountType.ProgramGovernance,
GovernanceAccountType.MintGovernance,
GovernanceAccountType.TokenGovernance,
];
default:
throw Error(`${accountClass} account is not supported`);
}
}
export enum VoteThresholdPercentageType {
YesVote,
Quorum,
}
export enum VoteWeightSource {
Deposit,
Snapshot,
}
export enum InstructionExecutionStatus {
Success,
Error,
}
export enum InstructionExecutionFlags {
Ordered,
UseTransaction,
}
export class Realm {
accountType = GovernanceAccountType.Realm;
communityMint: PublicKey;
reserved: BN;
councilMint: PublicKey | undefined;
name: string;
constructor(args: {
communityMint: PublicKey;
reserved: BN;
councilMint: PublicKey | undefined;
name: string;
}) {
this.communityMint = args.communityMint;
this.reserved = args.reserved;
this.councilMint = args.councilMint;
this.name = args.name;
}
@ -79,48 +107,69 @@ export class Realm {
export class GovernanceConfig {
realm: PublicKey;
governedAccount: PublicKey;
yesVoteThresholdPercentage: number;
minTokensToCreateProposal: number;
minInstructionHoldUpTime: BN;
maxVotingTime: BN;
voteThresholdPercentageType: VoteThresholdPercentageType;
voteThresholdPercentage: number;
minTokensToCreateProposal: BN;
minInstructionHoldUpTime: number;
maxVotingTime: number;
voteWeightSource: VoteWeightSource;
proposalCoolOffTime: number;
constructor(args: {
realm: PublicKey;
governedAccount: PublicKey;
yesVoteThresholdPercentage: number;
minTokensToCreateProposal: number;
minInstructionHoldUpTime: BN;
maxVotingTime: BN;
voteThresholdPercentageType?: VoteThresholdPercentageType;
voteThresholdPercentage: number;
minTokensToCreateProposal: BN;
minInstructionHoldUpTime: number;
maxVotingTime: number;
voteWeightSource?: VoteWeightSource;
proposalCoolOffTime?: number;
}) {
this.realm = args.realm;
this.governedAccount = args.governedAccount;
this.yesVoteThresholdPercentage = args.yesVoteThresholdPercentage;
this.voteThresholdPercentageType =
args.voteThresholdPercentageType ?? VoteThresholdPercentageType.YesVote;
this.voteThresholdPercentage = args.voteThresholdPercentage;
this.minTokensToCreateProposal = args.minTokensToCreateProposal;
this.minInstructionHoldUpTime = args.minInstructionHoldUpTime;
this.maxVotingTime = args.maxVotingTime;
this.voteWeightSource = args.voteWeightSource ?? VoteWeightSource.Deposit;
this.proposalCoolOffTime = args.proposalCoolOffTime ?? 0;
}
}
export class Governance {
accountType: GovernanceAccountType;
config: GovernanceConfig;
reserved: BN;
proposalCount: number;
isProgramGovernance() {
return this.accountType === GovernanceAccountType.ProgramGovernance;
}
isAccountGovernance() {
return this.accountType === GovernanceAccountType.AccountGovernance;
}
isMintGovernance() {
return this.accountType === GovernanceAccountType.MintGovernance;
}
isTokenGovernance() {
return this.accountType === GovernanceAccountType.TokenGovernance;
}
constructor(args: {
accountType: number;
config: GovernanceConfig;
reserved?: BN;
proposalCount: number;
}) {
this.accountType = args.accountType;
this.config = args.config;
this.reserved = args.reserved ?? ZERO;
this.proposalCount = args.proposalCount;
}
}
@ -136,12 +185,14 @@ export class TokenOwnerRecord {
governingTokenDepositAmount: BN;
governanceDelegate?: PublicKey;
unrelinquishedVotesCount: number;
totalVotesCount: number;
reserved: BN;
governanceDelegate?: PublicKey;
constructor(args: {
realm: PublicKey;
governingTokenMint: PublicKey;
@ -149,6 +200,7 @@ export class TokenOwnerRecord {
governingTokenDepositAmount: BN;
unrelinquishedVotesCount: number;
totalVotesCount: number;
reserved: BN;
}) {
this.realm = args.realm;
this.governingTokenMint = args.governingTokenMint;
@ -156,6 +208,7 @@ export class TokenOwnerRecord {
this.governingTokenDepositAmount = args.governingTokenDepositAmount;
this.unrelinquishedVotesCount = args.unrelinquishedVotesCount;
this.totalVotesCount = args.totalVotesCount;
this.reserved = args.reserved;
}
}
@ -212,31 +265,35 @@ export class Proposal {
signatoriesSignedOffCount: number;
descriptionLink: string;
name: string;
yesVotesCount: BN;
noVotesCount: BN;
instructionsExecutedCount: number;
instructionsCount: number;
instructionsNextIndex: number;
draftAt: BN;
signingOffAt: BN | null;
votingAt: BN | null;
votingAtSlot: BN | null;
votingCompletedAt: BN | null;
executingAt: BN | null;
closedAt: BN | null;
instructionsExecutedCount: number;
executionFlags: InstructionExecutionFlags | null;
instructionsCount: number;
name: string;
instructionsNextIndex: number;
descriptionLink: string;
constructor(args: {
governance: PublicKey;
@ -252,12 +309,14 @@ export class Proposal {
draftAt: BN;
signingOffAt: BN | null;
votingAt: BN | null;
votingAtSlot: BN | null;
votingCompletedAt: BN | null;
executingAt: BN | null;
closedAt: BN | null;
instructionsExecutedCount: number;
instructionsCount: number;
instructionsNextIndex: number;
executionFlags: InstructionExecutionFlags;
}) {
this.governance = args.governance;
this.governingTokenMint = args.governingTokenMint;
@ -272,12 +331,14 @@ export class Proposal {
this.draftAt = args.draftAt;
this.signingOffAt = args.signingOffAt;
this.votingAt = args.votingAt;
this.votingAtSlot = args.votingAtSlot;
this.votingCompletedAt = args.votingCompletedAt;
this.executingAt = args.executingAt;
this.closedAt = args.closedAt;
this.instructionsExecutedCount = args.instructionsExecutedCount;
this.instructionsCount = args.instructionsCount;
this.instructionsNextIndex = args.instructionsNextIndex;
this.executionFlags = args.executionFlags;
}
}
@ -381,19 +442,25 @@ export class InstructionData {
export class ProposalInstruction {
accountType = GovernanceAccountType.ProposalInstruction;
proposal: PublicKey;
holdUpTime: BN;
instructionIndex: number;
holdUpTime: number;
instruction: InstructionData;
executedAt: BN | null;
executionStatus: InstructionExecutionStatus | null;
constructor(args: {
proposal: PublicKey;
holdUpTime: BN;
instructionIndex: number;
holdUpTime: number;
instruction: InstructionData;
executedAt: BN | null;
executionStatus: InstructionExecutionStatus | null;
}) {
this.proposal = args.proposal;
this.instructionIndex = args.instructionIndex;
this.holdUpTime = args.holdUpTime;
this.instruction = args.instruction;
this.executedAt = args.executedAt;
this.executionStatus = args.executionStatus;
}
}

View File

@ -7,4 +7,5 @@ export enum GovernanceType {
Account,
Program,
Mint,
Token,
}

View File

@ -60,6 +60,11 @@ export const GovernanceError: Record<number, string> = [
"Provided upgrade authority doesn't match current program upgrade authority", // InvalidUpgradeAuthority
'Current program upgrade authority must sign transaction', // UpgradeAuthorityMustSign
'Given program is not upgradable', //ProgramNotUpgradable
'Invalid token owner', //InvalidTokenOwner
'Current token owner must sign transaction', // TokenOwnerMustSign
'Given VoteThresholdPercentageType is not supported', //VoteThresholdPercentageTypeNotSupported
'Given VoteWeightSource is not supported', //VoteWeightSourceNotSupported
'Proposal cool off time is not supported', // ProposalCoolOffTimeNotSupported
];
export function getTransactionErrorMsg(error: SendTransactionError) {

View File

@ -24,6 +24,7 @@ export enum GovernanceInstruction {
ExecuteInstruction = 16,
CreateMintGovernance = 17,
CreateTokenGovernance = 18,
}
export class CreateRealmArgs {
@ -85,6 +86,18 @@ export class CreateMintGovernanceArgs {
}
}
export class CreateTokenGovernanceArgs {
instruction: GovernanceInstruction =
GovernanceInstruction.CreateTokenGovernance;
config: GovernanceConfig;
transferTokenOwner: boolean;
constructor(args: { config: GovernanceConfig; transferTokenOwner: boolean }) {
this.config = args.config;
this.transferTokenOwner = !!args.transferTokenOwner;
}
}
export class CreateProposalArgs {
instruction: GovernanceInstruction = GovernanceInstruction.CreateProposal;
name: string;

View File

@ -10,6 +10,7 @@ export async function createUpgradeInstruction(
programId: PublicKey,
bufferAddress: PublicKey,
governance: PublicKey,
spillAddress: PublicKey,
) {
const PROGRAM_IDS = utils.programIds();
@ -35,7 +36,7 @@ export async function createUpgradeInstruction(
isSigner: false,
},
{
pubkey: governance,
pubkey: spillAddress,
isWritable: true,
isSigner: false,
},

View File

@ -15,6 +15,7 @@ import {
CreateProgramGovernanceArgs,
CreateProposalArgs,
CreateRealmArgs,
CreateTokenGovernanceArgs,
DepositGoverningTokensArgs,
ExecuteInstructionArgs,
FinalizeVoteArgs,
@ -39,12 +40,6 @@ import {
} from './accounts';
import { serialize } from 'borsh';
// TODO: Review the limits. Most likely they are leftovers from the legacy version
export const MAX_PROPOSAL_DESCRIPTION_LENGTH = 200;
export const MAX_PROPOSAL_NAME_LENGTH = 32;
export const MAX_REALM_NAME_LENGTH = 32;
export const MAX_INSTRUCTION_BASE64_LENGTH = 450;
// Temp. workaround to support u16.
(BinaryReader.prototype as any).readU16 = function () {
const reader = (this as unknown) as BinaryReader;
@ -138,6 +133,17 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
],
},
],
[
CreateTokenGovernanceArgs,
{
kind: 'struct',
fields: [
['instruction', 'u8'],
['config', GovernanceConfig],
['transferTokenOwner', 'u8'],
],
},
],
[
CreateProposalArgs,
{
@ -205,7 +211,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
fields: [
['instruction', 'u8'],
['index', 'u16'],
['holdUpTime', 'u64'],
['holdUpTime', 'u32'],
['instructionData', InstructionData],
],
},
@ -253,6 +259,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
fields: [
['accountType', 'u8'],
['communityMint', 'pubkey'],
['reserved', 'u64'],
['councilMint', { kind: 'option', type: 'pubkey' }],
['name', 'string'],
],
@ -265,6 +272,7 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
fields: [
['accountType', 'u8'],
['config', GovernanceConfig],
['reserved', 'u64'],
['proposalCount', 'u32'],
],
},
@ -276,10 +284,13 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
fields: [
['realm', 'pubkey'],
['governedAccount', 'pubkey'],
['yesVoteThresholdPercentage', 'u8'],
['minTokensToCreateProposal', 'u16'],
['minInstructionHoldUpTime', 'u64'],
['maxVotingTime', 'u64'],
['voteThresholdPercentageType', 'u8'],
['voteThresholdPercentage', 'u8'],
['minTokensToCreateProposal', 'u64'],
['minInstructionHoldUpTime', 'u32'],
['maxVotingTime', 'u32'],
['voteWeightSource', 'u8'],
['proposalCoolOffTime', 'u32'],
],
},
],
@ -293,9 +304,10 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
['governingTokenMint', 'pubkey'],
['governingTokenOwner', 'pubkey'],
['governingTokenDepositAmount', 'u64'],
['governanceDelegate', { kind: 'option', type: 'pubkey' }],
['unrelinquishedVotesCount', 'u32'],
['totalVotesCount', 'u32'],
['reserved', 'u64'],
['governanceDelegate', { kind: 'option', type: 'pubkey' }],
],
},
],
@ -311,19 +323,21 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
['tokenOwnerRecord', 'pubkey'],
['signatoriesCount', 'u8'],
['signatoriesSignedOffCount', 'u8'],
['descriptionLink', 'string'],
['name', 'string'],
['yesVotesCount', 'u64'],
['noVotesCount', 'u64'],
['draftAt', 'u64'],
['signingOffAt', { kind: 'option', type: 'u64' }],
['votingAt', { kind: 'option', type: 'u64' }],
['votingCompletedAt', { kind: 'option', type: 'u64' }],
['executingAt', { kind: 'option', type: 'u64' }],
['closedAt', { kind: 'option', type: 'u64' }],
['instructionsExecutedCount', 'u16'],
['instructionsCount', 'u16'],
['instructionsNextIndex', 'u16'],
['draftAt', 'u64'],
['signingOffAt', { kind: 'option', type: 'u64' }],
['votingAt', { kind: 'option', type: 'u64' }],
['votingAtSlot', { kind: 'option', type: 'u64' }],
['votingCompletedAt', { kind: 'option', type: 'u64' }],
['executingAt', { kind: 'option', type: 'u64' }],
['closedAt', { kind: 'option', type: 'u64' }],
['executionFlags', { kind: 'option', type: 'u8' }],
['name', 'string'],
['descriptionLink', 'string'],
],
},
],
@ -369,9 +383,11 @@ export const GOVERNANCE_SCHEMA = new Map<any, any>([
fields: [
['accountType', 'u8'],
['proposal', 'pubkey'],
['holdUpTime', 'u64'],
['instructionIndex', 'u16'],
['holdUpTime', 'u32'],
['instruction', InstructionData],
['executedAt', { kind: 'option', type: 'u64' }],
['executionStatus', { kind: 'option', type: 'u8' }],
],
},
],

View File

@ -0,0 +1,89 @@
import { utils } from '@oyster/common';
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { GOVERNANCE_SCHEMA } from './serialisation';
import { serialize } from 'borsh';
import { GovernanceConfig } from './accounts';
import { CreateTokenGovernanceArgs } from './instructions';
export const withCreateTokenGovernance = async (
instructions: TransactionInstruction[],
realm: PublicKey,
config: GovernanceConfig,
transferTokenOwner: boolean,
tokenOwner: PublicKey,
payer: PublicKey,
): Promise<{ governanceAddress: PublicKey }> => {
const PROGRAM_IDS = utils.programIds();
const args = new CreateTokenGovernanceArgs({
config,
transferTokenOwner,
});
const data = Buffer.from(serialize(GOVERNANCE_SCHEMA, args));
const [tokenGovernanceAddress] = await PublicKey.findProgramAddress(
[
Buffer.from('token-governance'),
realm.toBuffer(),
config.governedAccount.toBuffer(),
],
PROGRAM_IDS.governance.programId,
);
const keys = [
{
pubkey: realm,
isWritable: false,
isSigner: false,
},
{
pubkey: tokenGovernanceAddress,
isWritable: true,
isSigner: false,
},
{
pubkey: config.governedAccount,
isWritable: true,
isSigner: false,
},
{
pubkey: tokenOwner,
isWritable: false,
isSigner: true,
},
{
pubkey: payer,
isWritable: false,
isSigner: true,
},
{
pubkey: PROGRAM_IDS.token,
isWritable: false,
isSigner: false,
},
{
pubkey: PROGRAM_IDS.system,
isWritable: false,
isSigner: false,
},
{
pubkey: SYSVAR_RENT_PUBKEY,
isWritable: false,
isSigner: false,
},
];
instructions.push(
new TransactionInstruction({
keys,
programId: PROGRAM_IDS.governance.programId,
data,
}),
);
return { governanceAddress: tokenGovernanceAddress };
};

View File

@ -14,7 +14,3 @@ export const formDefaults = {
...formVerticalLayout,
validateMessages: formValidateMessages,
};
export const formSlotInputStyle = {
width: 250,
};

View File

@ -0,0 +1,9 @@
import { MintInfo } from '@solana/spl-token';
import BN from 'bn.js';
import { BigNumber } from 'bignumber.js';
export function formatTokenAmount(mint: MintInfo | undefined, amount: BN) {
return mint
? new BigNumber(amount.toString()).shiftedBy(-mint.decimals).toFormat()
: amount.toString();
}

View File

@ -20,7 +20,11 @@ const GovernanceArtifacts = () => {
const [realmName, setRealmName] = useState('');
const [communityMint, setCommunityMint] = useState('');
const [councilMint, setCouncilMint] = useState('');
const [instruction, setInstruction] = useState('');
const [tokenGovernance, setTokenGovernance] = useState({
tokenAccountAddress: '',
beneficiaryTokenAccountAddress: '',
});
const [generated, setGenerated] = useState(false);
const onGenerateArtifacts = async () => {
@ -30,13 +34,14 @@ const GovernanceArtifacts = () => {
communityMintAddress,
councilMintAddress,
realmName,
instructionBase64,
tokenGovernance,
} = await generateGovernanceArtifacts(connection, wallet);
setCommunityMint(communityMintAddress.toBase58());
setCouncilMint(councilMintAddress.toBase58());
setRealmName(realmName);
setInstruction(instructionBase64);
setTokenGovernance(tokenGovernance);
setGenerated(true);
};
@ -64,8 +69,16 @@ const GovernanceArtifacts = () => {
</div>
<div>
<h3>instruction: </h3>
<div className="test-data">{instruction}</div>
<h3>token governance - token account: </h3>
<div className="test-data">
{tokenGovernance.tokenAccountAddress}
</div>
</div>
<div>
<h3>token governance - beneficiary token account: </h3>
<div className="test-data">
{tokenGovernance.beneficiaryTokenAccountAddress}
</div>
</div>
</>
)}

View File

@ -1,4 +1,4 @@
import { Avatar, Badge, Col, List, Row } from 'antd';
import { Badge, Col, List, Row } from 'antd';
import React, { useMemo, useState } from 'react';
import { useRealm } from '../../contexts/GovernanceContext';
@ -11,6 +11,7 @@ import { AddNewProposal } from './NewProposal';
import { useKeyParam } from '../../hooks/useKeyParam';
import { Proposal, ProposalState } from '../../models/accounts';
import { ClockCircleOutlined } from '@ant-design/icons';
import { GovernanceBadge } from '../../components/GovernanceBadge/governanceBadge';
const PAGE_SIZE = 10;
@ -34,8 +35,6 @@ export const GovernanceView = () => {
const mint = communityTokenMint?.toBase58() || '';
const color = governance?.info.isProgramGovernance() ? 'green' : 'gray';
const proposalItems = useMemo(() => {
const getCompareKey = (p: Proposal) =>
`${p.state === ProposalState.Voting ? 0 : 1}${p.name}`;
@ -71,16 +70,14 @@ export const GovernanceView = () => {
>
<Col flex="auto" xxl={15} xs={24} className="proposals-container">
<div className="proposals-header">
{governance?.info.isMintGovernance() ? (
<TokenIcon
mintAddress={governance?.info.config.governedAccount}
{governance && (
<GovernanceBadge
size={60}
/>
) : (
<Avatar style={{ background: color, marginRight: 5 }} size={60}>
{governance?.info.config.governedAccount.toBase58().slice(0, 5)}
</Avatar>
governance={governance}
showVotingCount={false}
></GovernanceBadge>
)}
<div>
<h1>{realm?.info.name}</h1>
<h2>

View File

@ -2,10 +2,7 @@ import React, { useState } from 'react';
import { ButtonProps, Radio } from 'antd';
import { Form, Input } from 'antd';
import { PublicKey } from '@solana/web3.js';
import {
MAX_PROPOSAL_DESCRIPTION_LENGTH,
MAX_PROPOSAL_NAME_LENGTH,
} from '../../models/serialisation';
import { LABELS } from '../../constants';
import { contexts, ParsedAccount } from '@oyster/common';
import { createProposal } from '../../actions/createProposal';
@ -81,7 +78,7 @@ export function AddNewProposal({
governance.info.config.realm,
governance.pubkey,
values.name,
values.descriptionLink,
values.descriptionLink ?? '',
governingTokenMint,
proposalIndex,
);
@ -136,17 +133,14 @@ export function AddNewProposal({
label={LABELS.NAME_LABEL}
rules={[{ required: true }]}
>
<Input maxLength={MAX_PROPOSAL_NAME_LENGTH} />
<Input />
</Form.Item>
<Form.Item
name="descriptionLink"
label={LABELS.DESCRIPTION_LABEL}
rules={[{ required: true }]}
rules={[{ required: false }]}
>
<Input
maxLength={MAX_PROPOSAL_DESCRIPTION_LENGTH}
placeholder={LABELS.GIST_PLACEHOLDER}
/>
<Input placeholder={LABELS.GIST_PLACEHOLDER} />
</Form.Item>
</ModalFormAction>
);

View File

@ -10,26 +10,51 @@ import { RegisterRealm } from './registerRealm';
import { LABELS } from '../../constants';
import { RealmBadge } from '../../components/RealmBadge/realmBadge';
import { useWalletTokenOwnerRecords } from '../../hooks/apiHooks';
import { RealmDepositBadge } from '../../components/RealmDepositBadge/realmDepositBadge';
export const HomeView = () => {
const history = useHistory();
const realms = useRealms();
const tokenOwnerRecords = useWalletTokenOwnerRecords();
const realmItems = useMemo(() => {
return realms
.sort((r1, r2) => r1.info.name.localeCompare(r2.info.name))
.map(r => ({
href: '/realm/' + r.pubkey.toBase58(),
title: r.info.name,
badge: (
<RealmBadge
communityMint={r.info.communityMint}
councilMint={r.info.councilMint}
></RealmBadge>
),
key: r.pubkey.toBase58(),
}));
}, [realms]);
.map(r => {
const communityTokenOwnerRecord = tokenOwnerRecords.find(
tor =>
tor.info.governingTokenMint.toBase58() ===
r.info.communityMint.toBase58(),
);
const councilTokenOwnerRecord =
r.info.councilMint &&
tokenOwnerRecords.find(
tor =>
tor.info.governingTokenMint.toBase58() ===
r.info.councilMint!.toBase58(),
);
return {
href: '/realm/' + r.pubkey.toBase58(),
title: r.info.name,
badge: (
<RealmBadge
communityMint={r.info.communityMint}
councilMint={r.info.councilMint}
></RealmBadge>
),
key: r.pubkey.toBase58(),
description: (
<RealmDepositBadge
communityTokenOwnerRecord={communityTokenOwnerRecord}
councilTokenOwnerRecord={councilTokenOwnerRecord}
></RealmDepositBadge>
),
};
});
}, [realms, tokenOwnerRecords]);
return (
<>
@ -56,6 +81,7 @@ export const HomeView = () => {
<List.Item.Meta
avatar={item.badge}
title={item.title}
description={item.description}
></List.Item.Meta>
</List.Item>
)}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { ButtonProps, Switch } from 'antd';
import { Form, Input } from 'antd';
import { PublicKey } from '@solana/web3.js';
import { MAX_REALM_NAME_LENGTH } from '../../models/serialisation';
import { LABELS } from '../../constants';
import { contexts } from '@oyster/common';
import { Redirect } from 'react-router';
@ -65,7 +65,7 @@ export function RegisterRealm({ buttonProps }: { buttonProps: ButtonProps }) {
label={LABELS.NAME_LABEL}
rules={[{ required: true }]}
>
<Input maxLength={MAX_REALM_NAME_LENGTH} />
<Input />
</Form.Item>
<MintFormItem

View File

@ -399,33 +399,35 @@ function InnerProposalView({
<Row>
<Col span={24}>
<Tabs
defaultActiveKey="1"
defaultActiveKey="description"
size="large"
style={{ marginBottom: 32 }}
>
<TabPane tab="Description" key="1">
{loading ? (
<Spin />
) : isUrl ? (
failed ? (
<p>
{LABELS.DESCRIPTION}:{' '}
<a
href={proposal.info.descriptionLink}
target="_blank"
rel="noopener noreferrer"
>
{msg ? msg : LABELS.NO_LOAD}
</a>
</p>
{proposal.info.descriptionLink && (
<TabPane tab="Description" key="description">
{loading ? (
<Spin />
) : isUrl ? (
failed ? (
<p>
{LABELS.DESCRIPTION}:{' '}
<a
href={proposal.info.descriptionLink}
target="_blank"
rel="noopener noreferrer"
>
{msg ? msg : LABELS.NO_LOAD}
</a>
</p>
) : (
<ReactMarkdown children={content} />
)
) : (
<ReactMarkdown children={content} />
)
) : (
content
)}
</TabPane>
<TabPane tab={LABELS.INSTRUCTIONS} key="2">
content
)}
</TabPane>
)}
<TabPane tab={LABELS.INSTRUCTIONS} key="instructions">
<Row
gutter={[
{ xs: 8, sm: 16, md: 24, lg: 32 },
@ -464,10 +466,10 @@ function getMinRequiredYesVoteScore(
governingTokenMint: MintInfo,
): string {
const minVotes =
governance.info.config.yesVoteThresholdPercentage === 100
governance.info.config.voteThresholdPercentage === 100
? governingTokenMint.supply
: governingTokenMint.supply
.mul(new BN(governance.info.config.yesVoteThresholdPercentage))
.mul(new BN(governance.info.config.voteThresholdPercentage))
.div(new BN(100));
return new BigNumber(minVotes.toString())

View File

@ -76,7 +76,7 @@ export function InstructionCard({
<>
<p>{`${LABELS.INSTRUCTION}: ${instructionDetails.dataBase64}`}</p>
<p>
{LABELS.HOLD_UP_TIME}: {instruction.info.holdUpTime.toNumber()}
{LABELS.HOLD_UP_TIME_DAYS}: {instruction.info.holdUpTime / 86400}
</p>
</>
}

View File

@ -1,5 +1,5 @@
import { PlusCircleOutlined } from '@ant-design/icons';
import { ExplorerLink, ParsedAccount, utils } from '@oyster/common';
import { ExplorerLink, ParsedAccount, utils, contexts } from '@oyster/common';
import { Token } from '@solana/spl-token';
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import {
@ -14,27 +14,28 @@ import {
} from 'antd';
import React from 'react';
import { useState } from 'react';
import { AccountFormItem } from '../../../components/AccountFormItem/accountFormItem';
import { Governance } from '../../../models/accounts';
import { createUpgradeInstruction } from '../../../models/sdkInstructions';
import {
MAX_INSTRUCTION_BASE64_LENGTH,
serializeInstructionToBase64,
} from '../../../models/serialisation';
import { formVerticalLayout } from '../../../tools/forms';
import { serializeInstructionToBase64 } from '../../../models/serialisation';
import { formDefaults } from '../../../tools/forms';
const { useWallet } = contexts.Wallet;
const InstructionInput = ({
export default function InstructionInput({
governance,
onChange,
}: {
governance: ParsedAccount<Governance>;
onChange?: (v: any) => void;
}) => {
}) {
const [isFormVisible, setIsFormVisible] = useState(false);
const [instruction, setInstruction] = useState('');
const [form] = Form.useForm();
const creatorsEnabled =
governance.info.isMintGovernance() || governance.info.isProgramGovernance();
governance.info.isMintGovernance() ||
governance.info.isProgramGovernance() ||
governance.info.isTokenGovernance();
const updateInstruction = (instruction: string) => {
setInstruction(instruction);
@ -53,7 +54,6 @@ const InstructionInput = ({
<Input.TextArea
value={instruction}
onChange={e => updateInstruction(e.target.value)}
maxLength={MAX_INSTRUCTION_BASE64_LENGTH}
placeholder={`base64 encoded serialized Solana Instruction`}
/>
</Col>
@ -75,7 +75,11 @@ const InstructionInput = ({
okText="Create"
onCancel={() => setIsFormVisible(false)}
title={`Create ${
governance.info.isProgramGovernance() ? 'Upgrade Program' : 'Mint To'
governance.info.isProgramGovernance()
? 'Upgrade Program'
: governance.info.isMintGovernance()
? 'Mint To'
: 'Transfer'
} Instruction`}
>
{governance.info.isProgramGovernance() && (
@ -92,10 +96,17 @@ const InstructionInput = ({
governance={governance}
></MintToForm>
)}
{governance.info.isTokenGovernance() && (
<TransferForm
form={form}
onCreateInstruction={onCreateInstruction}
governance={governance}
></TransferForm>
)}
</Modal>
</>
);
};
}
const UpgradeProgramForm = ({
form,
@ -106,17 +117,24 @@ const UpgradeProgramForm = ({
governance: ParsedAccount<Governance>;
onCreateInstruction: (instruction: TransactionInstruction) => void;
}) => {
const { wallet } = useWallet();
if (!wallet?.publicKey) {
return <div>Wallet not connected</div>;
}
const onCreate = async ({ bufferAddress }: { bufferAddress: string }) => {
const upgradeIx = await createUpgradeInstruction(
governance.info.config.governedAccount,
new PublicKey(bufferAddress),
governance.pubkey,
wallet.publicKey!,
);
onCreateInstruction(upgradeIx);
};
return (
<Form {...formVerticalLayout} form={form} onFinish={onCreate}>
<Form {...formDefaults} form={form} onFinish={onCreate}>
<Form.Item label="program id">
<ExplorerLink
address={governance.info.config.governedAccount}
@ -126,6 +144,9 @@ const UpgradeProgramForm = ({
<Form.Item label="upgrade authority (governance account)">
<ExplorerLink address={governance.pubkey} type="address" />
</Form.Item>
<Form.Item label="spill account (wallet)">
<ExplorerLink address={wallet.publicKey} type="address" />
</Form.Item>
<Form.Item
name="bufferAddress"
label="buffer address"
@ -169,7 +190,7 @@ const MintToForm = ({
return (
<Form
{...formVerticalLayout}
{...formDefaults}
form={form}
onFinish={onCreate}
initialValues={{ amount: 1 }}
@ -197,4 +218,59 @@ const MintToForm = ({
);
};
export default InstructionInput;
const TransferForm = ({
form,
governance,
onCreateInstruction,
}: {
form: FormInstance;
governance: ParsedAccount<Governance>;
onCreateInstruction: (instruction: TransactionInstruction) => void;
}) => {
const onCreate = async ({
destination,
amount,
}: {
destination: string;
amount: number;
}) => {
const { token: tokenProgramId } = utils.programIds();
const mintToIx = Token.createTransferInstruction(
tokenProgramId,
governance.info.config.governedAccount,
new PublicKey(destination),
governance.pubkey,
[],
amount,
);
onCreateInstruction(mintToIx);
};
return (
<Form
{...formDefaults}
form={form}
onFinish={onCreate}
initialValues={{ amount: 1 }}
>
<Form.Item label="source account">
<ExplorerLink
address={governance.info.config.governedAccount}
type="address"
/>
</Form.Item>
<Form.Item label="account owner (governance account)">
<ExplorerLink address={governance.pubkey} type="address" />
</Form.Item>
<AccountFormItem
name="destination"
label="destination account"
></AccountFormItem>
<Form.Item name="amount" label="amount" rules={[{ required: true }]}>
<InputNumber min={1} />
</Form.Item>
</Form>
);
};

View File

@ -12,7 +12,7 @@ import { useProposalAuthority } from '../../../hooks/apiHooks';
import { insertInstruction } from '../../../actions/insertInstruction';
import '../style.less';
import { formVerticalLayout } from '../../../tools/forms';
import { formDefaults } from '../../../tools/forms';
import InstructionInput from './InstructionInput';
const { useWallet } = contexts.Wallet;
@ -47,7 +47,7 @@ export function NewInstructionCard({
proposal,
proposalAuthority!.pubkey,
index,
values.holdUpTime,
values.holdUpTime * 86400,
values.instruction,
);
@ -57,35 +57,33 @@ export function NewInstructionCard({
}
};
const minHoldUpTime = governance.info.config.minInstructionHoldUpTime / 86400;
return !proposalAuthority ? null : (
<Card
title="New Instruction"
actions={[<SaveOutlined key="save" onClick={form.submit} />]}
>
<Form
{...formVerticalLayout}
{...formDefaults}
form={form}
name="control-hooks"
onFinish={onFinish}
initialValues={{
holdUpTime:
governance.info.config.minInstructionHoldUpTime.toNumber(),
holdUpTime: minHoldUpTime,
}}
>
<Form.Item
name="holdUpTime"
label={LABELS.HOLD_UP_TIME}
label={LABELS.HOLD_UP_TIME_DAYS}
rules={[{ required: true }]}
>
<InputNumber
maxLength={64}
min={governance.info.config.minInstructionHoldUpTime.toNumber()}
/>
<InputNumber min={minHoldUpTime} />
</Form.Item>
<Form.Item
name="instruction"
label="Instruction"
label="instruction"
rules={[{ required: true }]}
>
<InstructionInput governance={governance}></InstructionInput>

View File

@ -1,8 +1,11 @@
import { Col, List, Row } from 'antd';
import { Col, List, Row, Typography } from 'antd';
import React, { useMemo } from 'react';
import { useRealm } from '../../contexts/GovernanceContext';
import { useGovernancesByRealm } from '../../hooks/apiHooks';
import {
useGovernancesByRealm,
useWalletTokenOwnerRecord,
} from '../../hooks/apiHooks';
import './style.less'; // Don't remove this line, it will break dark mode if you do due to weird transpiling conditions
import { Background } from '../../components/Background';
@ -14,7 +17,11 @@ import { DepositGoverningTokens } from './DepositGoverningTokens';
import { WithdrawGoverningTokens } from './WithdrawGoverningTokens';
import { RealmBadge } from '../../components/RealmBadge/realmBadge';
import { GovernanceBadge } from './governanceBadge';
import { GovernanceBadge } from '../../components/GovernanceBadge/governanceBadge';
import AccountDescription from './accountDescription';
import { RealmDepositBadge } from '../../components/RealmDepositBadge/realmDepositBadge';
const { Text } = Typography;
export const RealmView = () => {
const history = useHistory();
@ -23,6 +30,16 @@ export const RealmView = () => {
const realm = useRealm(realmKey);
const governances = useGovernancesByRealm(realmKey);
const communityTokenOwnerRecord = useWalletTokenOwnerRecord(
realm?.pubkey,
realm?.info.communityMint,
);
const councilTokenOwnerRecord = useWalletTokenOwnerRecord(
realm?.pubkey,
realm?.info.councilMint,
);
const governanceItems = useMemo(() => {
return governances
.sort((g1, g2) =>
@ -35,6 +52,7 @@ export const RealmView = () => {
href: '/governance/' + g.pubkey,
title: g.info.config.governedAccount.toBase58(),
badge: <GovernanceBadge governance={g}></GovernanceBadge>,
description: <AccountDescription governance={g}></AccountDescription>,
}));
}, [governances]);
@ -52,8 +70,14 @@ export const RealmView = () => {
councilMint={realm?.info.councilMint}
></RealmBadge>
<Col>
<Col style={{ textAlign: 'left', marginLeft: 8 }}>
<h1>{realm?.info.name}</h1>
<Text type="secondary">
<RealmDepositBadge
communityTokenOwnerRecord={communityTokenOwnerRecord}
councilTokenOwnerRecord={councilTokenOwnerRecord}
></RealmDepositBadge>
</Text>
</Col>
</Row>
</Col>
@ -102,6 +126,7 @@ export const RealmView = () => {
<List.Item.Meta
title={item.title}
avatar={item.badge}
description={item.description}
></List.Item.Meta>
</List.Item>
)}

View File

@ -0,0 +1,40 @@
import React, { useEffect, useState } from 'react';
import { Governance } from '../../models/accounts';
import {
deserializeMint,
ParsedAccount,
useAccount,
useConnection,
} from '@oyster/common';
import { MintInfo } from '@solana/spl-token';
export default function AccountDescription({
governance,
}: {
governance: ParsedAccount<Governance>;
}) {
const connection = useConnection();
const [mintAccount, setMintAccount] = useState<MintInfo | null>();
const tokenAccount = useAccount(governance.info.config.governedAccount);
useEffect(() => {
if (!governance.info.isMintGovernance()) {
return;
}
connection
.getAccountInfo(governance.info.config.governedAccount)
.then(info => info && deserializeMint(info.data))
.then(setMintAccount);
}, [connection, governance]);
return (
<>
{governance.info.isTokenGovernance() &&
tokenAccount &&
`Token Balance: ${tokenAccount.info.amount}`}
{mintAccount && `Mint Supply: ${mintAccount.supply}`}
</>
);
}

View File

@ -1,36 +0,0 @@
import { ParsedAccount, TokenIcon } from '@oyster/common';
import { Avatar, Badge } from 'antd';
import React from 'react';
import { Governance, ProposalState } from '../../models/accounts';
import { useProposalsByGovernance } from '../../hooks/apiHooks';
export function GovernanceBadge({
governance,
}: {
governance: ParsedAccount<Governance>;
}) {
const proposals = useProposalsByGovernance(governance?.pubkey);
const color = governance.info.isProgramGovernance() ? 'green' : 'gray';
return (
<Badge
count={
proposals.filter(p => p.info.state === ProposalState.Voting).length
}
>
<div style={{ width: 55, height: 45 }}>
{governance.info.isMintGovernance() ? (
<TokenIcon
mintAddress={governance.info.config.governedAccount}
size={40}
/>
) : (
<Avatar size="large" gap={2} style={{ background: color }}>
{governance.info.config.governedAccount.toBase58().slice(0, 5)}
</Avatar>
)}
</div>
</Badge>
);
}

View File

@ -10,12 +10,12 @@ import { Redirect } from 'react-router';
import { GovernanceType } from '../../models/enums';
import { registerGovernance } from '../../actions/registerGovernance';
import { GovernanceConfig } from '../../models/accounts';
import BN from 'bn.js';
import { useKeyParam } from '../../hooks/useKeyParam';
import { ModalFormAction } from '../../components/ModalFormAction/modalFormAction';
import { formSlotInputStyle } from '../../tools/forms';
import { AccountFormItem } from '../../components/AccountFormItem/accountFormItem';
import BN from 'bn.js';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
@ -43,10 +43,10 @@ export function RegisterGovernance({
const config = new GovernanceConfig({
realm: realmKey,
governedAccount: new PublicKey(values.governedAccountAddress),
yesVoteThresholdPercentage: values.yesVoteThresholdPercentage,
minTokensToCreateProposal: values.minTokensToCreateProposal,
minInstructionHoldUpTime: new BN(values.minInstructionHoldUpTime),
maxVotingTime: new BN(values.maxVotingTime),
voteThresholdPercentage: values.yesVoteThresholdPercentage,
minTokensToCreateProposal: new BN(values.minTokensToCreateProposal),
minInstructionHoldUpTime: values.minInstructionHoldUpTime * 86400,
maxVotingTime: values.maxVotingTime * 86400,
});
return await registerGovernance(
connection,
@ -89,6 +89,9 @@ export function RegisterGovernance({
{LABELS.PROGRAM}
</Radio.Button>
<Radio.Button value={GovernanceType.Mint}>{LABELS.MINT}</Radio.Button>
<Radio.Button value={GovernanceType.Token}>
{LABELS.TOKEN_ACCOUNT}
</Radio.Button>
</Radio.Group>
</Form.Item>
@ -99,19 +102,24 @@ export function RegisterGovernance({
? LABELS.PROGRAM_ID_LABEL
: governanceType === GovernanceType.Mint
? LABELS.MINT_ADDRESS_LABEL
: governanceType === GovernanceType.Token
? LABELS.TOKEN_ACCOUNT_ADDRESS
: LABELS.ACCOUNT_ADDRESS
}
></AccountFormItem>
{(governanceType === GovernanceType.Program ||
governanceType === GovernanceType.Mint) && (
governanceType === GovernanceType.Mint ||
governanceType === GovernanceType.Token) && (
<Form.Item
name="transferAuthority"
label={
label={`transfer ${
governanceType === GovernanceType.Program
? LABELS.TRANSFER_UPGRADE_AUTHORITY
: LABELS.TRANSFER_MINT_AUTHORITY
}
? LABELS.UPGRADE_AUTHORITY
: governanceType === GovernanceType.Mint
? LABELS.MINT_AUTHORITY
: LABELS.TOKEN_OWNER
} to governance`}
valuePropName="checked"
>
<Checkbox></Checkbox>
@ -129,20 +137,20 @@ export function RegisterGovernance({
<Form.Item
name="minInstructionHoldUpTime"
label={LABELS.MIN_INSTRUCTION_HOLD_UP_TIME}
label={LABELS.MIN_INSTRUCTION_HOLD_UP_TIME_DAYS}
rules={[{ required: true }]}
initialValue={1}
>
<InputNumber min={1} style={formSlotInputStyle} />
<InputNumber min={0} />
</Form.Item>
<Form.Item
name="maxVotingTime"
label={LABELS.MAX_VOTING_TIME}
label={LABELS.MAX_VOTING_TIME_DAYS}
rules={[{ required: true }]}
initialValue={1000000}
initialValue={3}
>
<InputNumber min={1} style={formSlotInputStyle} />
<InputNumber min={1} />
</Form.Item>
<Form.Item
name="yesVoteThresholdPercentage"