Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F/csp voting #27

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/chakra-components/src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const useClientProvider = ({ env: e, client: c, signer: s }: ClientProvid

const opts: ClientOptions = {
env: env as EnvOptions,
// TODO: REMOVE THE FOLLOWING csp_url when https://github.com/vocdoni/vocdoni-sdk/issues/163 is ready
csp_url: 'http://localhost:5000/v1',
elboletaire marked this conversation as resolved.
Show resolved Hide resolved
}

if (signer) {
Expand Down Expand Up @@ -147,12 +149,29 @@ export const useClientProvider = ({ env: e, client: c, signer: s }: ClientProvid
client.wallet = signer
}

const generateSigner = (seed?: string | string[]) => {
if (!client) {
throw new Error('No client initialized')
}

let signer: Wallet
if (!seed) {
client.generateRandomWallet()
signer = client.wallet as Wallet
} else {
signer = VocdoniSDKClient.generateWalletFromData(seed)
}

return signer
}

return {
account,
balance,
client,
env,
signer,
generateSigner,
fetchAccount,
fetchBalance,
setClient,
Expand Down
209 changes: 196 additions & 13 deletions packages/chakra-components/src/components/Election/Election.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChakraProps } from '@chakra-ui/system'
import { Signer } from '@ethersproject/abstract-signer'
import { Wallet } from '@ethersproject/wallet'
import { PublishedElection, Vote } from '@vocdoni/sdk'
import { CensusType, CspVote, ElectionStatus, PublishedElection, Vote } from '@vocdoni/sdk'
import { ComponentType, PropsWithChildren, createContext, useContext, useEffect, useState } from 'react'
import { FieldValues } from 'react-hook-form'
import { useClient } from '../../client'
Expand Down Expand Up @@ -43,6 +43,11 @@ export const useElectionProvider = ({
const [isAbleToVote, setIsAbleToVote] = useState<boolean | undefined>(undefined)
const [votesLeft, setVotesLeft] = useState<number>(0)
const [isInCensus, setIsInCensus] = useState<boolean>(false)
const [censusType, setCensusType] = useState<CensusType | undefined>(undefined)
const [voteInstance, setVoteInstance] = useState<Vote | undefined>(undefined)
const [cspVotingToken, setCspVotingToken] = useState<string | undefined>(undefined)
const [authToken, setAuthToken] = useState<any>(null)
const [handler, setHandler] = useState<string>('facebook') // Hardcoded until we let to choose

// set signer in case it has been specified in the election
// provider (rather than the client provider). Not sure if this is useful tho...
Expand All @@ -52,6 +57,12 @@ export const useElectionProvider = ({
setSigner(s)
}, [signer, client, s])

useEffect(() => {
if (cspVotingToken && voteInstance) {
cspVote(cspVotingToken, voteInstance)
}
}, [cspVotingToken, voteInstance])

// fetch election
useEffect(() => {
if (election || !id || loaded || !client) return
Expand Down Expand Up @@ -80,21 +91,65 @@ export const useElectionProvider = ({
useEffect(() => {
if (!fetchCensus || !signer || !election || !loaded || !client || isAbleToVote !== undefined) return
;(async () => {
const censusType: CensusType = election.census.type
const isIn = await client.isInCensus(election.id)
let left = 0
if (isIn) {
if (isIn || censusType == CensusType.WEIGHTED) {
// no need to check votes left if member ain't in census
left = await client.votesLeftCount(election.id)
setVotesLeft(left)

const voted = await client.hasAlreadyVoted(election.id)
setVoted(voted)
}
setCensusType(censusType)
setIsInCensus(isIn)
setIsAbleToVote(left > 0 && isIn)
setIsAbleToVote((left > 0 && isIn) || censusType == CensusType.CSP)
})()
}, [fetchCensus, election, loaded, client, isAbleToVote, signer])

// CSP OAuth flow
// Listening for the popup window meessage (oauth flows)
useEffect(() => {
;(async () => {
const handleMessage = (event: any) => {
if (event.data.code && event.data.handler) {
getOAuthToken(client, event.data.code, event.data.handler)
}
}

if (window.opener || !client || censusType !== CensusType.CSP) {
return
}

window.addEventListener('message', handleMessage)

return () => {
window.removeEventListener('message', handleMessage)
}
})()
}, [client, censusType])

// CSP OAuth flow
// Posting the message to the main window
useEffect(() => {
;(async () => {
if (typeof window == 'undefined') return
if (window.location.href.split('?').length < 2) return

const params: URLSearchParams = new URLSearchParams(window.location.search);
const code: string | null = params.get('code');
const handler: string | null = params.get('handler');
if (!code || !handler) return

if (window.opener) {
// If it is, send the code to the parent window and close the popup
window.opener.postMessage({ code, handler }, '*')
window.close()
}
})()
}, [])

// context vote function (the one to be used with the given components)
const vote = async (values: FieldValues) => {
if (!client) {
Expand All @@ -114,18 +169,21 @@ export const useElectionProvider = ({
// map questions back to expected Vote[] values
const mapped = election.questions.map((q, k) => parseInt(values[k.toString()], 10))

let vote = new Vote(mapped)
setVoteInstance(vote)
if (typeof beforeSubmit === 'function' && !beforeSubmit(vote)) {
return
}

try {
const vote = new Vote(mapped)
if (typeof beforeSubmit === 'function' && !beforeSubmit(vote)) {
return
if (censusType == CensusType.CSP) {
await cspAuthAndVote()
} else if (censusType == CensusType.WEIGHTED) {
const vid = await weightedVote(vote)
setVoted(vid)
setVotesLeft(votesLeft - 1)
setIsAbleToVote(isInCensus && votesLeft - 1 > 0)
}

const vid = await client.submitVote(vote)
setVoted(vid)
setVotesLeft(votesLeft - 1)
setIsAbleToVote(isInCensus && votesLeft - 1 > 0)

return vid
} catch (e: any) {
if ('reason' in e) {
return setError(e.reason as string)
Expand All @@ -139,6 +197,131 @@ export const useElectionProvider = ({
}
}

const weightedVote = async (vote: Vote): Promise<string> => {
if (!vote) {
throw new Error('no vote instance')
}
if (censusType != CensusType.WEIGHTED) {
throw new Error('not a Weighted election')
}

return await client.submitVote(vote)
}

// CSP OAuth flow
const cspAuthAndVote = async () => {
if (!client) {
throw new Error('no client initialized')
}
if (!election) {
throw new Error('no election initialized')
}
if (censusType != CensusType.CSP) {
throw new Error('not a CSP election')
}

const params: URLSearchParams = new URLSearchParams(window.location.search);

if (!params.has('electionId')) {
params.append('electionId', election.id);
}

if (!params.has('handler')) {
params.append('handler', handler);
}

const redirectURL: string = `${window.location.origin}${window.location.pathname}?${params.toString()}${window.location.hash}`;

let step0: any
try {
step0 = await client.cspStep(0, [handler, redirectURL])
} catch (e: any) {
if ('reason' in e) {
return setError(e.reason as string)
}
}

setAuthToken(step0.authToken)
openLoginPopup(handler, step0['response'][0])
}

// CSP OAuth flow
// Opens a popup window to the service login page
const openLoginPopup = (handler: string, url: string) => {
const width = 600
const height = 600
const left = window.outerWidth / 2 - width / 2
const top = window.outerHeight / 2 - height / 2
const params = [
`width=${width}`,
`height=${height}`,
`top=${top}`,
`left=${left}`,
`status=no`,
`resizable=yes`,
`scrollbars=yes`,
].join(',')

window.open(url, handler, params)
}

// CSP OAuth flow
const getOAuthToken = async (vocdoniClient: any, code: string, handler: string) => {
if (cspVotingToken) {
return
}

if (!code) {
throw new Error('no code provided')
}
if (!handler) {
throw new Error('no handler provided')
}

// Extract the electionId query param from the redirectURL
const existingParams = new URLSearchParams(window.location.search);
const electionId = existingParams.get('electionId');
const params: URLSearchParams = new URLSearchParams();
params.append('electionId', electionId as string);
params.append('handler', handler);

const redirectURL = `${window.location.origin}${window.location.pathname}?${params.toString()}${window.location.hash}`;

let step1
try {
step1 = await vocdoniClient.cspStep(1, [handler, code, redirectURL], authToken)
setCspVotingToken(step1.token)
} catch (e) {
setError('Not authorized to vote')
return false
}
}

const cspVote = async (token: string, vote: Vote) => {
if (!client) {
throw new Error('no client initialized')
}

if (censusType != CensusType.CSP) {
throw new Error('not a CSP election')
}

try {
const walletAddress: string = (await client.wallet?.getAddress()) as string
const signature: string = await client.cspSign(walletAddress, token)
const cspVote: CspVote = client.cspVote(vote as Vote, signature)
const vid: string = await client.submitVote(cspVote)
setVoted(vid)
setVotesLeft(votesLeft - 1)
setCspVotingToken(undefined)
setVoteInstance(undefined)
return vid
} catch (e) {
setError('Error submitting vote')
return false
}
}

return {
...rest,
election,
Expand Down