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

Add support for burning tokens #1035

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
80 changes: 71 additions & 9 deletions src/app/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { createContext, useCallback, useContext, useState } from 'react'
import React from 'react'
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { Box, Button, Layer, Heading, Paragraph } from 'grommet'
import { useTranslation } from 'react-i18next'
import { Alert, Checkmark, Close } from 'grommet-icons/icons'
import { AlertBox } from '../AlertBox'
import { selectAllowDangerousSetting } from '../SettingsDialog/slice/selectors'
import { useSelector } from 'react-redux'

interface Modal {
title: string
description: string
handleConfirm: () => void

/**
* Is this a dangerous operation?
*
* If marked as such, it will only be possible to execute it if the wallet is configured to run in dangerous mode.
*
* It also automatically implies a mandatory waiting time of 10 sec, unless specified otherwise.
*/
isDangerous: boolean

/**
* How long does the user have to wait before he can actually confirm the action?
*/
mustWaitSecs?: number
}

interface ModalContainerProps {
Expand All @@ -32,27 +49,72 @@ const ModalContainer = ({ modal, closeModal }: ModalContainerProps) => {
modal.handleConfirm()
closeModal()
}, [closeModal, modal])
const { isDangerous, mustWaitSecs } = modal
const allowDangerous = useSelector(selectAllowDangerousSetting)
const forbidden = isDangerous && !allowDangerous
const waitingTime = forbidden
? 0 // If the action is forbidden, there is nothing to wait for
: isDangerous
? mustWaitSecs ?? 10 // For dangerous actions, we require 10 seconds of waiting, unless specified otherwise.
: mustWaitSecs ?? 0 // For normal, non-dangerous operations, just use what was specified

const [secsLeft, setSecsLeft] = useState(0)

useEffect(() => {
if (waitingTime) {
setSecsLeft(waitingTime)
const stopCounting = () => window.clearInterval(interval)
const interval = window.setInterval(
() =>
setSecsLeft(seconds => {
const remains = seconds - 1
if (!remains) stopCounting()
return remains
}),
1000,
)
return stopCounting
}
}, [waitingTime])

