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

Feat: locking, unlocking and withdrawal with walletconnect #82

Merged
merged 4 commits into from
Mar 27, 2024
Merged
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
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
Loading