Skip to content
This repository was archived by the owner on Nov 10, 2023. It is now read-only.

Commit 7c0fd70

Browse files
author
Diogo Soares
committed
add delegate
1 parent e115a19 commit 7c0fd70

File tree

3 files changed

+188
-6
lines changed

3 files changed

+188
-6
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { ReactElement } from 'react'
2+
3+
import { Modal } from 'src/components/Modal'
4+
import Field from 'src/components/forms/Field'
5+
import GnoForm from 'src/components/forms/GnoForm'
6+
import TextField from 'src/components/forms/TextField'
7+
import Block from 'src/components/layout/Block'
8+
import Col from 'src/components/layout/Col'
9+
import Row from 'src/components/layout/Row'
10+
import { composeValidators, mustBeAddressHash, required } from 'src/components/forms/validator'
11+
12+
import { useStyles } from './style'
13+
14+
type DelegateEntry = {
15+
address: string
16+
label: string
17+
}
18+
19+
const formMutators = {
20+
setDelegateAddress: (args, state, utils) => {
21+
utils.changeValue(state, 'address', () => args[0])
22+
},
23+
setDelegateLabel: (args, state, utils) => {
24+
utils.changeValue(state, 'label', () => args[0])
25+
},
26+
}
27+
28+
type AddDelegateModalProps = {
29+
isOpen: boolean
30+
onClose: () => void
31+
onSubmit: (entry: DelegateEntry) => void
32+
}
33+
34+
export const AddDelegateModal = ({ isOpen, onClose, onSubmit }: AddDelegateModalProps): ReactElement => {
35+
const classes = useStyles()
36+
37+
const onFormSubmitted = (values: DelegateEntry) => {
38+
onSubmit(values)
39+
}
40+
41+
return (
42+
<Modal description="Add a new Safe delegate" handleClose={onClose} open={isOpen} title="Add a delegate">
43+
<Modal.Header onClose={onClose}>
44+
<Modal.Header.Title>{'Add a delegate'}</Modal.Header.Title>
45+
</Modal.Header>
46+
<Modal.Body withoutPadding>
47+
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
48+
{(...args) => {
49+
const formState = args[2]
50+
51+
return (
52+
<>
53+
<Block className={classes.container}>
54+
<Row margin="md">
55+
<Col xs={11}>
56+
<Field
57+
component={TextField}
58+
name="address"
59+
placeholder="Delegate address"
60+
label="Delegate*"
61+
type="text"
62+
validate={composeValidators(required, mustBeAddressHash)}
63+
/>
64+
</Col>
65+
</Row>
66+
<Row margin="md">
67+
<Col xs={11}>
68+
<Field
69+
component={TextField}
70+
name="label"
71+
placeholder="Label"
72+
label="Label*"
73+
type="text"
74+
validate={required}
75+
/>
76+
</Col>
77+
</Row>
78+
</Block>
79+
<Modal.Footer>
80+
<Modal.Footer.Buttons
81+
cancelButtonProps={{ onClick: onClose }}
82+
confirmButtonProps={{
83+
disabled: !formState.valid,
84+
text: 'Add',
85+
}}
86+
/>
87+
</Modal.Footer>
88+
</>
89+
)
90+
}}
91+
</GnoForm>
92+
</Modal.Body>
93+
</Modal>
94+
)
95+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createStyles, makeStyles } from '@material-ui/core/styles'
2+
3+
import { lg, md } from 'src/theme/variables'
4+
5+
export const useStyles = makeStyles(
6+
createStyles({
7+
heading: {
8+
padding: lg,
9+
justifyContent: 'space-between',
10+
boxSizing: 'border-box',
11+
height: '74px',
12+
},
13+
manage: {
14+
fontSize: lg,
15+
},
16+
container: {
17+
padding: `${md} ${lg}`,
18+
},
19+
close: {
20+
height: '35px',
21+
width: '35px',
22+
},
23+
}),
24+
)

src/routes/safe/components/Settings/Delegates/index.tsx

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ReactElement, useEffect, useMemo, useState } from 'react'
1+
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
22
import { useSelector } from 'react-redux'
33
import styled from 'styled-components'
4-
import { Table, TableHeader, TableRow, Text } from '@gnosis.pm/safe-react-components'
4+
import { ButtonLink, Table, TableHeader, TableRow, Text } from '@gnosis.pm/safe-react-components'
55

