Skip to content
Open
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:20-alpine
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is unrelated but needed to make it for it to build

RUN apk add --no-cache libc6-compat git python3 py3-pip make g++ libusb-dev eudev-dev linux-headers
WORKDIR /app
COPY . .
Expand Down
8 changes: 8 additions & 0 deletions src/components/tx-flow/common/TxButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ export const SendNFTsButton = () => {
</Link>
)
}

export const CustomTransactionButton = ({ onClick, sx }: { onClick: () => void; sx?: ButtonProps['sx'] }) => {
return (
<Button onClick={onClick} variant="outlined" sx={sx ?? buttonSx} fullWidth>
Custom transaction
</Button>
)
}
170 changes: 170 additions & 0 deletions src/components/tx-flow/flows/CustomTransaction/CreateCustomTx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useCallback } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { Button, Typography, Divider, Box, TextField, InputAdornment, Tooltip, IconButton, SvgIcon } from '@mui/material'
import AddressInput from '@/components/common/AddressInput'
import TxCard from '../../common/TxCard'
import { validateAddress, validateHexData } from '@/utils/validation'
import { parseUnits } from '@ethersproject/units'
import type { CustomTransactionParams } from '.'
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { safeFormatUnits } from '@/utils/formatters'
import useBalances from '@/hooks/useBalances'

export type CustomTxParams = {
contractAddress: string
value: string
calldata: string
nonce?: number
}

const CreateCustomTx = ({
params,
onSubmit,
txNonce,
}: {
params: CustomTransactionParams
onSubmit: (data: CustomTxParams) => void
txNonce?: number
}) => {
const chain = useCurrentChain()
const { safe } = useSafeInfo()
const { balances } = useBalances()
const nativeToken = balances.find((item) => item.tokenInfo.type === 'NATIVE_TOKEN')

const formMethods = useForm<CustomTxParams>({
defaultValues: {
contractAddress: params.contractAddress,
value: params.value,
calldata: params.calldata || '0x',
nonce: txNonce,
},
mode: 'onChange',
})

const {
register,
handleSubmit,
watch,
formState: { errors, isValid },
} = formMethods

const contractAddress = watch('contractAddress')
const value = watch('value')
const calldata = watch('calldata')

const validateValue = useCallback(
(value: string) => {
if (!value || value === '0') return

try {
const valueBN = parseUnits(value, nativeToken?.tokenInfo.decimals || 18)
const balance = nativeToken?.balance || '0'

if (valueBN.gt(balance)) {
return 'Insufficient balance'
}
} catch (e) {
return 'Invalid amount'
}
},
[nativeToken],
)

const onCopyCalldata = () => {
navigator.clipboard.writeText(calldata)
}

const handleFormSubmit = (data: CustomTxParams) => {
onSubmit({
...data,
value: data.value || '0',
})
}

return (
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<TxCard>
<Typography variant="body2" mb={2}>
Interact with any contract by providing the address and calldata.
</Typography>

<AddressInput
name="contractAddress"
label="Contract address"
validate={(value) => validateAddress(value) || undefined}
required
/>

<Box mt={2}>
<TextField
{...register('value', {
validate: validateValue,
})}
label="Value (ETH)"
fullWidth
error={!!errors.value}
helperText={
errors.value?.message ||
(nativeToken && (
<Typography variant="body2" color="text.secondary">
Balance: {safeFormatUnits(nativeToken.balance, nativeToken.tokenInfo.decimals)}{' '}
{nativeToken.tokenInfo.symbol}
</Typography>
))
}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography variant="body2">{chain?.nativeCurrency.symbol || 'ETH'}</Typography>
</InputAdornment>
),
}}
/>
</Box>

<Box mt={2}>
<TextField
{...register('calldata', {
required: 'Calldata is required',
validate: (value) => validateHexData(value),
})}
label="Calldata"
multiline
rows={4}
fullWidth
error={!!errors.calldata}
helperText={errors.calldata?.message}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip title="Copy calldata">
<IconButton onClick={onCopyCalldata} edge="end">
<SvgIcon component={ContentCopyIcon} fontSize="small" />
</IconButton>
</Tooltip>
</InputAdornment>
),
sx: { alignItems: 'flex-start' },
}}
/>
</Box>

<Typography variant="caption" color="text.secondary" mt={1}>
Tip: You can copy the transaction data to simulate it in external tools before execution.
</Typography>

<Divider sx={{ mt: 3, mb: 3 }} />

<Button variant="contained" type="submit" disabled={!isValid} fullWidth>
Next
</Button>
</TxCard>
</form>
</FormProvider>
)
}

export default CreateCustomTx
91 changes: 91 additions & 0 deletions src/components/tx-flow/flows/CustomTransaction/ReviewCustomTx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useContext, useEffect } from 'react'
import type { CustomTransactionParams } from '.'
import { encodeAbiParameters, parseAbiParameters, type Hex } from 'viem'
import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { SafeTxContext } from '../../SafeTxProvider'
import { createTx } from '@/services/tx/tx-sender'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { parseUnits } from '@ethersproject/units'
import TxCard from '../../common/TxCard'
import { Box, Typography, Divider } from '@mui/material'
import { HexEncodedData } from '@/components/transactions/HexEncodedData'
import EthHashInfo from '@/components/common/EthHashInfo'
import { safeFormatUnits } from '@/utils/formatters'

