diff --git a/.eslintignore b/.eslintignore index b5e376f..ad16fac 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,3 +15,9 @@ dist-ssr *.local pnpm-workspace.yaml pnpm-lock.yaml + +# Ignore backend autogenerated files +backend/abis +backend/artifacts +backend/cache +backend/src diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index bd53a13..e130bf1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -43,6 +43,9 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install run: pnpm install --frozen-lockfile --ignore-scripts + # Required for FE build, since it depends on autogenerated files + - name: Build backend app + run: pnpm --filter @oasisprotocol/dapp-voting-backend build - name: Build frontend app run: pnpm --filter @oasisprotocol/dapp-voting-frontend build - name: Run tests for backend app diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index c8b91e2..4b2b004 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -52,6 +52,9 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install run: pnpm install --frozen-lockfile --ignore-scripts + # Required for FE build, since it depends on autogenerated files + - name: Build backend app + run: pnpm --filter @oasisprotocol/dapp-voting-backend build - name: Install gitlint run: | python -m pip install gitlint diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml index 439de62..872e9a0 100644 --- a/.github/workflows/frontend-deploy.yml +++ b/.github/workflows/frontend-deploy.yml @@ -52,6 +52,9 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install run: pnpm install --frozen-lockfile --ignore-scripts + # Required for FE build, since it depends on autogenerated files + - name: Build backend app + run: pnpm --filter @oasisprotocol/dapp-voting-backend build - name: Build frontend app run: pnpm --filter @oasisprotocol/dapp-voting-frontend build # https://github.com/actions/upload-pages-artifact#example-permissions-fix-for-linux diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..3f02b74 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,17 @@ +VITE_NETWORK=23295 +VITE_WEB3_GATEWAY=https://testnet.sapphire.oasis.dev + +# AllowAllACL tx 0x827ba96e652e4ca0776c279dd74585d6bad0988b34cd591d8dbb268c2677f0e6 +VITE_CONTRACT_ACL_ALLOWALL=0x8e29375FE5Db7eBb1b5eF24B7D397bBF0B01De09 + +# NativeBalanceACL tx 0x827ba96e652e4ca0776c279dd74585d6bad0988b34cd591d8dbb268c2677f0e6 +VITE_CONTRACT_ACL_NATIVEBALANCE=0x38FF18441b182ac603aADCA8ADDb257c6F5d103d + +# PollManager tx 0x8913a698a0686c273425e448a1a310a3a2be6fa2a68d431a2a3af8dae9233417 +VITE_CONTRACT_POLLMANAGER=0xdAB5845136b3102E63023BB2A2405cb71608605d + +# IPollManagerACL used by PollManager +VITE_CONTRACT_POLLMANAGER_ACL=0x8e29375FE5Db7eBb1b5eF24B7D397bBF0B01De09 + +# Proposal ID for poll pre-created poll +VITE_PROPOSAL_ID=0x2b4a9a515b37e6bc762a37db6b97a7c28d161c7ec1ba5ba7aae7924e5eeecf4c diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..372b9d8 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1,8 @@ +VITE_NETWORK=23294 +VITE_WEB3_GATEWAY=https://sapphire.oasis.io + +VITE_CONTRACT_ACL_ALLOWALL= +VITE_CONTRACT_ACL_NATIVEBALANCE= +VITE_CONTRACT_POLLMANAGER= +VITE_CONTRACT_POLLMANAGER_ACL= +VITE_PROPOSAL_ID= diff --git a/frontend/package.json b/frontend/package.json index cde4615..1af1d26 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "dependencies": { "@metamask/detect-provider": "^2.0.0", "@metamask/jazzicon": "^2.0.0", + "@oasisprotocol/dapp-voting-backend": "workspace:^", "@oasisprotocol/sapphire-paratime": "^1.3.2", "@phosphor-icons/core": "^2.0.8", "ethers": "^6.11.1", diff --git a/frontend/src/components/Alert/index.module.css b/frontend/src/components/Alert/index.module.css index e259113..103ae13 100644 --- a/frontend/src/components/Alert/index.module.css +++ b/frontend/src/components/Alert/index.module.css @@ -1,5 +1,4 @@ .alert { - min-height: 688px; text-align: center; h2 { diff --git a/frontend/src/components/Alert/index.tsx b/frontend/src/components/Alert/index.tsx index 97c1be6..c49a4ce 100644 --- a/frontend/src/components/Alert/index.tsx +++ b/frontend/src/components/Alert/index.tsx @@ -42,15 +42,16 @@ const alertTypeClassMap: Record = { interface Props extends PropsWithChildren { type: AlertType actions?: ReactElement + headerText?: string } -export const Alert: FC = ({ children, type, actions }) => { +export const Alert: FC = ({ children, type, actions, headerText }) => { const { header, icon } = alertTypeValuesMap[type] return (
-