66
import Block from 'src/components/layout/Block'
77
import Heading from 'src/components/layout/Heading'
@@ -10,7 +10,12 @@ import { lg } from 'src/theme/variables'
1010
import { currentSafeWithNames } from 'src/logic/safe/store/selectors'
1111
import { getChainInfo } from 'src/config'
1212
import { checksumAddress } from 'src/utils/checksumAddress'
13+
import { getWeb3 } from 'src/logic/wallets/getWeb3'
14+
import { AddDelegateModal } from 'src/routes/safe/components/Settings/Delegates/AddDelegateModal'
15+
import { userAccountSelector } from 'src/logic/wallets/store/selectors'
16+
import { keccak256, fromAscii } from 'web3-utils'
1317

18+
// TODO: these types will come from the Client GW SDK once #72 is merged
1419
type Page<T> = {
1520
next?: string
1621
previous?: string
@@ -37,8 +42,10 @@ const StyledHeading = styled(Heading)`
3742

3843
const Delegates = (): ReactElement => {
3944
const { address: safeAddress } = useSelector(currentSafeWithNames)
40-
const [delegatesList, setDelegatesList] = useState<DelegateResponse['results']>([])
45+
const userAccount = useSelector(userAccountSelector)
4146
const { transactionService } = getChainInfo()
47+
const [delegatesList, setDelegatesList] = useState<DelegateResponse['results']>([])
48+
const [addDelegateModalOpen, setAddDelegateModalOpen] = useState<boolean>(false)
4249

4350
const headerCells: TableHeader[] = useMemo(
4451
() => [
@@ -50,9 +57,7 @@ const Delegates = (): ReactElement => {
5057
)
5158
const rows: TableRow[] = useMemo(() => [], [])
5259

53-
useEffect(() => {
54-
if (!safeAddress || !transactionService) return
55-
60+
const fetchDelegates = useCallback(() => {
5661
const url = `${transactionService}/api/v1/safes/${safeAddress}/delegates/`
5762
fetch(url)
5863
.then((response) => response.json())
@@ -61,6 +66,22 @@ const Delegates = (): ReactElement => {
6166
})
6267
}, [safeAddress, transactionService])
6368

69+
const getSignature = async (delegate) => {
70+
const totp = Math.floor(Date.now() / 1000 / 3600)
71+
const web3 = getWeb3()
72+
const msg = checksumAddress(delegate) + totp
73+
74+
const hashMessage = keccak256(fromAscii(msg))
75+
const signature = await web3.eth.sign(hashMessage, userAccount)
76+
77+
return signature
78+
}
79+
80+
useEffect(() => {
81+
if (!safeAddress || !transactionService) return
82+
fetchDelegates()
83+
}, [fetchDelegates, safeAddress, transactionService])
84+
6485
useEffect(() => {
6586
if (delegatesList.length) {
6687
let index = 0
@@ -77,12 +98,54 @@ const Delegates = (): ReactElement => {
7798
}
7899
}, [delegatesList, rows])
79100

101+
const handleAddDelegate = async ({ address, label }) => {
102+
// close Add delegate modal
103+
setAddDelegateModalOpen(false)
104+
105+
const delegate = checksumAddress(address)
106+
107+
const signature = await getSignature(delegate)
108+
const requestOptions = {
109+
method: 'POST',
110+
headers: { 'Content-type': 'application/json' },
111+
body: JSON.stringify({
112+
safe: safeAddress,
113+
delegate: delegate,
114+
signature: signature,
115+
label: label,
116+
}),
117+
}
118+
119+
const url = `${transactionService}/api/v1/safes/${safeAddress}/delegates/`
120+
fetch(url, requestOptions)
121+
.then((response) => response.json())
122+
.then(() => {
123+
fetchDelegates()
124+
})
125+
}
126+
80127
return (
81128
<StyledBlock>
82129
<StyledHeading tag="h2">Manage Safe Delegates</StyledHeading>
83130
<Paragraph>Get, add and delete delegates.</Paragraph>
131+
<ButtonLink
132+
onClick={() => {
133+
setAddDelegateModalOpen(true)
134+
}}
135+
color="primary"
136+
iconType="add"
137+
iconSize="sm"
138+
textSize="xl"
139+
>
140+
Add delegate
141+
</ButtonLink>
84142
<pre>{JSON.stringify(delegatesList, undefined, 2)}</pre>
85143
<Table headers={headerCells} rows={rows} />
144+
<AddDelegateModal
145+
isOpen={addDelegateModalOpen}
146+
onClose={() => setAddDelegateModalOpen(false)}
147+
onSubmit={handleAddDelegate}
148+
/>
86149
</StyledBlock>
87150
)
88151
}

0 commit comments

Comments
 (0)