return (
<Layer modal onEsc={closeModal} onClickOutside={closeModal} background="background-front">
<Box margin="medium">
<Heading size="small">{modal.title}</Heading>
<Paragraph fill>{modal.description}</Paragraph>
{forbidden && (
<AlertBox color={'status-error'}>
{t(
'dangerMode.youDontWantThis',
"You most probably don't want to do this, so I won't allow it. If you really do, then please enable the 'dangerous mode' in wallet settings, and try again.",
)}
</AlertBox>
)}
{isDangerous && allowDangerous && (
<AlertBox color={'status-warning'}>
{t(
'dangerMode.youCanButDoYouWant',
"You most probably shouldn't do this, but since you have specifically enabled 'dangerous mode' in wallet settings, we won't stop you.",
)}
</AlertBox>
)}
<Box direction="row" gap="small" alignSelf="end" pad={{ top: 'large' }}>
<Button
label={t('common.cancel', 'Cancel')}
onClick={closeModal}
secondary
icon={<Close size="18px" />}
/>
<Button
label={t('common.confirm', 'Confirm')}
onClick={confirm}
disabled={modal.isDangerous}
primary={modal.isDangerous}
color={modal.isDangerous ? 'status-error' : ''}
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
/>
{!forbidden && (
<Button
label={t('common.confirm', 'Confirm') + (secsLeft ? ` (${secsLeft})` : '')}
onClick={confirm}
disabled={!!secsLeft}
primary={modal.isDangerous}
color={modal.isDangerous ? 'status-error' : ''}
icon={modal.isDangerous ? <Alert size="18px" /> : <Checkmark size="18px" />}
/>
)}
</Box>
</Box>
</Layer>
Expand Down
22 changes: 22 additions & 0 deletions src/app/components/SettingsButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { useState } from 'react'
import { SidebarButton } from '../Sidebar'
import { Configure } from 'grommet-icons/icons'
import { useTranslation } from 'react-i18next'
import { SettingsDialog } from '../SettingsDialog'

export const SettingsButton = () => {
const [layerVisibility, setLayerVisibility] = useState(false)
const { t } = useTranslation()
return (
<>
<SidebarButton
icon={<Configure />}
label={t('menu.settings', 'Settings')}
onClick={() => {
setLayerVisibility(true)
}}
/>
{layerVisibility && <SettingsDialog closeHandler={() => setLayerVisibility(false)} />}
</>
)
}
80 changes: 80 additions & 0 deletions src/app/components/SettingsDialog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { ResponsiveLayer } from '../ResponsiveLayer'
import { Box, Button, Heading, Paragraph, RadioButtonGroup, ResponsiveContext } from 'grommet'
import { useDispatch, useSelector } from 'react-redux'
import { selectAllowDangerousSetting } from './slice/selectors'
import { Threats } from 'grommet-icons'
import { settingsActions } from './slice'

interface SettingsDialogProps {
closeHandler: () => void
}

export const SettingsDialog = (props: SettingsDialogProps) => {
const { t } = useTranslation()
const size = useContext(ResponsiveContext)

const dispatch = useDispatch()
const dangerousMode = useSelector(selectAllowDangerousSetting)

return (
<ResponsiveLayer
onClickOutside={props.closeHandler}
onEsc={props.closeHandler}
animation="slide"
background="background-front"
modal
>
<Box pad={{ vertical: 'small' }} margin="medium" width={size === 'small' ? 'auto' : '700px'}>
<Heading size="1" margin={{ vertical: 'small' }}>
{t('settings.dialog.title', 'Wallet settings')}
</Heading>
<Paragraph fill>
{t(
'settings.dialog.description',
'This is where you can configure the behavior of the Oasis Wallet.',
)}
</Paragraph>
<Box
gap="small"
pad={{ vertical: 'medium', right: 'small' }}
overflow={{ vertical: 'auto' }}
height={{ max: '400px' }}
>
<Paragraph fill>
<strong>
{t(
'dangerMode.description',
'Dangerous mode: should the wallet let the user shoot himself in the foot?',
)}
</strong>
</Paragraph>
<RadioButtonGroup
name="doc"
options={[
{
value: false,
label: t('dangerMode.off', 'Off - Refuse to execute nonsensical actions'),
},
{
value: true,
label: (
<span>
{t('dangerMode.on', "On - Allow executing nonsensical actions. Don't blame Oasis!")}{' '}
<Threats size={'large'} />
</span>
),
},
]}
value={dangerousMode}
onChange={event => dispatch(settingsActions.setAllowDangerous(event.target.value === 'true'))}
/>
</Box>
<Box align="end" pad={{ top: 'medium' }}>
<Button primary label={t('settings.dialog.close', 'Close')} onClick={props.closeHandler} />
</Box>
</Box>
</ResponsiveLayer>
)
}
22 changes: 22 additions & 0 deletions src/app/components/SettingsDialog/slice/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from 'utils/@reduxjs/toolkit'

import { SettingsState } from './types'

export const initialState: SettingsState = {
allowDangerous: false,
}

const slice = createSlice({
name: 'settings',
initialState,
reducers: {
setAllowDangerous(state, action: PayloadAction<boolean>) {
state.allowDangerous = action.payload
},
},
})

export const { actions: settingsActions } = slice

export default slice.reducer
8 changes: 8 additions & 0 deletions src/app/components/SettingsDialog/slice/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createSelector } from '@reduxjs/toolkit'

import { RootState } from 'types'
import { initialState } from '.'

const selectSlice = (state: RootState) => state.settings || initialState

