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

improve Launch form #70

Merged
merged 2 commits into from
Dec 18, 2023
Merged
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 frontend/src/assets/argent-x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions frontend/src/assets/webwallet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import clsx from 'clsx'
import React, { forwardRef, useEffect, useState } from 'react'
import Box, { BoxProps } from 'src/theme/components/Box'

import * as styles from './style.css'
import * as styles from './NumericalInput.css'

const formatNumber = (value: string) => {
const numericValue = parseFloat(value.replace(/[^0-9.]/g, ''))
// add 999 at the end to not lose potential `.0` and not shift commas
const numericValue = parseInt(`${value.replace(/[^0-9]/g, '')}`)
if (isNaN(numericValue)) return ''

return new Intl.NumberFormat('en-US', {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
maximumFractionDigits: 18,
}).format(numericValue)
}

type NumberInputProps = {
addon?: React.ReactNode
} & BoxProps

const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
({ addon, className, value, onChange, onBlur, ...props }, ref) => {
const NumericalInput = forwardRef<HTMLInputElement, NumberInputProps>(
({ addon, className, value, onChange, ...props }, ref) => {
const [inputValue, setInputValue] = useState('')
useEffect(() => {
if (value !== undefined && value !== null) {
Expand All @@ -29,12 +29,8 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
}, [value])

const handleInputEvent = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value.replace(/[^0-9.]/g, ''))
onChange && onChange(event)
}
const handleBlurEvent = (event: React.FocusEvent<HTMLInputElement>) => {
setInputValue(formatNumber(event.target.value))
onBlur && onBlur(event)
onChange && onChange(event)
}

return (
Expand All @@ -48,13 +44,12 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
className={styles.input}
value={inputValue}
onChange={handleInputEvent}
onBlur={handleBlurEvent}
/>
</Box>
)
}
)

NumberInput.displayName = 'NumberInput'
NumericalInput.displayName = 'NumericalInput'

export default NumberInput
export default NumericalInput
1 change: 1 addition & 0 deletions frontend/src/constants/misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_HOLDERS_PER_DEPLOYMENT = 10
70 changes: 53 additions & 17 deletions frontend/src/pages/Launch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,49 @@ import { useCallback, useState } from 'react'
import { useFieldArray, useForm } from 'react-hook-form'
import { IconButton, PrimaryButton, SecondaryButton } from 'src/components/Button'
import Input from 'src/components/Input'
import NumberInput from 'src/components/NumberInput'
import NumericalInput from 'src/components/Input/NumericalInput'
import { TOKEN_CLASS_HASH, UDC } from 'src/constants/contracts'
import { MAX_HOLDERS_PER_DEPLOYMENT } from 'src/constants/misc'
import { useDeploymentStore } from 'src/hooks/useDeployment'
import Box from 'src/theme/components/Box'
import { Column, Row } from 'src/theme/components/Flex'
import * as Text from 'src/theme/components/Text'
import { isValidL2Address } from 'src/utils/address'
import { CallData, hash, stark, uint256 } from 'starknet'
import { z } from 'zod'

import * as styles from './style.css'

const MAX_HOLDERS = 10
// zod schemes

