diff --git a/packages/DApp/src/components/card/CardFeature.tsx b/packages/DApp/src/components/card/CardFeature.tsx index ef2abbb..1361a92 100644 --- a/packages/DApp/src/components/card/CardFeature.tsx +++ b/packages/DApp/src/components/card/CardFeature.tsx @@ -12,6 +12,7 @@ import { useEthers } from '@usedapp/core' import { VoteSubmitButton } from './VoteSubmitButton' import { VoteSendingBtn, VoteBtn } from '../Button' import { VotingRoom } from '../../models/smartContract' +import { useWakuFeature } from '../../providers/wakuFeature/provider' interface CardFeatureProps { community: CommunityDetail @@ -28,12 +29,11 @@ export const CardFeature = ({ community, heading, icon, sum, timeLeft, currentVo const [showFeatureModal, setShowFeatureModal] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false) const [showOngoingVote, setShowOngoingVote] = useState(false) - + const { featured } = useWakuFeature() const setNewModal = (val: boolean) => { setShowConfirmModal(val) setShowFeatureModal(false) } - return ( @@ -78,7 +78,10 @@ export const CardFeature = ({ community, heading, icon, sum, timeLeft, currentVo )} - setShowFeatureModal(true)}> + el[0] === community.publicKey)} + onClick={() => setShowFeatureModal(true)} + > Feature this community! ⭐️ diff --git a/packages/DApp/src/components/card/FeatureModal.tsx b/packages/DApp/src/components/card/FeatureModal.tsx index b884809..678d690 100644 --- a/packages/DApp/src/components/card/FeatureModal.tsx +++ b/packages/DApp/src/components/card/FeatureModal.tsx @@ -5,6 +5,7 @@ import { CardCommunity } from './CardCommunity' import { ButtonPrimary } from '../Button' import { VotePropose } from '../votes/VotePropose' import { ColumnFlexDiv } from '../../constants/styles' +import { useSendWakuFeature } from '../../hooks/useSendWakuFeature' interface FeatureModalProps { community: CommunityDetail @@ -14,6 +15,7 @@ interface FeatureModalProps { export function FeatureModal({ community, availableAmount, setShowConfirmModal }: FeatureModalProps) { const [proposingAmount, setProposingAmount] = useState(0) + const sendWaku = useSendWakuFeature() const disabled = proposingAmount === 0 return ( @@ -26,7 +28,13 @@ export function FeatureModal({ community, availableAmount, setShowConfirmModal } proposingAmount={proposingAmount} disabled={disabled} /> - setShowConfirmModal(true)}> + { + await sendWaku(proposingAmount, community.publicKey) + setShowConfirmModal(true) + }} + > Confirm vote to feature community diff --git a/packages/DApp/src/components/directory/DirectoryCard.tsx b/packages/DApp/src/components/directory/DirectoryCard.tsx index 446fcf5..e4f5675 100644 --- a/packages/DApp/src/components/directory/DirectoryCard.tsx +++ b/packages/DApp/src/components/directory/DirectoryCard.tsx @@ -48,9 +48,9 @@ export function DirectoryCard({ community }: DirectoryCardProps) { diff --git a/packages/DApp/src/components/directory/DirectoryCards.tsx b/packages/DApp/src/components/directory/DirectoryCards.tsx index ee3e1d1..ce37750 100644 --- a/packages/DApp/src/components/directory/DirectoryCards.tsx +++ b/packages/DApp/src/components/directory/DirectoryCards.tsx @@ -10,7 +10,6 @@ import { DirectoryCardSkeleton } from './DirectoryCardSkeleton' import { useDirectoryCommunities } from '../../hooks/useDirectoryCommunities' import { SearchEmpty } from '../SearchEmpty' import { DirectoryCard } from './DirectoryCard' - export function DirectoryCards() { const [filterKeyword, setFilterKeyword] = useState('') const [sortedBy, setSortedBy] = useState(DirectorySortingEnum.IncludedRecently) @@ -27,7 +26,7 @@ export function DirectoryCards() { /> - + {communities.map((community, idx) => { if (community) { diff --git a/packages/DApp/src/helpers/apiMock.ts b/packages/DApp/src/helpers/apiMock.ts index a8c88ee..26df0f6 100644 --- a/packages/DApp/src/helpers/apiMock.ts +++ b/packages/DApp/src/helpers/apiMock.ts @@ -64,16 +64,16 @@ export function getCommunitiesInDirectorySync( break case DirectorySortingEnum.MostVotes: sortFunction = (a: CommunityDetail, b: CommunityDetail) => { - if (!a.directoryInfo?.featureVotes) return 1 - if (!b.directoryInfo?.featureVotes) return -1 - return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? 1 : -1 + if (!a?.featureVotes) return 1 + if (!b?.featureVotes) return -1 + return a?.featureVotes < b?.featureVotes ? 1 : -1 } break case DirectorySortingEnum.LeastVotes: sortFunction = (a: CommunityDetail, b: CommunityDetail) => { - if (!a.directoryInfo?.featureVotes) return 1 - if (!b.directoryInfo?.featureVotes) return -1 - return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? -1 : 1 + if (!a?.featureVotes) return 1 + if (!b?.featureVotes) return -1 + return a?.featureVotes < b?.featureVotes ? -1 : 1 } break } diff --git a/packages/DApp/src/helpers/apiMockData.ts b/packages/DApp/src/helpers/apiMockData.ts index 495921d..53a2050 100644 --- a/packages/DApp/src/helpers/apiMockData.ts +++ b/packages/DApp/src/helpers/apiMockData.ts @@ -56,9 +56,9 @@ export const communities: Array = [ voteFor: BigNumber.from(16740235), voteAgainst: BigNumber.from(6740235), }, + featureVotes: BigNumber.from(62142321), directoryInfo: { additionDate: new Date(1622802882000), - featureVotes: BigNumber.from(62142321), }, }, { @@ -91,9 +91,9 @@ export const communities: Array = [ validForAddition: true, votingHistory: [], currentVoting: undefined, + featureVotes: BigNumber.from(5214321), directoryInfo: { additionDate: new Date(1622630082000), - featureVotes: BigNumber.from(5214321), }, }, { @@ -108,9 +108,9 @@ export const communities: Array = [ validForAddition: false, votingHistory: [], currentVoting: undefined, + featureVotes: BigNumber.from(314321), directoryInfo: { additionDate: new Date(1622543682000), - featureVotes: BigNumber.from(314321), }, }, { diff --git a/packages/DApp/src/helpers/communityFiltering.ts b/packages/DApp/src/helpers/communityFiltering.ts index 43ca732..27113f5 100644 --- a/packages/DApp/src/helpers/communityFiltering.ts +++ b/packages/DApp/src/helpers/communityFiltering.ts @@ -78,13 +78,13 @@ export function sortDirectoryFunction(sortedBy: DirectorySortingEnum) { if (!b.directoryInfo) return 1 return a?.directoryInfo?.additionDate < b?.directoryInfo?.additionDate ? 1 : -1 case DirectorySortingEnum.MostVotes: - if (!a.directoryInfo?.featureVotes) return 1 - if (!b.directoryInfo?.featureVotes) return -1 - return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? 1 : -1 + if (!a?.featureVotes) return 1 + if (!b?.featureVotes) return -1 + return a?.featureVotes < b?.featureVotes ? 1 : -1 case DirectorySortingEnum.LeastVotes: - if (!a.directoryInfo?.featureVotes) return 1 - if (!b.directoryInfo?.featureVotes) return -1 - return a?.directoryInfo?.featureVotes < b?.directoryInfo?.featureVotes ? -1 : 1 + if (!a?.featureVotes) return 1 + if (!b?.featureVotes) return -1 + return a?.featureVotes < b?.featureVotes ? -1 : 1 } } } diff --git a/packages/DApp/src/helpers/getWeek.ts b/packages/DApp/src/helpers/getWeek.ts new file mode 100644 index 0000000..91a086b --- /dev/null +++ b/packages/DApp/src/helpers/getWeek.ts @@ -0,0 +1,3 @@ +export function getWeek(date: Date) { + return Math.floor((date.getTime() - 259200000) / 604800000) +} diff --git a/packages/DApp/src/helpers/receiveWakuFeature.ts b/packages/DApp/src/helpers/receiveWakuFeature.ts new file mode 100644 index 0000000..48488fd --- /dev/null +++ b/packages/DApp/src/helpers/receiveWakuFeature.ts @@ -0,0 +1,47 @@ +import { receiveWakuFeatureMsg } from '../helpers/wakuMessage' +import { getWeek } from '../helpers/getWeek' +import { merge } from 'lodash' +import { BigNumber } from 'ethers' +import { Waku } from 'js-waku' + +function sumVotes(map: any) { + for (const [publicKey, community] of Object.entries(map) as any[]) { + map[publicKey]['sum'] = BigNumber.from(0) + for (const votes of Object.entries(community['votes'])) { + map[publicKey]['sum'] = map[publicKey]['sum'].add(votes[1]) + } + } +} + +function getTop(map: any, top: number) { + sumVotes(map) + return Object.entries(map) + .sort((a: any, b: any) => (a[1].sum > b[1].sum ? -1 : 1)) + .slice(0, top) +} + +export async function receiveWakuFeature(waku: Waku | undefined, topic: string) { + let messages = await receiveWakuFeatureMsg(waku, topic) + const wakuFeatured: any = {} + let top5: any[] = [] + if (messages && messages?.length > 0) { + messages = messages.sort((a, b) => (a.timestamp > b.timestamp ? 1 : -1)) + let prevWeek = getWeek(messages[0].timestamp) + messages.forEach((el: any) => { + if (prevWeek === getWeek(el.timestamp)) { + if (!top5.find((featuredComm) => featuredComm[0] === el.publicKey)) { + merge(wakuFeatured, { [el.publicKey]: { votes: { [el.voter]: el.sntAmount } } }) + } + } else { + top5 = getTop(wakuFeatured, 5) + top5.forEach((featuredComm) => { + wakuFeatured[featuredComm[0]].votes = {} + wakuFeatured[featuredComm[0]].sum = BigNumber.from(0) + }) + prevWeek = getWeek(el.timestamp) + } + }) + sumVotes(wakuFeatured) + } + return { wakuFeatured, top5 } +} diff --git a/packages/DApp/src/helpers/wakuMessage.ts b/packages/DApp/src/helpers/wakuMessage.ts index b9f31af..e71164b 100644 --- a/packages/DApp/src/helpers/wakuMessage.ts +++ b/packages/DApp/src/helpers/wakuMessage.ts @@ -45,6 +45,74 @@ export async function receiveWakuMessages(waku: Waku, topic: string, room: numbe return messages?.map((msg) => JSON.parse(msg.payloadAsUtf8)) } +export async function receiveWakuFeatureMsg(waku: Waku | undefined, topic: string) { + if (waku) { + const messages = await waku.store.queryHistory({ contentTopics: [topic] }) + if (messages) { + return messages.filter(verifyWakuFeatureMsg).map((msg) => { + const data = JSON.parse(msg.payloadAsUtf8) + return { ...data, timestamp: new Date(data.timestamp) } + }) + } + } + return [] +} + +export function verifyWakuFeatureMsg(msg: WakuMessage) { + const wakuTimestamp = msg.timestamp + const data = JSON.parse(msg.payloadAsUtf8) + const timestamp = new Date(data.timestamp) + const types = ['address', 'uint256', 'address', 'uint256'] + const message = [data.voter, data.sntAmount, data.publicKey, BigNumber.from(timestamp.getTime())] + + const verifiedAddress = utils.verifyMessage(packAndArrayify(types, message), data.sign) + + if (wakuTimestamp?.getTime() != timestamp.getTime() || verifiedAddress != data.voter) { + return false + } + return true +} + +export async function createWakuFeatureMsg( + account: string | null | undefined, + signer: JsonRpcSigner | undefined, + sntAmount: BigNumber, + publicKey: string, + contentTopic: string +) { + if (!account || !signer) { + return undefined + } + + const signerAddress = await signer?.getAddress() + if (signerAddress != account) { + return undefined + } + const timestamp = new Date() + + const types = ['address', 'uint256', 'address', 'uint256'] + const message = [account, sntAmount, publicKey, BigNumber.from(timestamp.getTime())] + const sign = await signer.signMessage(packAndArrayify(types, message)) + + if (sign) { + const msg = WakuMessage.fromUtf8String( + JSON.stringify({ + voter: account, + sntAmount, + publicKey, + timestamp, + sign, + }), + { + contentTopic, + timestamp, + } + ) + return msg + } + return undefined +} + export async function createWakuMessage( account: string | null | undefined, signer: JsonRpcSigner | undefined, diff --git a/packages/DApp/src/hooks/useCommunities.ts b/packages/DApp/src/hooks/useCommunities.ts index 286e6a7..f8ba73f 100644 --- a/packages/DApp/src/hooks/useCommunities.ts +++ b/packages/DApp/src/hooks/useCommunities.ts @@ -1,17 +1,28 @@ import { getCommunityDetails } from '../helpers/apiMock' import { useCommunitiesProvider } from '../providers/communities/provider' - +import { useWakuFeature } from '../providers/wakuFeature/provider' +import { BigNumber } from 'ethers' export function useCommunities(publicKeys: string[]) { const { communitiesDetails, dispatch } = useCommunitiesProvider() + const { featureVotes } = useWakuFeature() return publicKeys.map((publicKey) => { const detail = communitiesDetails[publicKey] if (detail) { - return { ...detail } + if (featureVotes[publicKey]) { + return { ...detail, featureVotes: featureVotes[publicKey].sum } + } else { + return { ...detail, featureVotes: BigNumber.from(0) } + } } else { if (publicKey) { const setCommunity = async () => { - const communityDetail = await getCommunityDetails(publicKey) + let communityDetail = await getCommunityDetails(publicKey) + if (featureVotes[publicKey]) { + communityDetail = { ...communityDetail, featureVotes: featureVotes[publicKey].sum } + } else { + communityDetail = { ...communityDetail, featureVotes: BigNumber.from(0) } + } if (communityDetail) { dispatch(communityDetail) } diff --git a/packages/DApp/src/hooks/useSendWakuFeature.ts b/packages/DApp/src/hooks/useSendWakuFeature.ts new file mode 100644 index 0000000..8544ef4 --- /dev/null +++ b/packages/DApp/src/hooks/useSendWakuFeature.ts @@ -0,0 +1,34 @@ +import { useCallback } from 'react' +import { useWaku } from '../providers/waku/provider' +import { useEthers } from '@usedapp/core' +import { useConfig } from '../providers/config' +import { createWakuFeatureMsg } from '../helpers/wakuMessage' +import { BigNumber } from 'ethers' + +export function useSendWakuFeature() { + const { waku } = useWaku() + const { account, library } = useEthers() + const { config } = useConfig() + + const sendWakuFeature = useCallback( + async (voteAmount: number, publicKey: string) => { + const msg = await createWakuFeatureMsg( + account, + library?.getSigner(), + BigNumber.from(voteAmount), + publicKey, + config.wakuFeatureTopic + ) + if (msg) { + if (waku) { + await waku.relay.send(msg) + } else { + alert('error sending vote please try again') + } + } + }, + [waku, library, account] + ) + + return sendWakuFeature +} diff --git a/packages/DApp/src/index.tsx b/packages/DApp/src/index.tsx index 69944ed..43b8fd1 100644 --- a/packages/DApp/src/index.tsx +++ b/packages/DApp/src/index.tsx @@ -6,6 +6,7 @@ import { DEFAULT_CONFIG } from '@usedapp/core/dist/cjs/src/model/config/default' import { ConfigProvider } from './providers/config/provider' import { WakuProvider } from './providers/waku/provider' import { CommunitiesProvider } from './providers/communities/provider' +import { WakuFeatureProvider } from './providers/wakuFeature/provider' const config = { readOnlyChainId: ChainId.Ropsten, @@ -29,7 +30,9 @@ render( - + + + diff --git a/packages/DApp/src/models/community.ts b/packages/DApp/src/models/community.ts index df8bdfc..80da05f 100644 --- a/packages/DApp/src/models/community.ts +++ b/packages/DApp/src/models/community.ts @@ -29,10 +29,10 @@ export type CommunityDetail = { }[] | [] currentVoting: CurrentVoting | undefined + featureVotes?: BigNumber // number of votes for featuring community undefined if community can't be voted on directoryInfo?: { // if community is in directory this object describes additional directory info additionDate: Date // date of addition to directory - featureVotes?: BigNumber // number of votes for featuring community undefined if community can't be voted on untilNextFeature?: number // number of seconds until community can be featured again } } diff --git a/packages/DApp/src/providers/config/config.ts b/packages/DApp/src/providers/config/config.ts index 7aea66a..0596cae 100644 --- a/packages/DApp/src/providers/config/config.ts +++ b/packages/DApp/src/providers/config/config.ts @@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid' export interface Config { numberPerPage: number wakuTopic: string + wakuFeatureTopic: string contracts: { [chainID: number]: { [name: string]: string @@ -28,16 +29,19 @@ const contracts = { export const config: EnvConfigs = { localhost: { wakuTopic: `/myApp/localhost/${uuidv4()}/0.0.5/votingRoom/`, + wakuFeatureTopic: `/myApp/localhost/${uuidv4()}/0.0.5/feature/`, numberPerPage: 2, contracts, }, development: { wakuTopic: '/myApp/development/0.0.5/votingRoom/', + wakuFeatureTopic: `/myApp/development/0.0.5/feature/`, numberPerPage: 3, contracts, }, production: { wakuTopic: '/myApp/production/0.0.5/votingRoom/', + wakuFeatureTopic: `/myApp/production/0.0.5/feature/`, numberPerPage: 4, contracts, }, diff --git a/packages/DApp/src/providers/wakuFeature/provider.tsx b/packages/DApp/src/providers/wakuFeature/provider.tsx new file mode 100644 index 0000000..746ae4b --- /dev/null +++ b/packages/DApp/src/providers/wakuFeature/provider.tsx @@ -0,0 +1,39 @@ +import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react' +import { receiveWakuFeature } from '../../helpers/receiveWakuFeature' +import { useConfig } from '../config' +import { useWaku } from '../waku/provider' + +const WakuFeatureContext = createContext<{ + featureVotes: any + featured: any[] +}>({ + featureVotes: {}, + featured: [], +}) + +export function useWakuFeature() { + return useContext(WakuFeatureContext) +} + +interface WakuFeatureProviderProps { + children: ReactNode +} + +export function WakuFeatureProvider({ children }: WakuFeatureProviderProps) { + const [featureVotes, setFeatureVotes] = useState({}) + const [featured, setFeatured] = useState([]) + const { waku } = useWaku() + const { config } = useConfig() + + useEffect(() => { + const get = async () => { + const { wakuFeatured, top5 } = await receiveWakuFeature(waku, config.wakuFeatureTopic) + setFeatureVotes(wakuFeatured) + setFeatured(top5) + } + get() + const task = setInterval(get, 10000) + return () => clearInterval(task) + }, [waku?.libp2p?.peerId?.toString()]) + return +}