Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:safe-global/safe-wallet-web into bul…
Browse files Browse the repository at this point in the history
…k-execute
  • Loading branch information
katspaugh committed Sep 3, 2023
2 parents ec8a9f4 + 8042e8d commit 441ce4c
Show file tree
Hide file tree
Showing 47 changed files with 741 additions and 614 deletions.
1 change: 0 additions & 1 deletion .github/workflows/deploy-dockerhub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ on:
jobs:
dockerhub-push:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v3
- name: Dockerhub login
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM node:16-alpine
RUN apk add --no-cache libc6-compat git python3 py3-pip make g++
FROM node:18-alpine
RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers
WORKDIR /app
COPY . .

# install deps
RUN yarn install
RUN yarn install --frozen-lockfile

ENV NODE_ENV production

Expand Down
12 changes: 6 additions & 6 deletions cypress/e2e/smoke/address_book.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { format } from 'date-fns'

const NAME = 'Owner1'
const EDITED_NAME = 'Edited Owner1'
const ENS_NAME = 'diogo.eth'
const ENS_ADDRESS = '0x6a5602335a878ADDCa4BF63a050E34946B56B5bC'
const ADDRESS = '0x6a5602335a878ADDCa4BF63a050E34946B56B5bC'
const GOERLI_TEST_SAFE = 'gor:0x97d314157727D517A706B5D08507A1f9B44AaaE9'
const GNO_TEST_SAFE = 'gno:0xB8d760a90a5ed54D3c2b3EFC231277e99188642A'
const GOERLI_CSV_ENTRY = {
Expand All @@ -30,13 +29,14 @@ describe('Address book', () => {
// Add a new entry manually
cy.contains('Create entry').click()
cy.get('input[name="name"]').type(NAME)
cy.get('input[name="address"]').type(ENS_NAME)
// Name was translated
cy.get(ENS_NAME).should('not.exist')
cy.get('input[name="address"]').type(ADDRESS)

// Save the entry
cy.contains('button', 'Save').click()

// The new entry is visible
cy.contains(NAME).should('exist')
cy.contains(ENS_ADDRESS).should('exist')
cy.contains(ADDRESS).should('exist')
})

it('should save an edited entry name', () => {
Expand Down
2 changes: 2 additions & 0 deletions cypress/e2e/smoke/batch_tx.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ describe('Create batch transaction', () => {
before(() => {
cy.visit(`/home?safe=${SAFE}`)
cy.contains('Accept selection').click()

cy.contains(/E2E Wallet @ G(ö|oe)rli/, { timeout: 10000 })
})

it('Should open an empty batch list', () => {
Expand Down
1 change: 0 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ jest.mock('@web3-onboard/keystone/dist/index', () => jest.fn())
jest.mock('@web3-onboard/ledger/dist/index', () => jest.fn())
jest.mock('@web3-onboard/trezor', () => jest.fn())
jest.mock('@web3-onboard/walletconnect', () => jest.fn())
jest.mock('@web3-onboard/taho', () => jest.fn())

const mockOnboardState = {
chains: [],
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"serve": "npx -y serve out -p ${REVERSE_PROXY_UI_PORT:=8080}",
"static-serve": "yarn build && yarn serve"
},
"engines": {
"node": ">=16"
},
"pre-commit": [
"lint"
],
Expand Down Expand Up @@ -60,7 +63,6 @@
"@web3-onboard/injected-wallets": "^2.10.0",
"@web3-onboard/keystone": "^2.3.7",
"@web3-onboard/ledger": "2.3.2",
"@web3-onboard/taho": "^2.0.5",
"@web3-onboard/trezor": "^2.4.2",
"@web3-onboard/walletconnect": "^2.4.5",
"classnames": "^2.3.1",
Expand Down
55 changes: 34 additions & 21 deletions src/components/batch/BatchSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ConfirmBatchFlow from '@/components/tx-flow/flows/ConfirmBatch'
import Track from '@/components/common/Track'
import { BATCH_EVENTS } from '@/services/analytics'
import { BatchReorder } from './BatchTxList'
import CheckWallet from '@/components/common/CheckWallet'

import PlusIcon from '@/public/images/common/plus.svg'
import EmptyBatch from './EmptyBatch'
Expand Down Expand Up @@ -74,33 +75,45 @@ const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open:
<BatchReorder txItems={batchTxs} onDelete={deleteTx} onReorder={onReorder} />
</div>

<Track {...BATCH_EVENTS.BATCH_NEW_TX}>
<Button onClick={onAddClick}>
<SvgIcon component={PlusIcon} inheritViewBox fontSize="small" sx={{ mr: 1 }} />
Add new transaction
</Button>
</Track>
<CheckWallet>
{(isOk) => (
<Track {...BATCH_EVENTS.BATCH_NEW_TX}>
<Button onClick={onAddClick} disabled={!isOk}>
<SvgIcon component={PlusIcon} inheritViewBox fontSize="small" sx={{ mr: 1 }} />
Add new transaction
</Button>
</Track>
)}
</CheckWallet>

<Divider />

<Track {...BATCH_EVENTS.BATCH_CONFIRM} label={batchTxs.length}>
<Button
variant="contained"
onClick={onConfirmClick}
disabled={!batchTxs.length}
className={css.confirmButton}
>
Confirm batch
</Button>
</Track>
<CheckWallet>
{(isOk) => (
<Track {...BATCH_EVENTS.BATCH_CONFIRM} label={batchTxs.length}>
<Button
variant="contained"
onClick={onConfirmClick}
disabled={!batchTxs.length || !isOk}
className={css.confirmButton}
>
Confirm batch
</Button>
</Track>
)}
</CheckWallet>
</>
) : (
<EmptyBatch>
<Track {...BATCH_EVENTS.BATCH_NEW_TX}>
<Button onClick={onAddClick} variant="contained">
New transaction
</Button>
</Track>
<CheckWallet>
{(isOk) => (
<Track {...BATCH_EVENTS.BATCH_NEW_TX}>
<Button onClick={onAddClick} variant="contained" disabled={!isOk}>
New transaction
</Button>
</Track>
)}
</CheckWallet>
</EmptyBatch>
)}

Expand Down
9 changes: 8 additions & 1 deletion src/components/common/Notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from '@/store'
import type { Notification } from '@/store/notificationsSlice'
import { closeNotification, readNotification, selectNotifications } from '@/store/notificationsSlice'
import type { AlertColor, SnackbarCloseReason } from '@mui/material'
import { Alert, Link, Snackbar } from '@mui/material'
import { Alert, Link, Snackbar, Typography } from '@mui/material'
import css from './styles.module.css'
import NextLink from 'next/link'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
Expand Down Expand Up @@ -45,6 +45,7 @@ export const NotificationLink = ({
}

const Toast = ({
title,
message,
detailedMessage,
variant,
Expand Down Expand Up @@ -73,6 +74,12 @@ const Toast = ({
return (
<Snackbar open onClose={handleClose} sx={toastStyle} autoHideDuration={autoHideDuration}>
<Alert severity={variant} onClose={handleClose} elevation={3} sx={{ width: '340px' }}>
{title && (
<Typography variant="body2" fontWeight="700">
{title}
</Typography>
)}

{message}

{detailedMessage && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BigNumber } from 'ethers'
import SafeTokenWidget from '..'
import { hexZeroPad } from 'ethers/lib/utils'
import { AppRoutes } from '@/config/routes'
import useSafeTokenAllocation from '@/hooks/useSafeTokenAllocation'
import useSafeTokenAllocation, { useSafeVotingPower } from '@/hooks/useSafeTokenAllocation'

const MOCK_GOVERNANCE_APP_URL = 'https://mock.governance.safe.global'

Expand Down Expand Up @@ -52,21 +52,24 @@ describe('SafeTokenWidget', () => {

it('Should render nothing for unsupported chains', () => {
;(useChainId as jest.Mock).mockImplementationOnce(jest.fn(() => '100'))
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [BigNumber.from(0), false])
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(0), , false])

const result = render(<SafeTokenWidget />)
expect(result.baseElement).toContainHTML('<body><div /></body>')
})

it('Should display 0 if Safe has no SAFE token', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [BigNumber.from(0), false])
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(0), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => expect(result.baseElement).toHaveTextContent('0'))
})

it('Should display the value formatted correctly', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [BigNumber.from('472238796133701648384'), false])
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from('472238796133701648384'), , false])

// to avoid failing tests in some environments
const NumberFormat = Intl.NumberFormat
Expand All @@ -82,7 +85,8 @@ describe('SafeTokenWidget', () => {
})

it('Should render a link to the governance app', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [BigNumber.from(420000), false])
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(420000), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => {
Expand All @@ -91,4 +95,14 @@ describe('SafeTokenWidget', () => {
)
})
})

it('Should render a claim button for SEP5 qualification', async () => {
;(useSafeTokenAllocation as jest.Mock).mockImplementation(() => [[{ tag: 'user_v2' }], , false])
;(useSafeVotingPower as jest.Mock).mockImplementation(() => [BigNumber.from(420000), , false])

const result = render(<SafeTokenWidget />)
await waitFor(() => {
expect(result.baseElement).toContainHTML('New allocation')
})
})
})
64 changes: 56 additions & 8 deletions src/components/common/SafeTokenWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,46 @@ import { SafeAppsTag, SAFE_TOKEN_ADDRESSES } from '@/config/constants'
import { AppRoutes } from '@/config/routes'
import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps'
import useChainId from '@/hooks/useChainId'
import useSafeTokenAllocation from '@/hooks/useSafeTokenAllocation'
import useSafeTokenAllocation, { useSafeVotingPower, type Vesting } from '@/hooks/useSafeTokenAllocation'
import { OVERVIEW_EVENTS } from '@/services/analytics'
import { formatVisualAmount } from '@/utils/formatters'
import { Box, ButtonBase, Skeleton, Tooltip, Typography } from '@mui/material'
import { Box, Button, ButtonBase, Skeleton, Tooltip, Typography } from '@mui/material'
import { BigNumber } from 'ethers'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { UrlObject } from 'url'
import Track from '../Track'
import SafeTokenIcon from '@/public/images/common/safe-token.svg'
import css from './styles.module.css'
import UnreadBadge from '../UnreadBadge'
import classnames from 'classnames'

const TOKEN_DECIMALS = 18

export const getSafeTokenAddress = (chainId: string): string => {
return SAFE_TOKEN_ADDRESSES[chainId]
}

const canRedeemSep5Airdrop = (allocation?: Vesting[]): boolean => {
const sep5Allocation = allocation?.find(({ tag }) => tag === 'user_v2')

if (!sep5Allocation) {
return false
}

return !sep5Allocation.isRedeemed && !sep5Allocation.isExpired
}

const SEP5_DEADLINE = '27.10'

const SafeTokenWidget = () => {
const chainId = useChainId()
const router = useRouter()
const [apps] = useRemoteSafeApps(SafeAppsTag.SAFE_GOVERNANCE_APP)
const governanceApp = apps?.[0]

const [allocation, allocationLoading] = useSafeTokenAllocation()
const [allocationData, , allocationDataLoading] = useSafeTokenAllocation()
const [allocation, , allocationLoading] = useSafeVotingPower(allocationData)

const tokenAddress = getSafeTokenAddress(chainId)
if (!tokenAddress) {
Expand All @@ -40,24 +55,57 @@ const SafeTokenWidget = () => {
}
: undefined

const canRedeemSep5 = canRedeemSep5Airdrop(allocationData)
const flooredSafeBalance = formatVisualAmount(allocation || BigNumber.from(0), TOKEN_DECIMALS, 2)

return (
<Box className={css.buttonContainer}>
<Tooltip title={url ? `Open ${governanceApp?.name}` : ''}>
<Tooltip
title={
url
? canRedeemSep5
? `Claim any amount until ${SEP5_DEADLINE} to be eligible!`
: `Open ${governanceApp?.name}`
: ''
}
>
<span>
<Track {...OVERVIEW_EVENTS.SAFE_TOKEN_WIDGET}>
<Link href={url || ''} passHref legacyBehavior>
<ButtonBase
aria-describedby="safe-token-widget"
sx={{ alignSelf: 'stretch' }}
className={css.tokenButton}
className={classnames(css.tokenButton, { [css.sep5]: canRedeemSep5 })}
disabled={url === undefined}
>
<SafeTokenIcon width={24} height={24} />
<Typography component="div" lineHeight="16px" fontWeight={700}>
{allocationLoading ? <Skeleton width="16px" animation="wave" /> : flooredSafeBalance}
<Typography
component="div"
lineHeight="16px"
fontWeight={700}
// Badge does not accept className so must be here
className={css.allocationBadge}
>
<UnreadBadge
invisible={!canRedeemSep5}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
{allocationDataLoading || allocationLoading ? (
<Skeleton width="16px" animation="wave" />
) : (
flooredSafeBalance
)}
</UnreadBadge>
</Typography>
{canRedeemSep5 && (
<Track {...OVERVIEW_EVENTS.SEP5_ALLOCATION_BUTTON}>
<Button variant="contained" className={css.redeemButton}>
New allocation
</Button>
</Track>
)}
</ButtonBase>
</Link>
</Track>
Expand Down
14 changes: 14 additions & 0 deletions src/components/common/SafeTokenWidget/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,18 @@
gap: var(--space-1);
margin-left: 0;
margin-right: 0;
align-self: stretch;
}

.sep5 {
height: 42px;
}

[data-theme='dark'] .allocationBadge :global .MuiBadge-dot {
background-color: var(--color-primary-main);
}

.redeemButton {
margin-left: var(--space-1);
padding: calc(var(--space-1) / 2) var(--space-1);
}
Loading

0 comments on commit 441ce4c

Please sign in to comment.