const address = z.string().refine(
(addr) => {
// Wallets like to omit leading zeroes, so we cannot check for a fixed length.
// On the other hand, we don't want users to mistakenly enter an Ethereum address.
return /^0x[0-9a-fA-F]{50,64}$/.test(addr)
const address = z.string().refine((address) => isValidL2Address(address), { message: 'Invalid Starknet address' })

const currencyInput = z.string().refine(
(input) => {
console.log(input)
;+input.replace(/,/g, '') > 0
},
{ message: 'Invalid Starknet address' }
{ message: 'Invalid amount' }
)

const holder = z.object({
address,
amount: z.number().min(0),
amount: currencyInput,
})

const schema = z.object({
name: z.string().min(1),
symbol: z.string().min(1),
initialRecipientAddress: address,
ownerAddress: address,
initialSupply: z.number().min(1),
initialSupply: currencyInput,
holders: z.array(holder),
})

/**
* LaunchPage component
*/

export default function LaunchPage() {
const [deployedToken, setDeployedToken] = useState<{ address: string; tx: string } | undefined>(undefined)
const { pushDeployedContract } = useDeploymentStore()
Expand Down Expand Up @@ -128,34 +135,43 @@ export default function LaunchPage() {
<Column gap="20">
<Column gap="4">
<Text.Body className={styles.inputLabel}>Name</Text.Body>

<Input placeholder="Unruggable" {...register('name')} />

<Box className={styles.errorContainer}>
{errors.name?.message ? <Text.Error>{errors.name.message}</Text.Error> : null}
</Box>
</Column>

<Column gap="4">
<Text.Body className={styles.inputLabel}>Symbol</Text.Body>

<Input placeholder="MEME" {...register('symbol')} />

<Box className={styles.errorContainer}>
{errors.symbol?.message ? <Text.Error>{errors.symbol.message}</Text.Error> : null}
</Box>
</Column>

<Column gap="4">
<Text.Body className={styles.inputLabel}>Initial Recipient Address</Text.Body>

<Input
placeholder="0x000000000000000000"
addon={
<IconButton
type="button"
disabled={!address}
onClick={() => (address ? setValue('initialRecipientAddress', address) : null)}
onClick={() =>
address ? setValue('initialRecipientAddress', address, { shouldValidate: true }) : null
}
>
<Wallet />
</IconButton>
}
{...register('initialRecipientAddress')}
/>

<Box className={styles.errorContainer}>
{errors.initialRecipientAddress?.message ? (
<Text.Error>{errors.initialRecipientAddress.message}</Text.Error>
Expand All @@ -165,27 +181,31 @@ export default function LaunchPage() {

<Column gap="4">
<Text.Body className={styles.inputLabel}>Owner Address</Text.Body>

<Input
placeholder="0x000000000000000000"
addon={
<IconButton
type="button"
disabled={!address}
onClick={() => (address ? setValue('ownerAddress', address) : null)}
onClick={() => (address ? setValue('ownerAddress', address, { shouldValidate: true }) : null)}
>
<Wallet />
</IconButton>
}
{...register('ownerAddress')}
/>

<Box className={styles.errorContainer}>
{errors.ownerAddress?.message ? <Text.Error>{errors.ownerAddress.message}</Text.Error> : null}
</Box>
</Column>

<Column gap="4">
<Text.Body className={styles.inputLabel}>Initial Supply</Text.Body>
<NumberInput placeholder="10,000,000,000.00" {...register('initialSupply', { valueAsNumber: true })} />

<NumericalInput placeholder="10,000,000,000.00" {...register('initialSupply', { valueAsNumber: true })} />

<Box className={styles.errorContainer}>
{errors.initialSupply?.message ? <Text.Error>{errors.initialSupply.message}</Text.Error> : null}
</Box>
Expand All @@ -194,45 +214,61 @@ export default function LaunchPage() {
{fields.map((field, index) => (
<Column gap="4" key={field.id}>
<Text.Body className={styles.inputLabel}>Holder {index + 1}</Text.Body>

<Column gap="2" flexDirection="row">
<Input placeholder="Holder address" {...register(`holders.${index}.address`)} />
<Input placeholder="Tokens" {...register(`holders.${index}.amount`, { valueAsNumber: true })} />

<NumericalInput
placeholder="Tokens"
{...register(`holders.${index}.amount`, { valueAsNumber: true })}
/>

<IconButton type="button" onClick={() => remove(index)}>
<X />
</IconButton>
</Column>

<Box className={styles.errorContainer}>
{errors.holders?.[index]?.address?.message ? (
<Text.Error>{errors.holders?.[index]?.address?.message}</Text.Error>
) : null}

{errors.holders?.[index]?.amount?.message ? (
<Text.Error>{errors.holders?.[index]?.amount?.message}</Text.Error>
) : null}
</Box>
</Column>
))}

<SecondaryButton disabled={fields.length >= MAX_HOLDERS} onClick={() => append({ address: '', amount: 0 })}>
Add holder
</SecondaryButton>
{fields.length < MAX_HOLDERS_PER_DEPLOYMENT && (
<SecondaryButton
type="button"
onClick={() => append({ address: '', amount: '' }, { shouldFocus: false })}
>
Add holder
</SecondaryButton>
)}

<div />

{deployedToken ? (
<Column gap="4">
<Text.HeadlineMedium textAlign="center">Token deployed!</Text.HeadlineMedium>

<Column gap="2">
<Text.Body className={styles.inputLabel}>Token address</Text.Body>
<Text.Body className={styles.deployedAddress}>
<a href={explorer.contract(deployedToken.address)}>{deployedToken.address}</a>.
</Text.Body>
</Column>

<Column gap="2">
<Text.Body className={styles.inputLabel}>Transaction</Text.Body>
<Text.Body className={styles.deployedAddress}>
<a href={explorer.transaction(deployedToken.tx)}>{deployedToken.tx}</a>.
</Text.Body>
</Column>

<PrimaryButton onClick={restart} type="button" className={styles.deployButton}>
Start over
</PrimaryButton>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/Launch/style.css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css'
import { sprinkles } from 'src/theme/css/sprinkles.css'
import { sprinkles, vars } from 'src/theme/css/sprinkles.css'

export const wrapper = style([
sprinkles({
Expand All @@ -20,6 +20,7 @@ export const wrapper = style([
export const container = style([
{
padding: '16px 12px 12px',
boxShadow: `0 0 16px ${vars.color.bg1}`,
},
sprinkles({
maxWidth: '480',
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/theme/css/sprinkles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const vars = createGlobalTheme(':root', {
error: '#ff003e',

bg1: '#06080A',
bg2: '#0D1114',
bg2: '#12181c',
bg3: '#191B1D',

border1: '#191B1D',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ export function shortenL2Address(address: string, chars = 4): string {
throw Error(`Invalid 'address' parameter '${address}'.`)
}
}

export function isValidL2Address(address: string): boolean {
// Wallets like to omit leading zeroes, so we cannot check for a fixed length.
// On the other hand, we don't want users to mistakenly enter an Ethereum address.
return /^0x[0-9a-fA-F]{50,64}$/.test(address)
}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


Loading