{header}

+

{headerText ?? header}

{children}

{icon}
{actions}
diff --git a/frontend/src/components/Button/index.module.css b/frontend/src/components/Button/index.module.css index 0e928b9..fb2a898 100644 --- a/frontend/src/components/Button/index.module.css +++ b/frontend/src/components/Button/index.module.css @@ -61,6 +61,22 @@ } } +.buttonText { + &.buttonPrimary, + &.buttonSecondary { + background-color: transparent; + color: var(--old-silver); + border: transparent; + text-decoration: underline; + text-underline-offset: 0.5rem; + font-size: 16px; + font-weight: 400; + line-height: 137%; + letter-spacing: -0.03em; + padding: 0.5rem; + } +} + .buttonDisabled { cursor: not-allowed; background-color: var(--bright-gray); diff --git a/frontend/src/components/Button/index.tsx b/frontend/src/components/Button/index.tsx index b5d3f88..d8b6457 100644 --- a/frontend/src/components/Button/index.tsx +++ b/frontend/src/components/Button/index.tsx @@ -4,7 +4,7 @@ import { StringUtils } from '../../utils/string.utils.ts' type ButtonSize = 'small' | 'medium' type ButtonColor = 'primary' | 'secondary' -type ButtonVariant = 'solid' | 'outline' +type ButtonVariant = 'solid' | 'outline' | 'text' interface Props extends PropsWithChildren { disabled?: boolean @@ -30,6 +30,7 @@ const colorMap: Record = { const variantMap: Record = { solid: classes.buttonSolid, outline: classes.buttonOutline, + text: classes.buttonText, } export const Button: FC = ({ diff --git a/frontend/src/components/Card/index.module.css b/frontend/src/components/Card/index.module.css index 9308c27..ca4e047 100644 --- a/frontend/src/components/Card/index.module.css +++ b/frontend/src/components/Card/index.module.css @@ -4,6 +4,7 @@ margin: 0 auto; width: 100%; max-width: 876px; + min-height: 688px; padding: 2.25rem 4.6875rem 1.875rem; border-radius: 12px; background: var(--white); diff --git a/frontend/src/components/Layout/index.tsx b/frontend/src/components/Layout/index.tsx index 01601ba..69902bc 100644 --- a/frontend/src/components/Layout/index.tsx +++ b/frontend/src/components/Layout/index.tsx @@ -1,10 +1,47 @@ -import { FC } from 'react' -import { Outlet } from 'react-router-dom' +import { FC, useEffect, useState } from 'react' +import { Outlet, useNavigate } from 'react-router-dom' import classes from './index.module.css' import { LogoIcon } from '../icons/LogoIcon' import { ConnectWallet } from '../ConnectWallet' +import { useWeb3 } from '../../hooks/useWeb3.ts' +import { Alert } from '../Alert' export const Layout: FC = () => { + const { + state: { isVoidSignerConnected }, + getPoll, + } = useWeb3() + const navigate = useNavigate() + + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + if (!isVoidSignerConnected) return + + const init = async () => { + const { + active, + params: { closeTimestamp, numChoices }, + } = await getPoll() + + if (numChoices !== 3n) { + console.warn('[numChoices] Unexpected number of poll choices, this dApp may not behave as expected!') + } + + setIsLoading(false) + + if (!active) { + navigate('/results') + console.log('closeTimestamp', closeTimestamp) + } else { + navigate('/') + } + } + + init() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVoidSignerConnected]) + return (
@@ -16,7 +53,10 @@ export const Layout: FC = () => {

Oasis Mascot

- + {isLoading && ( + Fetching poll...} /> + )} + {!isLoading && }
diff --git a/frontend/src/constants/config.ts b/frontend/src/constants/config.ts index ba76923..ce2c02f 100644 --- a/frontend/src/constants/config.ts +++ b/frontend/src/constants/config.ts @@ -20,4 +20,58 @@ export const NETWORKS: Map = new Map([ ], ]) +export const MAX_GAS_LIMIT = 100000n + +interface PollChoice { + name: string + description: string + imagePath: string +} + +/** + * This array indexes correspond to the matching choiceId of the poll + * Desert Owl = 0 + * Capybara = 1 + * Fennec Fox = 2 + */ +export const POLL_CHOICES: readonly PollChoice[] = Object.freeze([ + { + name: 'Desert Owl', + description: 'Lorem ipsum dolor sit amet. A repellendus illo.', + imagePath: 'https://fakeimg.pl/182x175', + }, + { + name: 'Capybara', + description: 'Lorem ipsum dolor sit amet. A repellendus illo.', + imagePath: 'https://fakeimg.pl/182x175', + }, + { + name: 'Fennec Fox', + description: 'Lorem ipsum dolor sit amet. A repellendus illo.', + imagePath: 'https://fakeimg.pl/182x175', + }, +]) + export const METAMASK_HOME_PAGE = 'https://metamask.io/' + +const { + VITE_NETWORK: ENV_VITE_NETWORK, + VITE_WEB3_GATEWAY, + VITE_CONTRACT_ACL_ALLOWALL, + VITE_CONTRACT_ACL_NATIVEBALANCE, + VITE_CONTRACT_POLLMANAGER, + VITE_CONTRACT_POLLMANAGER_ACL, + VITE_PROPOSAL_ID, +} = import.meta.env + +const VITE_NETWORK = BigInt(ENV_VITE_NETWORK) ?? 0n + +export { + VITE_NETWORK, + VITE_WEB3_GATEWAY, + VITE_CONTRACT_ACL_ALLOWALL, + VITE_CONTRACT_ACL_NATIVEBALANCE, + VITE_CONTRACT_POLLMANAGER, + VITE_CONTRACT_POLLMANAGER_ACL, + VITE_PROPOSAL_ID, +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 5cc39bf..e32a675 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -11,6 +11,7 @@ --medium-blue: #0d00d2; --table-border-light: rgba(143, 142, 223, 0.39); --table-border-dark: rgba(36, 38, 41, 0.8); + --old-silver: #858486; } *, diff --git a/frontend/src/pages/HomePage/index.module.css b/frontend/src/pages/HomePage/index.module.css index 9be62cd..a9824cd 100644 --- a/frontend/src/pages/HomePage/index.module.css +++ b/frontend/src/pages/HomePage/index.module.css @@ -26,6 +26,16 @@ margin-bottom: 2rem; } +.voteBtnLabel { + cursor: pointer; + user-select: none; +} + +.voteBtnLabelDisabled { + cursor: not-allowed; + user-select: none; +} + .cardFooterText { font-size: 14px; font-weight: 500; @@ -34,3 +44,13 @@ text-align: center; color: var(--palatinate-blue); } + +.insufficientBalanceAlertActions { + display: flex; + flex-direction: column; + gap: 0.75rem; + + > * { + align-self: center; + } +} diff --git a/frontend/src/pages/HomePage/index.tsx b/frontend/src/pages/HomePage/index.tsx index e4a17d2..ee78208 100644 --- a/frontend/src/pages/HomePage/index.tsx +++ b/frontend/src/pages/HomePage/index.tsx @@ -1,98 +1,166 @@ -import { FC } from 'react' +import { FC, useState } from 'react' import { CaretRightIcon } from '../../components/icons/CaretRightIcon.tsx' import { Button } from '../../components/Button' import { Card } from '../../components/Card' import classes from './index.module.css' import { MascotCard } from '../../components/MascotCard' +import { POLL_CHOICES } from '../../constants/config.ts' +import { useWeb3 } from '../../hooks/useWeb3.ts' import { Alert } from '../../components/Alert' +import { StringUtils } from '../../utils/string.utils.ts' + +type MascotChoices = 0 | 1 | 2 export const HomePage: FC = () => { + const { + state: { isConnected }, + vote, + canVoteOnPoll, + } = useWeb3() + + const [selectedChoice, setSelectedChoice] = useState(null) + const [pageStatus, setPageStatus] = useState< + 'loading' | 'error' | 'success' | 'insufficient-balance' | 'vote' + >('vote') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState('') + + const handleSelectChoice = (choice: MascotChoices) => { + setSelectedChoice(choice) + } + + const handleVote = async () => { + if (selectedChoice === null) return + + setIsLoading(true) + + try { + const canVote = await canVoteOnPoll() + + if (!canVote) { + setPageStatus('insufficient-balance') + return + } + + setPageStatus('loading') + + await vote(selectedChoice) + + setPageStatus('success') + } catch (ex) { + console.error(ex) + setError((ex as Error).message ?? JSON.stringify(ex)) + + setPageStatus('error') + } finally { + setIsLoading(false) + } + } + + const resetPageState = () => { + setPageStatus('vote') + } + return ( -
- - Please note there is a 100 ROSE threshold in order to cast your vote. - -
-
- Submitting vote...}> - Once you confirm this vote you will not be able to cancel it. - -
-
- - Your vote has successfully submitted. -
- Thank you for your participation. - - } - /> -
-
- - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod Ipsum - -
-
- -

- Select your preferred mascot option. Once you confirm this vote you will not be able to cancel it. - Read more about the campaign on our website. -

-
- } - actions={ -
- -
- } - /> - } - actions={ -
- -
- } - /> - } - actions={ -
- -
- } - /> -
-
- -
-

