Merge pull request #911 from nymtech/feature/faucet-page-react

Feature/faucet page react
This commit is contained in:
Tommy Verrall
2021-11-25 09:59:54 +00:00
committed by GitHub
22 changed files with 6151 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
VALIDATOR_ADDRESS=
MNEMONIC=
TESTNET_URL_1=
TESTNET_URL_2=
ACCOUNT_ADDRESS=
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
.parcel-cache
+17
View File
@@ -0,0 +1,17 @@
# Nym Testnet Token Faucet Page
## Installation
`yarn install`
## Environment variables
Create a file in the root directory called `.env`. Copy everything from the `.env.sample` file into the `.env` file and add the required values`
## Development environment
`yarn dev`
## Build
`yarn build`
+27
View File
@@ -0,0 +1,27 @@
{
"name": "testnet-faucet",
"version": "1.0.0",
"description": "testnet faucet for token aquisition",
"license": "MIT",
"scripts": {
"dev": "yarn parcel ./src/index.html",
"build": "yarn parcel build ./src/index.html"
},
"dependencies": {
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
"@hookform/resolvers": "^2.8.3",
"@mui/icons-material": "^5.1.1",
"@mui/material": "^5.1.1",
"@nymproject/nym-validator-client": "^0.18.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.20.1",
"yup": "^0.32.11"
},
"devDependencies": {
"@types/react": "^17.0.35",
"@types/react-dom": "^17.0.11",
"parcel": "^2.0.1"
}
}
+34
View File
@@ -0,0 +1,34 @@
import { AppBar, Container, Toolbar } from '@mui/material'
import logo from './images/nym-logo.svg'
import { NymThemeProvider } from './theme'
import { Form } from './components/form'
import { Header } from './components/header'
import { GlobalContextProvider } from './context'
export const App = () => {
return (
<NymThemeProvider>
<GlobalContextProvider>
<AppBar
position="sticky"
sx={{
bgcolor: '#070B15',
backgroundImage: 'none',
boxShadow: 'none',
mt: 5,
}}
>
<Container fixed>
<Toolbar disableGutters>
<img src={logo} />
</Toolbar>
</Container>
</AppBar>
<Container fixed>
<Header />
<Form />
</Container>
</GlobalContextProvider>
</NymThemeProvider>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { useContext } from 'react'
import {
Card,
CardHeader,
CircularProgress,
IconButton,
Typography,
} from '@mui/material'
import RefreshIcon from '@mui/icons-material/Refresh'
import { GlobalContext, EnumRequestType } from '../context'
export const Balance = () => {
const { balance, loadingState, getBalance } = useContext(GlobalContext)
return (
<Card
sx={{
background: 'transparent',
border: (theme) => `1px solid ${theme.palette.common.white}`,
p: 2,
}}
>
<CardHeader
title={
<Typography variant="h6">
The current faucet balance is{' '}
<Typography
component="span"
variant="h6"
data-testid="punk-balance-message"
>
{balance} PUNKS
</Typography>
</Typography>
}
action={
loadingState.isLoading &&
loadingState.requestType === EnumRequestType.balance ? (
<CircularProgress size={12} />
) : (
<IconButton onClick={getBalance}>
<RefreshIcon />
</IconButton>
)
}
/>
</Card>
)
}
@@ -0,0 +1,105 @@
import { useContext } from 'react'
import {
Alert,
Button,
CircularProgress,
TextField,
useMediaQuery,
} from '@mui/material'
import { Box } from '@mui/system'
import { yupResolver } from '@hookform/resolvers/yup'
import { useForm, SubmitHandler } from 'react-hook-form'
import { validationSchema } from './validationSchema'
import { getCoinValue } from '../../utils'
import { EnumRequestType, GlobalContext } from '../../context'
import { TokenTransfer } from '../token-transfer'
type TFormData = {
address: string
amount: string
}
export const Form = () => {
const matches = useMediaQuery('(max-width:500px)')
const {
register,
handleSubmit,
setValue,
formState: { errors },
} = useForm({ resolver: yupResolver(validationSchema) })
console.log(errors)
const { requestTokens, loadingState, tokenTransfer, error } =
useContext(GlobalContext)
const onSubmit: SubmitHandler<TFormData> = async (data) => {
const upunks = getCoinValue(data.amount)
await requestTokens({
address: data.address,
upunks: upunks.toString(),
punks: data.amount,
})
resetForm()
}
const resetForm = () => {
setValue('address', '')
setValue('amount', '')
}
return (
<Box>
<TextField
label="Address"
fullWidth
{...register('address')}
sx={{ mb: 1 }}
helperText={errors?.address?.message}
error={!!errors.address}
data-testid="address"
/>
<TextField
label="Amount (PUNKS)"
fullWidth
{...register('amount')}
sx={{ mb: 1 }}
helperText={errors?.amount?.message}
error={!!errors.amount}
data-testid={'punk-amounts'}
/>
<Box
sx={{
mb: 5,
display: 'flex',
justifyContent: 'flex-end',
flexWrap: 'wrap',
}}
>
<Button
size="large"
variant="contained"
fullWidth={matches}
onClick={handleSubmit(onSubmit)}
endIcon={
loadingState.requestType === EnumRequestType.tokens && (
<CircularProgress size={20} color="inherit" />
)
}
disabled={loadingState.isLoading}
data-testid="request-token-button"
>
Request Tokens
</Button>
</Box>
{error && <Alert severity="error">{error}</Alert>}
{tokenTransfer && (
<TokenTransfer
address={tokenTransfer.address}
amount={tokenTransfer.amount}
/>
)}
</Box>
)
}
@@ -0,0 +1,12 @@
import * as Yup from 'yup'
import { validAmount } from '../../utils'
export const validationSchema = Yup.object().shape({
address: Yup.string().strict().trim('Cannot have leading space').required(),
amount: Yup.string()
.required()
.test('valid-amount', 'A valid amount is required', (amount) => {
if (!amount) return false
return validAmount(amount)
}),
})
+34
View File
@@ -0,0 +1,34 @@
import { Grid, Typography, useMediaQuery } from '@mui/material'
import { Box } from '@mui/system'
import { Balance } from './balance'
export const Header = () => {
const matches = useMediaQuery('(min-width: 500px)')
return (
<Box sx={{ mb: 3, mt: 3 }}>
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Typography
variant="h4"
sx={{ fontWeight: 'light' }}
data-testid="token-faucet"
>
Nym token faucet
</Typography>
{matches && (
<Typography
color="primary"
variant="h3"
sx={{ fontWeight: 'light' }}
>
Tokens to your address
</Typography>
)}
</Grid>
<Grid item xs={12} md={6}>
<Balance />
</Grid>
</Grid>
</Box>
)
}
@@ -0,0 +1,40 @@
import { Card, CardHeader, Link, Typography } from '@mui/material'
import { urls } from '../context/index'
export const TokenTransfer = ({
address,
amount,
}: {
address: string
amount: string
}) => {
return (
<Card
sx={{
background: 'transparent',
border: (theme) => `1px solid ${theme.palette.common.white}`,
p: 2,
overflow: 'auto',
}}
>
<CardHeader
title={
<>
<Typography component="span" variant="h5">
Successfully transferred {amount} PUNKS to
</Typography>{' '}
<Link
target="_blank"
rel="noopener"
href={`${urls.blockExplorer}/account/${address}`}
data-testid="success-sent-message"
variant="h5"
>
{address}
</Link>
</>
}
/>
</Card>
)
}
+126
View File
@@ -0,0 +1,126 @@
import { createContext, useEffect, useState } from 'react'
import ClientValidator, {
nativeToPrintable,
} from '@nymproject/nym-validator-client'
export const urls = {
blockExplorer: 'https://testnet-milhon-blocks.nymtech.net',
}
type TGlobalContext = {
getBalance: () => void
requestTokens: ({
address,
upunks,
}: {
address: string
upunks: string
punks: string
}) => void
loadingState: TLoadingState
balance?: string
tokenTransfer?: { address: string; amount: string }
error?: string
}
export const GlobalContext = createContext({} as TGlobalContext)
const { VALIDATOR_ADDRESS, MNEMONIC, TESTNET_URL_1, ACCOUNT_ADDRESS } =
process.env
export enum EnumRequestType {
balance = 'balance',
tokens = 'tokens',
}
type TLoadingState = {
isLoading: boolean
requestType?: EnumRequestType
}
export const GlobalContextProvider: React.FC = ({ children }) => {
const [validator, setValidator] = useState<ClientValidator>()
const [loadingState, setLoadingState] = useState<TLoadingState>({
isLoading: false,
requestType: undefined,
})
const [balance, setBalance] = useState<string>()
const [error, setError] = useState<string>()
const [tokenTransfer, setTokenTransfer] =
useState<{ address: string; amount: string }>()
const getValidator = async () => {
const Validator = await ClientValidator.connect(
VALIDATOR_ADDRESS,
MNEMONIC,
[TESTNET_URL_1],
'punk'
)
setValidator(Validator)
}
useEffect(() => {
if (loadingState.isLoading) {
setError(undefined)
}
}, [loadingState])
useEffect(() => {
getValidator()
}, [])
useEffect(() => {
if (validator || tokenTransfer) getBalance()
}, [validator, tokenTransfer])
const getBalance = async () => {
setLoadingState({ isLoading: true, requestType: EnumRequestType.balance })
try {
const balance = await validator?.getBalance(ACCOUNT_ADDRESS)
const punks = nativeToPrintable(balance?.amount || '')
setBalance(punks)
} catch (e) {
setError(`An error occured while getting the balance: ${e}`)
} finally {
setLoadingState({ isLoading: false, requestType: undefined })
}
}
const requestTokens = async ({
address,
upunks,
punks,
}: {
address: string
upunks: string
punks: string
}) => {
setTokenTransfer(undefined)
setLoadingState({ isLoading: true, requestType: EnumRequestType.tokens })
try {
await validator?.send(ACCOUNT_ADDRESS, address, [
{ amount: upunks, denom: 'upunk' },
])
setTokenTransfer({ address, amount: punks })
} catch (e) {
setError(`An error occured during the transfer request: ${e}`)
} finally {
setLoadingState({ isLoading: false, requestType: undefined })
}
}
return (
<GlobalContext.Provider
value={{
getBalance,
requestTokens,
loadingState,
balance,
tokenTransfer,
error,
}}
>
{children}
</GlobalContext.Provider>
)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="100" height="28" viewBox="0 0 100 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31117 3.0504L15.0523 27.4194H20.6547H26.9659V0H20.6547V24.3347L11.9473 0H6.31117H0V27.4194H6.31117V3.0504Z" fill="white"/>
<path d="M87.9513 0L81.9776 24.3347L76.004 0H63.9216V27.4194H70.2328V3.0504L76.2065 27.4194H87.7151L93.6887 3.0504V27.4194H99.9999V0H87.9513Z" fill="white"/>
<path d="M45.461 13.6411L37.6986 0H30.4087L42.2885 20.9073V27.4194H48.5997V20.9073L60.4795 0H53.1896L45.461 13.6411Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="./images/favicon.png" />
<title>Nym Testnet Faucet</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import ReactDOM from 'react-dom'
import { App } from './App'
const app = document.getElementById('app')
ReactDOM.render(<App />, app)
+14
View File
@@ -0,0 +1,14 @@
import * as React from 'react'
import { createTheme, ThemeProvider } from '@mui/material/styles'
import { CssBaseline } from '@mui/material'
import { getDesignTokens } from './theme'
export const NymThemeProvider: React.FC = ({ children }) => {
const theme = createTheme(getDesignTokens('dark'))
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
)
}
+115
View File
@@ -0,0 +1,115 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface */
import {
Theme,
ThemeOptions,
Palette,
PaletteOptions,
} from '@mui/material/styles'
import { PaletteMode } from '@mui/material'
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This interface defines a palette used across Nym for branding
*/
interface NymPalette {
highlight: string
text: {
nav: string
footer: string
}
}
interface NymPaletteVariant {
mode: PaletteMode
background: {
main: string
paper: string
}
text: {
main: string
}
topNav: {
background: string
}
nav: {
background: string
hover: string
}
}
/**
* A palette definition only for the Network Explorer that extends the Nym palette
*/
interface NetworkExplorerPalette {
networkExplorer: {
map: {
stroke: string
fills: string[]
}
background: {
tertiary: string
}
topNav: {
background: string
socialIcons: string
appBar: string
}
nav: {
selected: {
main: string
nested: string
}
background: string
hover: string
text: string
}
footer: {
socialIcons: string
}
}
}
interface NymPaletteAndNetworkExplorerPalette {
nym: NymPalette
}
type NymPaletteAndNetworkExplorerPaletteOptions =
Partial<NymPaletteAndNetworkExplorerPalette>
/**
* Add anything not palette related to the theme here
*/
interface NymTheme {}
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteAndNetworkExplorerPalette {}
interface PaletteOptions extends NymPaletteAndNetworkExplorerPaletteOptions {}
}
+181
View File
@@ -0,0 +1,181 @@
import { PaletteMode } from '@mui/material'
import {
PaletteOptions,
NymPalette,
NetworkExplorerPalette,
ThemeOptions,
createTheme,
NymPaletteVariant,
} from '@mui/material/styles'
//-----------------------------------------------------------------------------------------------
// Nym palette type definitions
//
/**
* The Nym palette.
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
const nymPalette: NymPalette = {
/** emphasises important elements */
highlight: '#FB6E4E',
text: {
nav: '#F2F2F2',
/** footer text colour */
footer: '#666B77',
},
}
const darkMode: NymPaletteVariant = {
mode: 'dark',
background: {
main: '#070B15',
paper: '#242C3D',
},
text: {
main: '#F2F2F2',
},
topNav: {
background: '#111826',
},
nav: {
background: '#242C3D',
hover: '#111826',
},
}
const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#F2F2F2',
paper: '#FFFFFF',
},
text: {
main: '#666666',
},
topNav: {
background: '#111826',
},
nav: {
background: '#242C3D',
hover: '#111826',
},
}
//-----------------------------------------------------------------------------------------------
// Nym palettes for light and dark mode
//
/**
* Map a Nym palette variant onto the MUI palette
*/
const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
text: {
primary: variant.text.main,
},
primary: {
main: nymPalette.highlight,
contrastText: '#fff',
},
background: {
default: variant.background.main,
paper: variant.background.paper,
},
})
/**
* Returns the Network Explorer palette for light mode.
*/
const createLightModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
},
...variantToMUIPalette(lightMode),
})
/**
* Returns the Network Explorer palette for dark mode.
*/
const createDarkModePalette = (): PaletteOptions => ({
nym: {
...nymPalette,
},
...variantToMUIPalette(darkMode),
})
/**
* IMPORANT: if you need to get the default MUI theme, use the following
*
* import { createTheme as systemCreateTheme } from '@mui/system';
*
* // get the MUI system defaults for light mode
* const systemTheme = systemCreateTheme({ palette: { mode: 'light' } });
*
*
* return {
* // change `primary` to default MUI `success`
* primary: {
* main: systemTheme.palette.success.main,
* },
* nym: {
* ...nymPalette,
* },
* };
*/
//-----------------------------------------------------------------------------------------------
// Nym theme overrides
//
/**
* Gets the theme options to be passed to `createTheme`.
*
* Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette.
*
* @param mode The theme mode: 'light' or 'dark'
*/
export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
// first, create the palette from user's choice of light or dark mode
const { palette } = createTheme({
palette: {
mode,
...(mode === 'light'
? createLightModePalette()
: createDarkModePalette()),
},
})
// then customise theme and components
return {
typography: {
fontFamily: [
'Open Sans',
'sans-serif',
'BlinkMacSystemFont',
'Roboto',
'Oxygen',
'Ubuntu',
'Helvetica Neue',
].join(','),
fontSize: 14,
fontWeightRegular: 600,
},
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
},
},
palette,
}
}
+16
View File
@@ -0,0 +1,16 @@
declare module '*.svg' {
const content: any
export default content
}
namespace NodeJS {
export interface Process {
env: {
VALIDATOR_ADDRESS: string
MNEMONIC: string
TESTNET_URL_1: string
TESTNET_URL_2: string
ACCOUNT_ADDRESS: string
}
}
}
+11
View File
@@ -0,0 +1,11 @@
import { printableBalanceToNative } from '@nymproject/nym-validator-client/dist/currency'
export const getCoinValue = (raw: string): number => {
let native = printableBalanceToNative(raw)
return parseInt(native)
}
export const validAmount = (amount: string): boolean => {
if (isNaN(+amount)) return false
return true
}
+21
View File
@@ -0,0 +1,21 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"jsx": "react-jsx",
"sourceMap": true,
"baseUrl": "."
},
"exclude": ["node_modules", "dist"]
}
File diff suppressed because it is too large Load Diff