From 55c3598088089befb9f02beb7c8807c411e83c0d Mon Sep 17 00:00:00 2001 From: Maximilian Schneider Date: Tue, 24 Aug 2021 05:16:47 +0200 Subject: [PATCH] refactor state management --- hooks/useRealm.tsx | 67 +++++++++++++++ pages/dao/[symbol].tsx | 151 +++++++++++--------------------- stores/useWalletStore.tsx | 175 +++++++++++++++++++++----------------- 3 files changed, 216 insertions(+), 177 deletions(-) create mode 100644 hooks/useRealm.tsx diff --git a/hooks/useRealm.tsx b/hooks/useRealm.tsx new file mode 100644 index 0000000..62348a8 --- /dev/null +++ b/hooks/useRealm.tsx @@ -0,0 +1,67 @@ +import { PublicKey } from '@solana/web3.js' +import { useEffect, useMemo } from 'react' +import { RealmInfo } from '../@types/types' +import useWalletStore from '../stores/useWalletStore' + +export const REALMS: RealmInfo[] = [ + { + symbol: 'MNGO', + programId: new PublicKey('GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J'), + realmId: new PublicKey('DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE'), + }, +] + +export default function useRealm(symbol: string) { + const { fetchAllRealms, fetchRealm } = useWalletStore((s) => s.actions) + const connected = useWalletStore((s) => s.connected) + const wallet = useWalletStore((s) => s.current) + const tokenAccounts = useWalletStore((s) => s.tokenAccounts) + const { + realm, + mint, + governances, + proposals, + tokenRecords, + votes, + } = useWalletStore((s) => s.selectedRealm) + + const realmInfo = useMemo(() => REALMS.find((r) => r.symbol === symbol), [ + symbol, + ]) + + useEffect(() => { + const fetch = async () => { + if (realmInfo) { + await fetchAllRealms(realmInfo.programId) + fetchRealm(realmInfo.programId, realmInfo.realmId) + } + } + fetch() + }, [realmInfo]) + + const realmTokenAccount = useMemo( + () => + realm && + tokenAccounts.find((a) => + a.account.mint.equals(realm.info.communityMint) + ), + [realm, tokenAccounts] + ) + + const ownTokenRecord = useMemo( + () => wallet?.connected && tokenRecords[wallet.publicKey.toBase58()], + [tokenRecords, wallet, connected] + ) + + return { + realm, + realmInfo, + mint, + governances, + proposals, + tokenRecords, + votes, + realmTokenAccount, + ownTokenRecord, + } +} diff --git a/pages/dao/[symbol].tsx b/pages/dao/[symbol].tsx index 847d1df..2604cee 100644 --- a/pages/dao/[symbol].tsx +++ b/pages/dao/[symbol].tsx @@ -1,19 +1,8 @@ -import { PublicKey } from '@solana/web3.js' import { useRouter } from 'next/router' import Link from 'next/link' -import { useMemo } from 'react' -import { useEffect } from 'react' -import { RealmInfo } from '../../@types/types' import useWalletStore from '../../stores/useWalletStore' import moment from 'moment' - -export const REALMS: RealmInfo[] = [ - { - symbol: 'MNGO', - programId: new PublicKey('GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J'), - realmId: new PublicKey('DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE'), - }, -] +import useRealm from '../../hooks/useRealm' export const ProposalStateLabels = { 0: 'Draft', @@ -31,68 +20,18 @@ const DAO = () => { const router = useRouter() const { symbol } = router.query - const { fetchRealm } = useWalletStore((s) => s.actions) - const connected = useWalletStore((s) => s.connected) const wallet = useWalletStore((s) => s.current) - const realms = useWalletStore((s) => s.realms) - const governances = useWalletStore((s) => s.governances) - const proposals = useWalletStore((s) => s.proposals) - const votes = useWalletStore((s) => s.votes) - const tokenAccounts = useWalletStore((s) => s.tokenAccounts) - const tokenRecords = useWalletStore((s) => s.tokenRecords) - - const realmInfo = useMemo(() => REALMS.find((r) => r.symbol === symbol), [ - symbol, - ]) - - useEffect(() => { - if (realmInfo) { - fetchRealm(realmInfo.programId, realmInfo.realmId) - } - }, [realmInfo]) - - const realm = useMemo(() => { - return realmInfo && realms[realmInfo.realmId.toBase58()] - }, [realmInfo, realms]) - - const realmGovernances = useMemo(() => { - return realmInfo - ? Object.fromEntries( - Object.entries(governances).filter(([_k, v]) => - v.info.realm.equals(realmInfo.realmId) - ) - ) - : {} - }, [realmInfo, governances]) - - const realmProposals = useMemo(() => { - return Object.fromEntries( - Object.entries(proposals) - .filter( - ([_k, v]) => - Object.keys(realmGovernances).includes( - v.info.governance.toBase58() - ) && v.info.votingAtSlot - ) - .sort( - (a, b) => - b[1].info.votingAt.toNumber() - a[1].info.votingAt.toNumber() - ) - ) - }, [realmGovernances, proposals]) - - const realmTokenAccount = useMemo( - () => - realm && - tokenAccounts.find((a) => - a.account.mint.equals(realm.info.communityMint) - ), - [realm, tokenAccounts] - ) + const { + governances, + proposals, + votes, + realmTokenAccount, + ownTokenRecord, + } = useRealm(symbol as string) + // DEBUG print remove console.log( - 'governance page tokenAccounts', - tokenAccounts, + 'governance page tokenAccount', realmTokenAccount && realmTokenAccount.publicKey.toBase58() ) @@ -101,14 +40,9 @@ const DAO = () => { wallet?.connected && wallet.publicKey.toBase58() ) - const realmTokenRecord = useMemo( - () => wallet?.connected && tokenRecords[wallet.publicKey.toBase58()], - [tokenRecords, wallet, connected] - ) - console.log( 'governance page tokenRecord', - wallet?.connected && realmTokenRecord + wallet?.connected && ownTokenRecord ) return ( @@ -123,32 +57,49 @@ const DAO = () => {

in Governance:{' '} - {realmTokenRecord - ? realmTokenRecord.info.governingTokenDepositAmount.toNumber() + {ownTokenRecord + ? ownTokenRecord.info.governingTokenDepositAmount.toNumber() : 'N/A'}

Proposals:

- {Object.entries(realmProposals).map(([k, v]) => ( -
- - -

{v.info.name}

-

{v.info.descriptionLink}

-

- {moment.unix(v.info.votingCompletedAt.toNumber()).fromNow()} -

-

{ProposalStateLabels[v.info.state]}

-

Votes {JSON.stringify(votes[k])}

-

- {`Yes Threshold: ${ - governances[v.info.governance.toBase58()]?.info.config - .voteThresholdPercentage.value - }%`} -

-
- -
- ))} + {Object.entries(proposals) + .filter(([_k, v]) => v.info.votingAt) + .sort( + (a, b) => + b[1].info.votingAt.toNumber() - a[1].info.votingAt.toNumber() + ) + .map(([k, v]) => ( +
+ + +

{v.info.name}

+

{v.info.descriptionLink}

+ + {v.info.votingCompletedAt ? ( +

+ Ended{' '} + {moment + .unix(v.info.votingCompletedAt.toNumber()) + .fromNow()} +

+ ) : ( +

+ Created{' '} + {moment.unix(v.info.votingAt.toNumber()).fromNow()} +

+ )} +

{ProposalStateLabels[v.info.state]}

+

Votes {JSON.stringify(votes[k])}

+

+ {`Yes Threshold: ${ + governances[v.info.governance.toBase58()]?.info.config + .voteThresholdPercentage.value + }%`} +

+
+ +
+ ))} ) diff --git a/stores/useWalletStore.tsx b/stores/useWalletStore.tsx index 175d99e..46fe8c4 100644 --- a/stores/useWalletStore.tsx +++ b/stores/useWalletStore.tsx @@ -44,11 +44,17 @@ interface WalletStore extends State { endpoint: string } current: WalletAdapter | undefined - governances: { [governance: string]: ParsedAccount } - proposals: { [proposal: string]: ParsedAccount } + realms: { [realm: string]: ParsedAccount } - tokenRecords: { [owner: string]: ParsedAccount } - votes: { [proposal: string]: { yes: number; no: number } } + selectedRealm: { + realm?: ParsedAccount + mint?: MintAccount + governances: { [governance: string]: ParsedAccount } + proposals: { [proposal: string]: ParsedAccount } + tokenRecords: { [owner: string]: ParsedAccount } + votes: { [proposal: string]: { yes: number; no: number } } + } + selectedProposal: any providerUrl: string tokenAccounts: ProgramAccount[] mints: { [pubkey: string]: MintAccount } @@ -56,6 +62,18 @@ interface WalletStore extends State { actions: any } +function mapKeys(xs: any, mapFn: (k: string) => any) { + return Object.keys(xs).map(mapFn) +} + +function mapEntries(xs: any, mapFn: (kv: any[]) => any[]) { + return Object.fromEntries(Object.entries(xs).map(mapFn)) +} + +function merge(...os) { + return Object.assign({}, ...os) +} + const useWalletStore = create((set, get) => ({ connected: false, connection: { @@ -65,14 +83,20 @@ const useWalletStore = create((set, get) => ({ endpoint: ENDPOINT.url, }, current: null, - governances: {}, - proposals: {}, realms: {}, - tokenRecords: {}, - votes: {}, + selectedRealm: { + realm: null, + mint: null, + governances: {}, + proposals: {}, + tokenRecords: {}, + votes: {}, + }, + selectedProposal: {}, providerUrl: DEFAULT_PROVIDER.url, tokenAccounts: [], mints: {}, + set: (fn) => set(produce(fn)), actions: { async fetchWalletTokenAccounts() { const connection = get().connection.current @@ -90,7 +114,7 @@ const useWalletStore = create((set, get) => ({ console.log( 'fetchWalletTokenAccounts', connected, - ownedTokenAccounts.map((t) => t.publicKey.toBase58()) + ownedTokenAccounts.map((t) => t.account.mint.toBase58()) ) set((state) => { @@ -102,11 +126,10 @@ const useWalletStore = create((set, get) => ({ }) } }, - async fetchRealm(programId: PublicKey, realmId: PublicKey) { + async fetchAllRealms(programId: PublicKey) { const connection = get().connection.current const endpoint = get().connection.endpoint - - console.log('fetchRealm', programId.toBase58(), realmId.toBase58()) + const set = get().set const realms = await getGovernanceAccounts( programId, @@ -114,95 +137,96 @@ const useWalletStore = create((set, get) => ({ Realm, getAccountTypes(Realm) ) - console.log('fetchRealm realms', realms[realmId.toBase58()]) set((s) => { - s.realms = Object.assign({}, s.realms, realms) + s.realms = realms }) - const realmMint = await getMint( - connection, - realms[realmId.toBase58()].info.communityMint + const mints = await Promise.all( + Object.values(realms).map((r) => + getMint(connection, r.info.communityMint) + ) ) + set((s) => { + s.mints = Object.fromEntries( + mints.map((m) => [m.publicKey.toBase58(), m.account]) + ) + }) + }, + async fetchRealm(programId: PublicKey, realmId: PublicKey) { + const endpoint = get().connection.endpoint + const realms = get().realms + const mints = get().mints + const realm = realms[realmId.toBase58()] + const realmMintPk = realm.info.communityMint + const realmMint = mints[realmMintPk.toBase58()] + const set = get().set + + console.log('fetchRealm', programId.toBase58(), realmId.toBase58()) + + const [governances, tokenRecords] = await Promise.all([ + getGovernanceAccounts( + programId, + endpoint, + Governance, + getAccountTypes(Governance), + [pubkeyFilter(1, realmId)] + ), + + getGovernanceAccounts( + programId, + endpoint, + TokenOwnerRecord, + getAccountTypes(TokenOwnerRecord), + [pubkeyFilter(1, realmId), pubkeyFilter(1 + 32, realmMintPk)] + ), + ]) + + const tokenRecordsByOwner = mapEntries(tokenRecords, ([_k, v]) => [ + v.info.governingTokenOwner.toBase58(), + v, + ]) + console.log('fetchRealm mint', realmMint) - - set( - (s) => - (s.mints = Object.assign( - {}, - s.mints, - Object.fromEntries([ - [realmMint.publicKey.toBase58(), realmMint.account], - ]) - )) - ) - - const tokenRecords = await getGovernanceAccounts( - programId, - endpoint, - TokenOwnerRecord, - getAccountTypes(TokenOwnerRecord), - [pubkeyFilter(1, realmId), pubkeyFilter(1 + 32, realmMint.publicKey)] - ) - const tokenRecordsByOwner = Object.fromEntries( - Object.entries(tokenRecords).map(([_k, v]) => [ - v.info.governingTokenOwner.toBase58(), - v, - ]) - ) - console.log('fetchRealm tokenOwners', tokenRecordsByOwner) - - set((s) => { - s.tokenRecords = Object.assign({}, s.tokenRecords, tokenRecordsByOwner) - }) - - const governances = await getGovernanceAccounts( - programId, - endpoint, - Governance, - getAccountTypes(Governance), - [pubkeyFilter(1, realmId)] - ) console.log('fetchRealm governances', governances) + console.log('fetchRealm tokenRecords', tokenRecordsByOwner) set((s) => { - s.governances = Object.assign({}, s.governances, governances) + s.selectedRealm.realm = realm + s.selectedRealm.mint = realmMint + s.selectedRealm.governances = governances + s.selectedRealm.tokenRecords = tokenRecordsByOwner }) - const governanceIds = Object.keys(governances).map( - (k) => new PublicKey(k) - ) - const proposalsByGovernance = await Promise.all( - governanceIds.map((governanceId) => { + mapKeys(governances, (g) => { return getGovernanceAccounts( programId, endpoint, Proposal, getAccountTypes(Proposal), - [pubkeyFilter(1, governanceId)] + [pubkeyFilter(1, new PublicKey(g))] ) }) ) - const proposals = Object.assign({}, ...proposalsByGovernance) + + const proposals = merge(...proposalsByGovernance) console.log('fetchRealm proposals', proposals) set((s) => { - s.proposals = Object.assign({}, s.proposals, proposals) + s.selectedRealm.proposals = proposals }) - const proposalIds = Object.keys(proposals).map((k) => new PublicKey(k)) - const votesByProposal = await Promise.all( - proposalIds.map(async (proposalId) => { + mapKeys(proposals, async (p) => { const voteRecords = await getGovernanceAccounts( programId, endpoint, VoteRecord, getAccountTypes(VoteRecord), - [pubkeyFilter(1, proposalId)] + [pubkeyFilter(1, new PublicKey(p))] ) const precision = Math.pow(10, 6) @@ -212,7 +236,7 @@ const useWalletStore = create((set, get) => ({ .filter((v) => !!v) .reduce((acc, cur) => acc.add(cur), new BN(0)) .mul(new BN(precision)) - .div(realmMint.account.supply) + .div(realmMint.supply) .toNumber() * (100 / precision) @@ -222,26 +246,23 @@ const useWalletStore = create((set, get) => ({ .filter((v) => !!v) .reduce((acc, cur) => acc.add(cur), new BN(0)) .mul(new BN(precision)) - .div(realmMint.account.supply) + .div(realmMint.supply) .toNumber() * (100 / precision) - return [proposalId.toBase58(), { yes, no }] + return [p, { yes, no }] }) ) + // TODO: cache votes of finished proposals permanently in local storage to avoid recounting + console.log('fetchRealm votes', votesByProposal) set((s) => { - s.votes = Object.assign( - {}, - s.votes, - Object.fromEntries(votesByProposal) - ) + s.selectedRealm.votes = Object.fromEntries(votesByProposal) }) }, }, - set: (fn) => set(produce(fn)), })) export default useWalletStore