const ReviewCustomTx = ({
params,
txNonce,
onSubmit,
}: {
params: CustomTransactionParams
txNonce?: number
onSubmit: () => void
}) => {
const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext)
const chain = useCurrentChain()
const { safe } = useSafeInfo()

useEffect(() => {
if (!params.contractAddress || !chain || !safe) return

const value = params.value || '0'
const data = params.calldata || '0x'

createTx({
to: params.contractAddress,
value: parseUnits(value, chain.nativeCurrency.decimals).toString(),
data,
nonce: txNonce,
})
.then(setSafeTx)
.catch(setSafeTxError)
}, [params, chain, safe, txNonce, setSafeTx, setSafeTxError])

const displayValue = params.value && params.value !== '0'
? `${params.value} ${chain?.nativeCurrency.symbol || 'ETH'}`
: undefined

return (
<SignOrExecuteForm onSubmit={onSubmit}>
<TxCard>
<Typography variant="h6" mb={2}>
Review transaction
</Typography>

<Box mb={2}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Contract
</Typography>
<EthHashInfo address={params.contractAddress} showCopyButton hasExplorer />
</Box>

{displayValue && (
<Box mb={2}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Value
</Typography>
<Typography variant="body1">{displayValue}</Typography>
</Box>
)}

<Box mb={2}>
<Typography variant="body2" color="text.secondary" gutterBottom>
Calldata
</Typography>
<HexEncodedData hexData={params.calldata} />
</Box>

{safeTxError && (
<Box mt={2}>
<Typography color="error" variant="body2">
{safeTxError.message}
</Typography>
</Box>
)}
</TxCard>
</SignOrExecuteForm>
)
}

export default ReviewCustomTx
50 changes: 50 additions & 0 deletions src/components/tx-flow/flows/CustomTransaction/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import TxLayout from '@/components/tx-flow/common/TxLayout'
import useTxStepper from '../../useTxStepper'
import CreateCustomTx from './CreateCustomTx'
import ReviewCustomTx from './ReviewCustomTx'
import { SvgIcon } from '@mui/material'
import SettingsIcon from '@/public/images/sidebar/settings.svg'

export type CustomTransactionParams = {
contractAddress: string
value: string
calldata: string
}

const defaultParams: CustomTransactionParams = {
contractAddress: '',
value: '0',
calldata: '',
}

type CustomTransactionFlowProps = {
txNonce?: number
}

const CustomTransactionFlow = ({ txNonce }: CustomTransactionFlowProps) => {
const { data, step, nextStep, prevStep } = useTxStepper<CustomTransactionParams>(defaultParams)

const steps = [
<CreateCustomTx
key={0}
params={data}
txNonce={txNonce}
onSubmit={(formData) => nextStep({ ...data, ...formData })}
/>,
<ReviewCustomTx key={1} params={data} txNonce={txNonce} onSubmit={() => null} />,
]

return (
<TxLayout
title={step === 0 ? 'New transaction' : 'Confirm transaction'}
subtitle="Custom transaction"
icon={SettingsIcon}
step={step}
onBack={prevStep}
>
{steps}
</TxLayout>
)
}

export default CustomTransactionFlow
15 changes: 14 additions & 1 deletion src/components/tx-flow/flows/NewTx/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useCallback, useContext } from 'react'
import { SendNFTsButton, SendTokensButton } from '@/components/tx-flow/common/TxButton'
import { SendNFTsButton, SendTokensButton, CustomTransactionButton } from '@/components/tx-flow/common/TxButton'
import { Container, Grid, Paper, SvgIcon, Typography } from '@mui/material'
import { TxModalContext } from '../../'
import TokenTransferFlow from '../TokenTransfer'
import CustomTransactionFlow from '../CustomTransaction'
import AssetsIcon from '@/public/images/sidebar/assets.svg'
import SettingsIcon from '@/public/images/sidebar/settings.svg'
import { ProgressBar } from '@/components/common/ProgressBar'
import ChainIndicator from '@/components/common/ChainIndicator'
import NewTxIcon from '@/public/images/transactions/new-tx.svg'
Expand All @@ -17,6 +19,10 @@ const NewTxFlow = () => {
setTxFlow(<TokenTransferFlow />)
}, [setTxFlow])

const onCustomClick = useCallback(() => {
setTxFlow(<CustomTransactionFlow />)
}, [setTxFlow])

const progress = 10

return (
Expand Down Expand Up @@ -49,6 +55,13 @@ const NewTxFlow = () => {
<SendTokensButton onClick={onTokensClick} />

<SendNFTsButton />

<Typography variant="h4" className={css.type} sx={{ mt: 3 }}>
<SvgIcon component={SettingsIcon} inheritViewBox color="secondary" />
Contract interaction
</Typography>

<CustomTransactionButton onClick={onCustomClick} />
</Grid>
</Grid>
</Grid>
Expand Down
1 change: 1 addition & 0 deletions src/components/tx-flow/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export const ReplaceTxFlow = dynamic(() => import('./ReplaceTx'))
export const SuccessScreenFlow = dynamic(() => import('./SuccessScreen'))
export const TokenTransferFlow = dynamic(() => import('./TokenTransfer'))
export const UpdateSafeFlow = dynamic(() => import('./UpdateSafe'))
export const CustomTransactionFlow = dynamic(() => import('./CustomTransaction'))
Loading