Skip to content

Commit

Permalink
Feat: locking, unlocking and withdrawal with walletconnect (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
schmanu authored Mar 27, 2024
1 parent dbc3629 commit 3d48a37
Show file tree
Hide file tree
Showing 18 changed files with 147 additions and 160 deletions.
9 changes: 9 additions & 0 deletions pages/connect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { NextPage } from 'next'

import { ConnectWallet } from '@/components/ConnectWallet'

const ConnectWalletPage: NextPage = () => {
return <ConnectWallet />
}

export default ConnectWalletPage
1 change: 1 addition & 0 deletions src/components/BackgroundCircles/styles.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.circles {
z-index: -1;
position: fixed;
height: 100%;
width: 100%;
Expand Down
46 changes: 27 additions & 19 deletions src/components/ConnectWallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { OverviewLinks } from '@/components/OverviewLinks'
import { useChainId } from '@/hooks/useChainId'
import { getConnectedWallet } from '@/hooks/useWallet'
import SafeLogo from '@/public/images/safe-logo.svg'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'
import PaperContainer from '../PaperContainer'

export const ConnectWallet = (): ReactElement => {
const onboard = useOnboard()
const chainId = useChainId()
const router = useRouter()

const onClick = async () => {
if (!onboard) {
Expand All @@ -28,36 +32,40 @@ export const ConnectWallet = (): ReactElement => {
if (isWrongChain) {
await onboard.setChain({ wallet: wallet.label, chainId: hexValue(parseInt(chainId)) })
}

router.push(AppRoutes.index)
} catch {
return
}
}

return (
<Grid container flexDirection="column" alignItems="center" px={1} py={6}>
<SafeLogo alt="Safe{DAO} logo" width={125} height={110} />
<PaperContainer>
<Grid container flexDirection="column" alignItems="center" px={1} py={6}>
<SafeLogo alt="Safe{DAO} logo" width={125} height={110} />

<Typography variant="h1" m={5} mb={6} textAlign="center">
Welcome to the next generation of digital ownership
</Typography>
<Typography variant="h1" m={5} mb={6} textAlign="center">
Welcome to the next generation of digital ownership
</Typography>

<Grid item xs>
<KeyholeIcon />
</Grid>
<Grid item xs>
<KeyholeIcon />
</Grid>

<Typography color="text.secondary" my={3} mx={18} textAlign="center">
Connect your wallet to view your SAFE balance and delegate voting power
</Typography>
<Typography color="text.secondary" my={3} mx={18} textAlign="center">
Connect your wallet to view your SAFE balance and delegate voting power
</Typography>

<Grid item xs={4} mb={4}>
<Button variant="contained" color="primary" size="stretched" onClick={onClick}>
Connect wallet
</Button>
</Grid>
<Grid item xs={4} mb={4}>
<Button variant="contained" color="primary" size="stretched" onClick={onClick}>
Connect wallet
</Button>
</Grid>

<Grid item xs={12}>
<OverviewLinks />
<Grid item xs={12}>
<OverviewLinks />
</Grid>
</Grid>
</Grid>
</PaperContainer>
)
}
15 changes: 13 additions & 2 deletions src/components/PageLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import Head from 'next/head'
import { Box } from '@mui/material'
import type { ReactElement, ReactNode } from 'react'
import { ReactElement, ReactNode, useEffect } from 'react'

import manifestJson from '@/public/manifest.json'
import { BackgroundCircles } from '@/components/BackgroundCircles'
import { Header } from '@/components/Header'

import css from './styles.module.css'
import NavTabs from '../NavTabs'
import { AppRoutes, RoutesWithNavigation } from '@/config/routes'
import { AppRoutes, RoutesWithNavigation, RoutesRequiringWallet } from '@/config/routes'
import { useRouter } from 'next/router'
import { useWallet } from '@/hooks/useWallet'
import { useIsSafeApp } from '@/hooks/useIsSafeApp'

export const PageLayout = ({ children }: { children: ReactNode }): ReactElement => {
const router = useRouter()
const showNavigation = RoutesWithNavigation.includes(router.route)

const wallet = useWallet()
const isSafeApp = useIsSafeApp()

useEffect(() => {
if (!wallet && !isSafeApp && RoutesRequiringWallet.includes(router.route)) {
router.push(AppRoutes.connect)
}
}, [isSafeApp, router, wallet])

return (
<>
<Head>
Expand Down
16 changes: 6 additions & 10 deletions src/components/TockenUnlocking/UnlockTokenWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { createUnlockTx, LockHistory } from '@/utils/lock'
import { useState, useMemo, ChangeEvent, useCallback } from 'react'
import { BigNumberish } from 'ethers'
import { useChainId } from '@/hooks/useChainId'
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk'
import { getCurrentDays } from '@/utils/date'
import { CHAIN_START_TIMESTAMPS } from '@/config/constants'
import { BoostBreakdown } from '../TokenLocking/BoostBreakdown'
import { SEASON2_START } from '../TokenLocking/BoostGraph/graphConstants'
import MilesReceipt from '@/components/TokenLocking/MilesReceipt'
import { useTxSender } from '@/hooks/useTxSender'

export const UnlockTokenWidget = ({
lockHistory,
Expand All @@ -30,7 +30,7 @@ export const UnlockTokenWidget = ({
const [isUnlocking, setIsUnlocking] = useState(false)

const chainId = useChainId()
const { sdk } = useSafeAppsSDK()
const txSender = useTxSender()

const todayInDays = getCurrentDays(CHAIN_START_TIMESTAMPS[chainId])

Expand Down Expand Up @@ -68,14 +68,16 @@ export const UnlockTokenWidget = ({
const unlockTx = createUnlockTx(chainId, parseUnits(unlockAmount, 18))

try {
await sdk.txs.send({ txs: [unlockTx] })
await txSender?.sendTxs([unlockTx])
setReceiptOpen(true)
} catch (err) {
console.error(err)
}
setIsUnlocking(false)
}

const isDisabled = !txSender || Boolean(unlockAmountError) || isUnlocking || cleanedAmount === '0'

return (
<Stack
spacing={3}
Expand Down Expand Up @@ -117,13 +119,7 @@ export const UnlockTokenWidget = ({
</Grid>

<Grid item xs={4}>
<Button
onClick={onUnlock}
variant="contained"
fullWidth
disableElevation
disabled={Boolean(unlockAmountError) || isUnlocking || cleanedAmount === '0'}
>
<Button onClick={onUnlock} variant="contained" fullWidth disableElevation disabled={isDisabled}>
{isUnlocking ? <CircularProgress size={20} /> : 'Unlock'}
</Button>
</Grid>
Expand Down
8 changes: 6 additions & 2 deletions src/components/TockenUnlocking/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ import SafeToken from '@/public/images/token.svg'
import { useMemo, useState } from 'react'
import { CHAIN_START_TIMESTAMPS } from '@/config/constants'
import { useSummarizedLockHistory } from '@/hooks/useSummarizedLockHistory'
import { useTxSender } from '@/hooks/useTxSender'

const TokenUnlocking = () => {
const { sdk } = useSafeAppsSDK()
const chainId = useChainId()
const startTime = CHAIN_START_TIMESTAMPS[chainId]
const lockHistory = useLockHistory()
const txSender = useTxSender()

const isTransactionPossible = !!txSender

const relativeLockHistory = useMemo(() => toRelativeLockHistory(lockHistory, startTime), [lockHistory, startTime])

Expand All @@ -36,7 +40,7 @@ const TokenUnlocking = () => {
setIsWithdrawing(true)
const withdrawTx = createWithdrawTx(chainId)
try {
await sdk.txs.send({ txs: [withdrawTx] })
await txSender?.sendTxs([withdrawTx])
} catch (error) {
console.error(error)
}
Expand Down Expand Up @@ -98,7 +102,7 @@ const TokenUnlocking = () => {
variant="contained"
color="primary"
onClick={onWithdraw}
disabled={totalWithdrawable.eq(0) || isWithdrawing}
disabled={totalWithdrawable.eq(0) || isWithdrawing || !isTransactionPossible}
sx={{ ml: 'auto !important' }}
>
{isWithdrawing ? <CircularProgress size={20} /> : 'Withdraw'}
Expand Down
17 changes: 3 additions & 14 deletions src/components/TokenLocking/ActionNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import { AppRoutes } from '@/config/routes'
import { useChainId } from '@/hooks/useChainId'
import { useWallet } from '@/hooks/useWallet'
import { getGovernanceAppSafeAppUrl } from '@/utils/safe-apps'
import { isSafe } from '@/utils/wallet'
import { ChevronRight } from '@mui/icons-material'
import { Grid, Typography, Stack, Button, Box } from '@mui/material'

import { Typography, Stack, Button, Box } from '@mui/material'
import { useRouter } from 'next/router'

import css from './styles.module.css'

export const ActionNavigation = () => {
const router = useRouter()
const wallet = useWallet()
const chainId = useChainId()

const onNavigate = (route: (typeof AppRoutes)[keyof typeof AppRoutes]) => async () => {
// Safe is connected via WC
if (wallet && (await isSafe(wallet))) {
window.open(getGovernanceAppSafeAppUrl(chainId, wallet.address), '_blank')?.focus()
} else {
router.push(route)
}
router.push(route)
}

const onUnlockAndWithdraw = onNavigate(AppRoutes.unlock)
Expand Down
42 changes: 28 additions & 14 deletions src/components/TokenLocking/LockTokenWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,28 @@ import { BoostGraph } from './BoostGraph/BoostGraph'
import css from './styles.module.css'
import { createLockTx, toRelativeLockHistory } from '@/utils/lock'
import { createApproveTx } from '@/utils/safe-token'
import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk'
import { useState, ChangeEvent, useMemo, useCallback } from 'react'
import { BigNumberish } from 'ethers'
import { BigNumber, BigNumberish } from 'ethers'
import { useChainId } from '@/hooks/useChainId'
import { floorNumber, getBoostFunction } from '@/utils/boost'
import { useLockHistory } from '@/hooks/useLockHistory'
import { useDebounce } from '@/hooks/useDebounce'
import { SEASON2_START } from './BoostGraph/graphConstants'
import { CHAIN_START_TIMESTAMPS } from '@/config/constants'
import { CHAIN_START_TIMESTAMPS, UNLIMITED_APPROVAL_AMOUNT } from '@/config/constants'
import { getCurrentDays } from '@/utils/date'
import { BoostBreakdown } from './BoostBreakdown'
import MilesReceipt from '@/components/TokenLocking/MilesReceipt'
import { useTxSender } from '@/hooks/useTxSender'
import { useSafeTokenLockingAllowance } from '@/hooks/useSafeTokenBalance'
import type { BaseTransaction } from '@gnosis.pm/safe-apps-sdk/dist/src/types/sdk.d'

export const LockTokenWidget = ({ safeBalance }: { safeBalance: BigNumberish | undefined }) => {
const [receiptOpen, setReceiptOpen] = useState<boolean>(false)
const { sdk } = useSafeAppsSDK()
const chainId = useChainId()
const txSender = useTxSender()
const startTime = CHAIN_START_TIMESTAMPS[chainId]
const todayInDays = getCurrentDays(startTime)
const { data: safeTokenAllowance, isLoading: isAllowanceLoading } = useSafeTokenLockingAllowance()

const pastLocks = useLockHistory()

Expand Down Expand Up @@ -78,18 +81,35 @@ export const LockTokenWidget = ({ safeBalance }: { safeBalance: BigNumberish | u
}, [safeBalance])

const onLockTokens = async () => {
if (!txSender) {
throw new Error('Cannot lock tokens without connected wallet')
}
setIsLocking(true)
const approveTx = createApproveTx(chainId, parseUnits(amount, 18))
const lockTx = createLockTx(chainId, parseUnits(amount, 18))
let txs: BaseTransaction[] = []
if (BigNumber.from(safeTokenAllowance).lt(amount)) {
// Approval is too low for the locking operation
const approvalAmount = txSender?.isBatchingSupported ? parseUnits(amount, 18) : UNLIMITED_APPROVAL_AMOUNT
txs.push(createApproveTx(chainId, approvalAmount))
}
txs.push(createLockTx(chainId, parseUnits(amount, 18)))
try {
await sdk.txs.send({ txs: [approveTx, lockTx] })
if (txSender?.isBatchingSupported) {
await txSender.sendTxs(txs)
} else {
for (let i = 0; i < txs.length; i++) {
await txSender?.sendTxs([txs[i]])
}
}

setReceiptOpen(true)
} catch (error) {
console.error(error)
}
setIsLocking(false)
}

const isDisabled = isAllowanceLoading || Boolean(amountError) || isLocking || cleanedAmount === '0'

return (
<>
<Box display="flex" flexDirection="row" alignItems="center" gap={2}>
Expand Down Expand Up @@ -141,13 +161,7 @@ export const LockTokenWidget = ({ safeBalance }: { safeBalance: BigNumberish | u
</Grid>

<Grid item xs={4}>
<Button
onClick={onLockTokens}
variant="contained"
fullWidth
disableElevation
disabled={Boolean(amountError) || isLocking || cleanedAmount === '0'}
>
<Button onClick={onLockTokens} variant="contained" fullWidth disableElevation disabled={isDisabled}>
{isLocking ? <CircularProgress size={20} /> : 'Lock'}
</Button>
</Grid>
Expand Down
12 changes: 2 additions & 10 deletions src/components/WalletIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,14 @@ import trezorIcon from '@web3-onboard/trezor/dist/icon'
import ledgerIcon from '@web3-onboard/ledger/dist/icon'
import tahoIcon from '@web3-onboard/taho/dist/icon'

import { INJECTED_WALLET_KEYS, WALLET_KEYS } from '@/utils/onboard'
import { WALLET_KEYS } from '@/utils/onboard'

type Props = {
[k in keyof (typeof WALLET_KEYS & typeof INJECTED_WALLET_KEYS)]: string
[k in keyof typeof WALLET_KEYS]: string
}

const WALLET_ICONS: Props = {
[INJECTED_WALLET_KEYS.METAMASK]: metamaskIcon,
[WALLET_KEYS.COINBASE]: coinbaseIcon,
[WALLET_KEYS.INJECTED]: metamaskIcon,
[WALLET_KEYS.KEYSTONE]: keystoneIcon,
[WALLET_KEYS.WALLETCONNECT]: walletConnectIcon,
[WALLET_KEYS.WALLETCONNECT_V2]: walletConnectIcon,
[WALLET_KEYS.TREZOR]: trezorIcon,
[WALLET_KEYS.LEDGER]: ledgerIcon,
[WALLET_KEYS.TAHO]: tahoIcon,
}

export const WalletIcon = ({ provider }: { provider: string }) => {
Expand Down
4 changes: 4 additions & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BigNumber } from 'ethers'

// General
export const IS_PRODUCTION = process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true'
export const INFURA_TOKEN = process.env.NEXT_PUBLIC_INFURA_TOKEN || ''
Expand Down Expand Up @@ -96,3 +98,5 @@ export const SEP5_PROPOSAL_URL =
'https://snapshot.org/#/safe.eth/proposal/0xb4765551b4814b592d02ce67de05527ac1d2b88a8c814c4346ecc0c947c9b941'

export const DISCORD_URL = 'https://chat.safe.global'

export const UNLIMITED_APPROVAL_AMOUNT = BigNumber.from(2).pow(256).sub(1)
3 changes: 3 additions & 0 deletions src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const AppRoutes = {
activity: '/activity',
unlock: '/unlock',
terms: '/terms',
connect: '/connect',
}

export const RoutesWithNavigation = [AppRoutes.activity, AppRoutes.index]

export const RoutesRequiringWallet = [AppRoutes.activity, AppRoutes.claim, AppRoutes.unlock, AppRoutes.index]
2 changes: 1 addition & 1 deletion src/hooks/useIsSafeApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState } from 'react'
import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect'

export const useIsSafeApp = (): boolean => {
const [isSafeApp, setIsSafeApp] = useState(false)
const [isSafeApp, setIsSafeApp] = useState(true)

useIsomorphicLayoutEffect(() => {
const isIframe = typeof window !== 'undefined' && window.self !== window.top
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useSafeTokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const useSafeTokenLockingAllowance = () => {
const chainId = useChainId()
const address = useAddress()

return useSWR(web3 ? QUERY_KEY : null, () => {
return useSWR(web3 && address ? QUERY_KEY : null, () => {
if (!address || !web3) {
return '0'
}
Expand Down
Loading

0 comments on commit 3d48a37

Please sign in to comment.