export const selectAllowDangerousSetting = createSelector([selectSlice], settings => settings.allowDangerous)
3 changes: 3 additions & 0 deletions src/app/components/SettingsDialog/slice/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SettingsState {
allowDangerous: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,39 @@ exports[`<Navigation /> should match snapshot 1`] = `
<div
class="c6"
/>
<button
aria-label="menu.settings"
class="c13"
type="button"
>
<div
class="c14"
>
<svg
aria-label="Configure"
class="c10"
viewBox="0 0 24 24"
>
<path
d="M16 15c4.009-.065 7-3.033 7-7 0-3.012-.997-2.015-2-1-.991.98-3 3-3 3l-4-4s2.02-2.009 3-3c1.015-1.003 1.015-2-1-2-3.967 0-6.947 2.991-7 7 .042.976 0 3 0 3-1.885 1.897-4.34 4.353-6 6-2.932 2.944 1.056 6.932 4 4 1.65-1.662 4.113-4.125 6-6 0 0 2.024-.042 3 0z"
fill="none"
stroke="#000"
stroke-width="2"
/>
</svg>
<div
class="c11"
/>
<span
class="c5"
>
menu.settings
</span>
</div>
</button>
<div
class="c6"
/>
<a
aria-label="GitHub"
href="https://github.com/oasisprotocol/oasis-wallet-web"
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Language } from '../../../styles/theme/icons/language/Language'
import { ThemeSwitcher } from '../ThemeSwitcher'
import logotype from '../../../../public/logo192.png'
import { languageLabels } from '../../../locales/i18n'
import { SettingsButton } from '../SettingsButton'

const SidebarTooltip = (props: { children: React.ReactNode; isActive: boolean; label: string }) => {
const size = useContext(ResponsiveContext)
Expand Down Expand Up @@ -211,6 +212,7 @@ const SidebarFooter = (props: SidebarFooterProps) => {
</Menu>
</Box>
</SidebarTooltip>
<SettingsButton />
<SidebarButton
icon={<Github />}
label="GitHub"
Expand Down
35 changes: 28 additions & 7 deletions src/app/components/Transaction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
LinkNext,
Atm,
Alert,
Trash,
} from 'grommet-icons/icons'
import type { Icon } from 'grommet-icons/icons'
import * as React from 'react'
Expand Down Expand Up @@ -99,6 +100,19 @@ export function Transaction(props: TransactionProps) {
),
}

const burnTransaction: TransactionDictionary[transactionTypes.TransactionType][TransactionSide] = {
destination: '',
icon: Trash,
header: () => (
<Trans
i18nKey="account.transaction.burnTransaction.header"
t={t}
components={{ Amount }}
defaults="Burned <Amount/>"
/>
),
}

const unrecognizedTransaction: TransactionDictionary[transactionTypes.TransactionType][TransactionSide] = {
destination: t('account.transaction.unrecognizedTransaction.destination', 'Other address'),
icon: Alert,
Expand Down Expand Up @@ -286,6 +300,10 @@ export function Transaction(props: TransactionProps) {
[TransactionSide.Received]: genericTransaction,
[TransactionSide.Sent]: genericTransaction,
},
[transactionTypes.TransactionType.StakingBurn]: {
[TransactionSide.Received]: burnTransaction,
[TransactionSide.Sent]: burnTransaction,
},
[transactionTypes.TransactionType.RoothashExecutorCommit]: {
[TransactionSide.Received]: genericTransaction,
[TransactionSide.Sent]: genericTransaction,
Expand Down Expand Up @@ -349,6 +367,7 @@ export function Transaction(props: TransactionProps) {

const Icon = matchingConfiguration.icon
const header = matchingConfiguration.header()
const hasDestination = transaction.type !== transactionTypes.TransactionType.StakingBurn
const destination = matchingConfiguration.destination
const backendLinks = config[props.network][backend()]
const externalExplorerLink = transaction.runtimeId
Expand Down Expand Up @@ -392,13 +411,15 @@ export function Transaction(props: TransactionProps) {
{!isMobile && (
<Grid columns={{ count: 'fit', size: 'xsmall' }} gap="none">
<Box pad="none">
<InfoBox
copyToClipboard={!!otherAddress}
icon={ContactInfo}
label={destination}
trimValue={!!otherAddress}
value={otherAddress || t('common.unavailable', 'Unavailable')}
/>
{hasDestination && (
<InfoBox
copyToClipboard={!!otherAddress}
icon={ContactInfo}
label={destination}
trimValue={!!otherAddress}
value={otherAddress || t('common.unavailable', 'Unavailable')}
/>
)}
<InfoBox
copyToClipboard={true}
icon={Package}
Expand Down
1 change: 1 addition & 0 deletions src/app/components/TransactionTypeFormatter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const TransactionTypeFormatter = memo((props: Props) => {
addEscrow: t('transaction.types.addEscrow', 'Delegating your tokens to a validator and generate rewards'),
reclaimEscrow: t('transaction.types.reclaimEscrow', 'Reclaiming your tokens delegated to a validator'),
transfer: t('transaction.types.transfer', 'Transferring tokens from your account to another'),
burn: t('transaction.types.burn', 'Burn tokens in your account'),
}

const typeMessage = typeMap[type]
Expand Down
Loading