+ <> + {pageStatus === 'loading' && ( + Submitting vote...}> + Once you confirm this vote you will not be able to cancel it. + + )} + {pageStatus === 'error' && error && ( + + Try again + + } + > + {error} + + )} + {pageStatus === 'success' && ( + + Your vote has successfully submitted. +
+ Thank you for your participation. + + } + /> + )} + {pageStatus === 'insufficient-balance' && ( + + + +

+ } + > Please note there is a 100 ROSE threshold in order to cast your vote. -
- Poll closes on March 31st, 2024 at 00:00 CET. -

- -
+ + )} + {pageStatus === null && ( + +

+ Select your preferred mascot option. Once you confirm this vote you will not be able to cancel it. + Read more about the campaign on our website. +

+
+ {POLL_CHOICES.map(({ name, description, imagePath }, choiceId) => ( + } + selected={choiceId === selectedChoice} + actions={ +
+ +
+ } + /> + ))} +
+
+ +
+

+ Please note there is a 100 ROSE threshold in order to cast your vote. +
+ Poll closes on March 31st, 2024 at 00:00 CET. +

+
+ )} + ) } diff --git a/frontend/src/providers/EIP1193Context.ts b/frontend/src/providers/EIP1193Context.ts index ff239fd..8443dfc 100644 --- a/frontend/src/providers/EIP1193Context.ts +++ b/frontend/src/providers/EIP1193Context.ts @@ -3,7 +3,7 @@ import { createContext } from 'react' export interface EIP1193ProviderContext { isEIP1193ProviderAvailable: () => Promise connectWallet: () => Promise - switchNetwork: (toChainId: number) => void + switchNetwork: (toChainId: bigint) => void } export const EIP1193Context = createContext({} as EIP1193ProviderContext) diff --git a/frontend/src/providers/EIP1193Provider.tsx b/frontend/src/providers/EIP1193Provider.tsx index 1ca1836..2c9b0c4 100644 --- a/frontend/src/providers/EIP1193Provider.tsx +++ b/frontend/src/providers/EIP1193Provider.tsx @@ -4,6 +4,7 @@ import * as sapphire from '@oasisprotocol/sapphire-paratime' import { EIP1193Error } from '../utils/errors' import detectEthereumProvider from '@metamask/detect-provider' import { EIP1193Context, EIP1193ProviderContext } from './EIP1193Context.ts' +import { VITE_NETWORK } from '../constants/config.ts' declare global { interface Window { @@ -32,8 +33,8 @@ export const EIP1193ContextProvider: FC = ({ children }) => { return accounts[0] } - const _addNetwork = (chainId = 0x5afe) => { - if (chainId === 0x5afe) { + const _addNetwork = (chainId = VITE_NETWORK) => { + if (chainId === 23294n) { return window.ethereum?.request?.({ method: 'wallet_addEthereumChain', params: [ @@ -55,7 +56,7 @@ export const EIP1193ContextProvider: FC = ({ children }) => { throw new Error('Unable to automatically add the network, please do it manually!') } - const switchNetwork = async (toChainId = 0x5afe) => { + const switchNetwork = async (toChainId = VITE_NETWORK) => { const ethProvider = new BrowserProvider(window.ethereum!) const sapphireEthProvider = sapphire.wrap(ethProvider) as BrowserProvider & sapphire.SapphireAnnex diff --git a/frontend/src/providers/Web3Context.ts b/frontend/src/providers/Web3Context.ts index 378d563..fd77bff 100644 --- a/frontend/src/providers/Web3Context.ts +++ b/frontend/src/providers/Web3Context.ts @@ -1,23 +1,42 @@ import { createContext } from 'react' import * as sapphire from '@oasisprotocol/sapphire-paratime' -import { BrowserProvider, TransactionResponse } from 'ethers' +import { BigNumberish, BrowserProvider, Signer, TransactionResponse } from 'ethers' +import { type PollManager } from '@oasisprotocol/dapp-voting-backend/src/contracts' +import { DefaultReturnType } from '@oasisprotocol/dapp-voting-backend/src/contracts/common.ts' export interface Web3ProviderState { isConnected: boolean + isVoidSignerConnected: boolean ethProvider: BrowserProvider | null sapphireEthProvider: (BrowserProvider & sapphire.SapphireAnnex) | null + signer: Signer | null account: string | null explorerBaseUrl: string | null networkName: string | null + pollManager: PollManager | null + pollManagerVoidSigner: PollManager | null } export interface Web3ProviderContext { readonly state: Web3ProviderState connectWallet: () => Promise - switchNetwork: (chainId?: number) => Promise + switchNetwork: (chainId?: bigint) => Promise getBalance: () => Promise getTransaction: (txHash: string) => Promise isProviderAvailable: () => Promise + getPoll: () => Promise< + DefaultReturnType< + [ + [boolean, bigint, PollManager.ProposalParamsStructOutput] & { + active: boolean + topChoice: bigint + params: PollManager.ProposalParamsStructOutput + }, + ] + > + > + canVoteOnPoll: () => Promise + vote: (choiceId: BigNumberish) => Promise } export const Web3Context = createContext({} as Web3ProviderContext) diff --git a/frontend/src/providers/Web3Provider.tsx b/frontend/src/providers/Web3Provider.tsx index b4d9dee..97214c3 100644 --- a/frontend/src/providers/Web3Provider.tsx +++ b/frontend/src/providers/Web3Provider.tsx @@ -1,18 +1,32 @@ -import { FC, PropsWithChildren, useCallback, useState } from 'react' +import { FC, PropsWithChildren, useCallback, useEffect, useState } from 'react' import * as sapphire from '@oasisprotocol/sapphire-paratime' -import { NETWORKS } from '../constants/config' +import { + MAX_GAS_LIMIT, + NETWORKS, + VITE_CONTRACT_POLLMANAGER, + VITE_NETWORK, + VITE_PROPOSAL_ID, + VITE_WEB3_GATEWAY, +} from '../constants/config' import { UnknownNetworkError } from '../utils/errors' import { Web3Context, Web3ProviderContext, Web3ProviderState } from './Web3Context' import { useEIP1193 } from '../hooks/useEIP1193.ts' -import { BrowserProvider } from 'ethers' +import { BigNumberish, BrowserProvider, JsonRpcProvider, toBeHex } from 'ethers' +import { PollManager__factory } from '@oasisprotocol/dapp-voting-backend/src/contracts' + +const EMPTY_IN_DATA = new Uint8Array([]) const web3ProviderInitialState: Web3ProviderState = { isConnected: false, + isVoidSignerConnected: false, ethProvider: null, sapphireEthProvider: null, + signer: null, account: null, explorerBaseUrl: null, networkName: null, + pollManager: null, + pollManagerVoidSigner: null, } export const Web3ContextProvider: FC = ({ children }) => { @@ -26,6 +40,29 @@ export const Web3ContextProvider: FC = ({ children }) => { ...web3ProviderInitialState, }) + useEffect(() => { + const initVoidSinger = async () => { + if (!VITE_WEB3_GATEWAY || !VITE_CONTRACT_POLLMANAGER) return + + const staticNetworkJsonRpcProvider = new JsonRpcProvider(VITE_WEB3_GATEWAY, undefined, { + staticNetwork: true, + }) + + const pollManagerWithoutSigner = await PollManager__factory.connect( + VITE_CONTRACT_POLLMANAGER, + staticNetworkJsonRpcProvider + ) + + setState(prevState => ({ + ...prevState, + pollManagerVoidSigner: pollManagerWithoutSigner, + isVoidSignerConnected: true, + })) + } + + initVoidSinger() + }, []) + const _connectionChanged = (isConnected: boolean) => { setState(prevState => ({ ...prevState, @@ -100,12 +137,17 @@ export const Web3ContextProvider: FC = ({ children }) => { const network = await sapphireEthProvider.getNetwork() _setNetworkSpecificVars(network.chainId, sapphireEthProvider) + const signer = await sapphireEthProvider.getSigner() + const pollManager = PollManager__factory.connect(VITE_CONTRACT_POLLMANAGER, signer) + setState(prevState => ({ ...prevState, isConnected: true, ethProvider, sapphireEthProvider, account, + signer, + pollManager, })) } catch (ex) { setState(prevState => ({ @@ -146,7 +188,7 @@ export const Web3ContextProvider: FC = ({ children }) => { _addEventListenersOnce(window.ethereum) } - const switchNetwork = async (chainId = 0x5afe) => { + const switchNetwork = async (chainId = VITE_NETWORK) => { return switchNetworkEIP1193(chainId) } @@ -161,12 +203,59 @@ export const Web3ContextProvider: FC = ({ children }) => { throw new Error('[sapphireEthProvider] not initialized!') } - const txReceipt = await sapphireEthProvider.waitForTransaction(txHash, 1, 60000) + const txReceipt = await sapphireEthProvider.waitForTransaction(txHash) if (txReceipt?.status === 0) throw new Error('Transaction failed') return await sapphireEthProvider.getTransaction(txHash) } + const getPoll = async () => { + const { pollManagerVoidSigner } = state + + if (!pollManagerVoidSigner) { + throw new Error('[pollManagerWithoutSigner] not initialized!') + } + + return await pollManagerVoidSigner.PROPOSALS(toBeHex(VITE_PROPOSAL_ID)) + } + + const canVoteOnPoll = async () => { + const { pollManagerVoidSigner, account } = state + + if (!pollManagerVoidSigner) { + throw new Error('[pollManagerVoidSigner] not initialized!') + } + + if (!account) { + throw new Error('[account] Wallet not connected!') + } + + return await pollManagerVoidSigner + .canVoteOnPoll(VITE_PROPOSAL_ID, account, EMPTY_IN_DATA) + .then(canVoteBigint => Promise.resolve(canVoteBigint === 1n)) + .catch(() => Promise.resolve(false)) + } + + const vote = async (choiceId: BigNumberish) => { + const { pollManager, signer } = state + + if (!pollManager) { + throw new Error('[pollManager] not initialized!') + } + + if (!signer) { + throw new Error('[signer] Signer not connected!') + } + + const unsignedTx = await pollManager.vote.populateTransaction(VITE_PROPOSAL_ID, choiceId, EMPTY_IN_DATA) + unsignedTx.gasLimit = MAX_GAS_LIMIT + unsignedTx.value = 0n + + const txResponse = await signer.sendTransaction(unsignedTx) + + return await getTransaction(txResponse.hash) + } + const providerState: Web3ProviderContext = { state, isProviderAvailable, @@ -174,6 +263,9 @@ export const Web3ContextProvider: FC = ({ children }) => { switchNetwork, getBalance, getTransaction, + getPoll, + canVoteOnPoll, + vote, } return {children} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 11f02fe..e5a9399 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1 +1,15 @@ /// + +interface ImportMetaEnv { + VITE_NETWORK: string + VITE_WEB3_GATEWAY: string + VITE_CONTRACT_ACL_ALLOWALL: string + VITE_CONTRACT_ACL_NATIVEBALANCE: string + VITE_CONTRACT_POLLMANAGER: string + VITE_CONTRACT_POLLMANAGER_ACL: string + VITE_PROPOSAL_ID: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70e5364..aa00348 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: '@metamask/jazzicon': specifier: ^2.0.0 version: 2.0.0 + '@oasisprotocol/dapp-voting-backend': + specifier: workspace:^ + version: link:../backend '@oasisprotocol/sapphire-paratime': specifier: ^1.3.2 version: 1.3.2