Compare commits

..

2 Commits

Author SHA1 Message Date
Joni Juup
d3b0fef406 optimize editor navigation by removing shortcuts on mobile, more space to tabs 2022-08-26 16:22:44 +03:00
Joni Juup
4cbc316c62 small mobile style fixes 2022-08-26 16:03:46 +03:00
48 changed files with 2525 additions and 5210 deletions

View File

@@ -5,8 +5,7 @@ GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build" NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000" NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c" NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com" NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com" NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com" NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
NEXT_PUBLIC_NETWORK_ID="21338" NEXT_PUBLIC_SITE_URL=http://localhost:3000
NEXT_PUBLIC_SITE_URL="http://localhost:3000"

4
.gitignore vendored
View File

@@ -33,7 +33,3 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.vscode .vscode
# yarn
.yarnrc.yml
.yarn/

View File

@@ -33,7 +33,6 @@ import { addFunds } from '../state/actions/addFaucetAccount'
import { deleteHook } from '../state/actions/deployHook' import { deleteHook } from '../state/actions/deployHook'
import { capitalize } from '../utils/helpers' import { capitalize } from '../utils/helpers'
import { deleteAccount } from '../state/actions/deleteAccount' import { deleteAccount } from '../state/actions/deleteAccount'
import { xrplSend } from '../state/actions/xrpl-client'
export const AccountDialog = ({ export const AccountDialog = ({
activeAccountAddress, activeAccountAddress,
@@ -302,7 +301,7 @@ const Accounts: FC<AccountProps> = props => {
const fetchAccInfo = async () => { const fetchAccInfo = async () => {
if (snap.clientStatus === 'online') { if (snap.clientStatus === 'online') {
const requests = snap.accounts.map(acc => const requests = snap.accounts.map(acc =>
xrplSend({ snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`, id: `hooks-builder-req-info-${acc.address}`,
command: 'account_info', command: 'account_info',
account: acc.address account: acc.address
@@ -330,7 +329,7 @@ const Accounts: FC<AccountProps> = props => {
} }
}) })
const objectRequests = snap.accounts.map(acc => { const objectRequests = snap.accounts.map(acc => {
return xrplSend({ return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`, id: `hooks-builder-req-objects-${acc.address}`,
command: 'account_objects', command: 'account_objects',
account: acc.address account: acc.address

View File

@@ -6,11 +6,14 @@ const ButtonGroup = styled('div', {
marginLeft: '1px', marginLeft: '1px',
[`& ${StyledButton}`]: { [`& ${StyledButton}`]: {
marginLeft: '-1px', marginLeft: '-1px',
px: '$4', px: '0.65rem',
zIndex: 2, zIndex: 2,
position: 'relative', position: 'relative',
'&:hover, &:focus': { '&:hover, &:focus': {
zIndex: 200 zIndex: 200
},
'@sm': {
px: '$4'
} }
}, },
[`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: { [`& ${StyledButton}:not(:only-of-type):not(:first-child):not(:last-child)`]: {

View File

@@ -15,7 +15,7 @@ const contentShow = keyframes({
'100%': { opacity: 1 } '100%': { opacity: 1 }
}) })
const StyledOverlay = styled(DialogPrimitive.Overlay, { const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 3000, zIndex: 10000,
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,
@@ -40,11 +40,10 @@ const StyledContent = styled(DialogPrimitive.Content, {
color: '$mauve12', color: '$mauve12',
borderRadius: '$md', borderRadius: '$md',
position: 'relative', position: 'relative',
mb: '15%', mb: '0%',
boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)', boxShadow: '0px 10px 38px -5px rgba(22, 23, 24, 0.25), 0px 10px 20px -5px rgba(22, 23, 24, 0.2)',
width: '90vw', width: '90vw',
maxWidth: '450px', maxWidth: '450px',
// maxHeight: "85vh",
padding: 25, padding: 25,
'@media (prefers-reduced-motion: no-preference)': { '@media (prefers-reduced-motion: no-preference)': {
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)` animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`
@@ -53,6 +52,9 @@ const StyledContent = styled(DialogPrimitive.Content, {
'.dark &': { '.dark &': {
backgroundColor: '$mauve5', backgroundColor: '$mauve5',
boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)' boxShadow: '0px 10px 38px 0px rgba(0, 0, 0, 0.85), 0px 10px 20px 0px rgba(0, 0, 0, 0.6)'
},
'@md': {
mb: '8%'
} }
}) })

View File

@@ -78,7 +78,6 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
return ( return (
<Flex css={{ flexShrink: 0, gap: '$0' }}> <Flex css={{ flexShrink: 0, gap: '$0' }}>
<Flex <Flex
id="kissa"
ref={scrollRef} ref={scrollRef}
css={{ css={{
overflowX: 'scroll', overflowX: 'scroll',
@@ -181,7 +180,21 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<Button outline size="sm" css={{ mr: '$3' }} onClick={() => setPopUp(true)}> <Button
outline
size="sm"
css={{
mr: '-1px',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
'@sm': {
borderTopRightRadius: '$sm',
borderBottomRightRadius: '$sm',
mr: '$3'
}
}}
onClick={() => setPopUp(true)}
>
<GithubLogo size="16px" /> Login <GithubLogo size="16px" /> Login
</Button> </Button>
)} )}
@@ -202,6 +215,9 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
alignSelf: 'flex-start', alignSelf: 'flex-start',
boxShadow: 'none' boxShadow: 'none'
}, },
'button:not(:last-child)': {
display: 'none'
},
'button:not(:first-child):not(:last-child)': { 'button:not(:first-child):not(:last-child)': {
borderRight: 0, borderRight: 0,
borderLeft: 0 borderLeft: 0
@@ -217,6 +233,11 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
'&:hover': { '&:hover': {
boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12' boxShadow: 'inset 0px 0px 0px 1px $colors$mauve12'
} }
},
'@sm': {
'button:not(:last-child)': {
display: 'flex'
}
} }
}} }}
> >

View File

@@ -200,15 +200,15 @@ const HooksEditor = () => {
defaultValue={file?.content} defaultValue={file?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save? // onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => { beforeMount={monaco => {
// if (!snap.editorCtx) { if (!snap.editorCtx) {
// snap.files.forEach(file => snap.files.forEach(file =>
// monaco.editor.createModel( monaco.editor.createModel(
// file.content, file.content,
// file.language, file.language,
// monaco.Uri.parse(`file:///work/c/${file.name}`) monaco.Uri.parse(`file:///work/c/${file.name}`)
// ) )
// ) )
// } }
// create the web socket // create the web socket
if (!subscriptionRef.current) { if (!subscriptionRef.current) {
@@ -218,11 +218,6 @@ const HooksEditor = () => {
aliases: ['C', 'c', 'H', 'h'], aliases: ['C', 'c', 'H', 'h'],
mimetypes: ['text/plain'] mimetypes: ['text/plain']
}) })
monaco.languages.register({
id: 'text',
extensions: ['.txt'],
mimetypes: ['text/plain'],
})
MonacoServices.install(monaco) MonacoServices.install(monaco)
const webSocket = createWebSocket( const webSocket = createWebSocket(
process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || '' process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''

View File

@@ -33,7 +33,7 @@ import { styled } from '../stitches.config'
const ImageWrapper = styled(Flex, { const ImageWrapper = styled(Flex, {
position: 'relative', position: 'relative',
mt: '$2', mt: '$2',
mb: '$10', mb: '$6',
svg: { svg: {
// fill: "red", // fill: "red",
'.angle': { '.angle': {
@@ -66,16 +66,23 @@ const Navigation = () => {
<Container <Container
css={{ css={{
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
px: '$3',
'@sm': {
px: '$4'
}
}} }}
> >
<Flex <Flex
css={{ css={{
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
borderRight: '1px solid $colors$mauve6',
py: '$3', py: '$3',
pr: '$4' pr: '$0',
'@sm': {
borderRight: '1px solid $colors$mauve6',
pr: '$4'
}
}} }}
> >
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref> <Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
@@ -84,7 +91,11 @@ const Navigation = () => {
css={{ css={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
color: '$textColor' color: '$textColor',
mr: '$2',
'@sm': {
mr: '$4'
}
}} }}
> >
<Logo width="32px" height="32px" /> <Logo width="32px" height="32px" />
@@ -92,9 +103,12 @@ const Navigation = () => {
</Link> </Link>
<Flex <Flex
css={{ css={{
ml: '$5',
flexDirection: 'column', flexDirection: 'column',
gap: '1px' gap: '1px',
display: 'none',
'@md': {
display: 'flex'
}
}} }}
> >
{snap.loading ? ( {snap.loading ? (
@@ -135,8 +149,8 @@ const Navigation = () => {
css={{ css={{
display: 'flex', display: 'flex',
maxWidth: '1080px', maxWidth: '1080px',
width: '80vw', width: '90vw',
maxHeight: '80%', maxHeight: '90%',
backgroundColor: '$mauve1 !important', backgroundColor: '$mauve1 !important',
overflowY: 'auto', overflowY: 'auto',
background: 'black', background: 'black',
@@ -265,15 +279,18 @@ const Navigation = () => {
gridTemplateColumns: '1fr', gridTemplateColumns: '1fr',
gridTemplateRows: 'max-content', gridTemplateRows: 'max-content',
flex: 1, flex: 1,
p: '$7', p: '$4',
pb: '$16', pb: '$8',
gap: '$3', gap: '$3',
alignItems: 'normal', alignItems: 'normal',
flexWrap: 'wrap', flexWrap: 'wrap',
backgroundColor: '$mauve1', backgroundColor: '$mauve1',
'@md': { '@md': {
gridTemplateColumns: '1fr 1fr', gridTemplateColumns: '1fr 1fr',
gridTemplateRows: 'max-content' gridTemplateRows: 'max-content',
p: '$7',
pb: '$10',
paddingRight: '$12'
}, },
'@lg': { '@lg': {
gridTemplateColumns: '1fr 1fr 1fr', gridTemplateColumns: '1fr 1fr 1fr',
@@ -316,23 +333,30 @@ const Navigation = () => {
<Flex <Flex
css={{ css={{
flexWrap: 'nowrap', flexWrap: 'nowrap',
marginLeft: '$4', marginLeft: '$2',
overflowX: 'scroll', overflowX: 'scroll',
'&::-webkit-scrollbar': { '&::-webkit-scrollbar': {
height: 0, height: 0,
background: 'transparent' background: 'transparent'
}, },
scrollbarColor: 'transparent', scrollbarColor: 'transparent',
scrollbarWidth: 'none' scrollbarWidth: 'none',
'@sm': {
marginLeft: '$4'
}
}} }}
> >
<Stack <Stack
css={{ css={{
ml: '$4', ml: '$0',
gap: '$3', gap: '$2',
flexWrap: 'nowrap', flexWrap: 'nowrap',
alignItems: 'center', alignItems: 'center',
marginLeft: 'auto' marginLeft: 'auto',
'@sm': {
marginLeft: '$4',
gap: '$3'
}
}} }}
> >
<ButtonGroup> <ButtonGroup>

View File

@@ -7,7 +7,7 @@ const PanelBox = styled('div', {
flexDirection: 'column', flexDirection: 'column',
border: '1px solid $colors$mauve6', border: '1px solid $colors$mauve6',
backgroundColor: '$mauve2', backgroundColor: '$mauve2',
padding: '$3', padding: '$5',
borderRadius: '$sm', borderRadius: '$sm',
fontWeight: 'lighter', fontWeight: 'lighter',
height: 'auto', height: 'auto',

View File

@@ -21,7 +21,7 @@ import { saveFile } from '../../state/actions/saveFile'
import { getErrors, getTags } from '../../utils/comment-parser' import { getErrors, getTags } from '../../utils/comment-parser'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
const generateHtmlTemplate = async (code: string, data?: Record<string, any>) => { const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
let processString: string | undefined let processString: string | undefined
const process = { env: { NODE_ENV: 'production' } } as any const process = { env: { NODE_ENV: 'production' } } as any
if (data) { if (data) {
@@ -29,10 +29,8 @@ const generateHtmlTemplate = async (code: string, data?: Record<string, any>) =>
process.env[key] = data[key] process.env[key] = data[key]
}) })
} }
processString = JSON.stringify(process) processString = JSON.stringify(process)
const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
return ` return `
<html> <html>
<head> <head>
@@ -74,9 +72,7 @@ const generateHtmlTemplate = async (code: string, data?: Record<string, any>) =>
window.addEventListener('error', windowErrorHandler); window.addEventListener('error', windowErrorHandler);
</script> </script>
<script>
${libs}
</script>
<script type="module"> <script type="module">
${code} ${code}
</script> </script>
@@ -104,7 +100,6 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const [fields, setFields] = useState<Fields>({}) const [fields, setFields] = useState<Fields>({})
const [iFrameCode, setIframeCode] = useState('') const [iFrameCode, setIframeCode] = useState('')
const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const getFields = useCallback(() => { const getFields = useCallback(() => {
const inputTags = ['input', 'param', 'arg', 'argument'] const inputTags = ['input', 'param', 'arg', 'argument']
@@ -132,31 +127,16 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
return fields return fields
}, [content]) }, [content])
const runScript = useCallback(async () => { const runScript = useCallback(() => {
setIsLoading(true);
try { try {
// Show loading toast only after 1 second, otherwise skip it.
let loaded = false
let toastId: string | undefined;
setTimeout(() => {
if (!loaded) {
toastId = toast.loading('Loading packages, may take a few seconds...', {
position: 'bottom-center',
})
}
}, 1000)
let data: any = {} let data: any = {}
Object.keys(fields).forEach(key => { Object.keys(fields).forEach(key => {
data[key] = fields[key].value data[key] = fields[key].value
}) })
const template = await generateHtmlTemplate(content, data) const template = generateHtmlTemplate(content, data)
setIframeCode(template) setIframeCode(template)
loaded = true
if (toastId) {
toast.dismiss(toastId)
}
state.scriptLogs = [{ type: 'success', message: 'Started running...' }] state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
} catch (err) { } catch (err) {
state.scriptLogs = [ state.scriptLogs = [
@@ -165,7 +145,6 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
{ type: 'error', message: err?.message || 'Could not parse template' } { type: 'error', message: err?.message || 'Could not parse template' }
] ]
} }
setIsLoading(false);
}, [content, fields, snap.scriptLogs]) }, [content, fields, snap.scriptLogs])
useEffect(() => { useEffect(() => {
@@ -195,11 +174,11 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const isDisabled = Object.values(fields).some(field => field.required && !field.value) const isDisabled = Object.values(fields).some(field => field.required && !field.value)
const handleRun = useCallback(async () => { const handleRun = useCallback(() => {
if (isDisabled) return toast.error('Please fill in all the required fields.') if (isDisabled) return toast.error('Please fill in all the required fields.')
state.scriptLogs = [] state.scriptLogs = []
await runScript(); runScript()
setIsDialogOpen(false) setIsDialogOpen(false)
}, [isDisabled, runScript]) }, [isDisabled, runScript])
@@ -300,7 +279,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
<DialogClose asChild> <DialogClose asChild>
<Button outline>Cancel</Button> <Button outline>Cancel</Button>
</DialogClose> </DialogClose>
<Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}> <Button variant="primary" isDisabled={isDisabled} onClick={handleRun}>
Run script Run script
</Button> </Button>
</Flex> </Flex>

View File

@@ -3,11 +3,13 @@ import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import { styled } from '../stitches.config' import { styled } from '../stitches.config'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import type { Props, StylesConfig } from 'react-select' import type { Props } from 'react-select'
const SelectInput = dynamic(() => import('react-select'), { ssr: false }) const SelectInput = dynamic(() => import('react-select'), { ssr: false })
const CreatableSelectInput = dynamic(() => import('react-select/creatable'), { ssr: false })
const getColors = (isDark: boolean) => { // eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const colors: any = { const colors: any = {
// primary: pink.pink9, // primary: pink.pink9,
active: isDark ? purpleDark.purple9 : purple.purple9, active: isDark ? purpleDark.purple9 : purple.purple9,
@@ -28,136 +30,93 @@ const getColors = (isDark: boolean) => {
} }
colors.outline = colors.background colors.outline = colors.background
colors.selected = colors.secondary colors.selected = colors.secondary
return colors
}
const getStyles = (isDark: boolean) => {
const colors = getColors(isDark)
const styles: StylesConfig = {
container: provided => {
return {
...provided,
position: 'relative',
width: '100%'
}
},
singleValue: provided => ({
...provided,
color: colors.mauve12
}),
menu: provided => ({
...provided,
backgroundColor: colors.dropDownBg
}),
control: (provided, state) => {
return {
...provided,
minHeight: 0,
border: '0px',
backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}`
}
},
input: provided => {
return {
...provided,
color: '$text'
}
},
multiValue: provided => {
return {
...provided,
backgroundColor: colors.mauve8
}
},
multiValueLabel: provided => {
return {
...provided,
color: colors.mauve12
}
},
multiValueRemove: provided => {
return {
...provided,
':hover': {
background: colors.mauve9
}
}
},
option: (provided, state) => {
return {
...provided,
color: colors.searchText,
backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg,
':hover': {
backgroundColor: colors.active,
color: '#ffffff'
},
':selected': {
backgroundColor: 'red'
}
}
},
indicatorSeparator: provided => {
return {
...provided,
backgroundColor: colors.secondary
}
},
dropdownIndicator: (provided, state) => {
return {
...provided,
padding: 6,
color: state.isFocused ? colors.border : colors.secondary,
':hover': {
color: colors.border
}
}
},
clearIndicator: provided => {
return {
...provided,
padding: 6,
color: colors.secondary,
':hover': {
color: colors.border
}
}
}
}
return styles
}
// eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return ( return (
<SelectInput <SelectInput
ref={ref} ref={ref}
menuPosition={props.menuPosition || 'fixed'} menuPosition={props.menuPosition || 'fixed'}
styles={styles} styles={{
{...props} container: provided => {
/> return {
) ...provided,
}) position: 'relative'
}
// eslint-disable-next-line react/display-name },
const Creatable = forwardRef<any, Props>((props, ref) => { singleValue: provided => ({
const { theme } = useTheme() ...provided,
const isDark = theme === 'dark' color: colors.mauve12
const styles = getStyles(isDark) }),
return ( menu: provided => ({
<CreatableSelectInput ...provided,
ref={ref} backgroundColor: colors.dropDownBg
formatCreateLabel={label => `Enter "${label}"`} }),
menuPosition={props.menuPosition || 'fixed'} control: (provided, state) => {
styles={styles} return {
...provided,
minHeight: 0,
border: '0px',
backgroundColor: colors.mauve4,
boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}`
}
},
input: provided => {
return {
...provided,
color: '$text'
}
},
multiValue: provided => {
return {
...provided,
backgroundColor: colors.mauve8
}
},
multiValueLabel: provided => {
return {
...provided,
color: colors.mauve12
}
},
multiValueRemove: provided => {
return {
...provided,
':hover': {
background: colors.mauve9
}
}
},
option: (provided, state) => {
return {
...provided,
color: colors.searchText,
backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg,
':hover': {
backgroundColor: colors.active,
color: '#ffffff'
},
':selected': {
backgroundColor: 'red'
}
}
},
indicatorSeparator: provided => {
return {
...provided,
backgroundColor: colors.secondary
}
},
dropdownIndicator: (provided, state) => {
return {
...provided,
color: state.isFocused ? colors.border : colors.secondary,
':hover': {
color: colors.border
}
}
}
}}
{...props} {...props}
/> />
) )
}) })
export default styled(Select, {}) export default styled(Select, {})
export const CreatableSelect = styled(Creatable, {})

View File

@@ -1,71 +0,0 @@
import { FC, useCallback, useState } from 'react'
import state from '../state'
import { Flex, Input, Button } from '.'
import fetchAccountInfo from '../utils/accountInfo'
import { useSnapshot } from 'valtio'
interface AccountSequenceProps {
address?: string
}
const AccountSequence: FC<AccountSequenceProps> = ({ address }) => {
const { accounts } = useSnapshot(state)
const account = accounts.find(acc => acc.address === address)
const [isLoading, setIsLoading] = useState(false)
const setSequence = useCallback(
(sequence: number) => {
const acc = state.accounts.find(acc => acc.address == address)
if (!acc) return
acc.sequence = sequence
},
[address]
)
const handleUpdateSequence = useCallback(
async (silent?: boolean) => {
if (!account) return
setIsLoading(true)
const info = await fetchAccountInfo(account.address, { silent })
if (info) {
setSequence(info.Sequence)
}
setIsLoading(false)
},
[account, setSequence]
)
const disabled = !account
return (
<Flex row align="center" fluid>
<Input
placeholder="Account sequence"
value={account?.sequence || ""}
disabled={!account}
type="number"
readOnly={true}
/>
<Button
size="xs"
variant="primary"
type="button"
outline
disabled={disabled}
isDisabled={disabled}
isLoading={isLoading}
css={{
background: '$backgroundAlt',
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={() => handleUpdateSequence()}
>
Update
</Button>
</Flex>
)
}
export default AccountSequence

View File

@@ -21,7 +21,6 @@ import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
import estimateFee from '../utils/estimateFee' import estimateFee from '../utils/estimateFee'
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook' import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
import { capitalize } from '../utils/helpers' import { capitalize } from '../utils/helpers'
import AccountSequence from './Sequence'
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => { ({ accountAddress }) => {
@@ -191,10 +190,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
onChange={(acc: any) => setSelectedAccount(acc)} onChange={(acc: any) => setSelectedAccount(acc)}
/> />
</Box> </Box>
<Box css={{ width: '100%', position: 'relative' }}>
<Label>Sequence</Label>
<AccountSequence address={selectedAccount?.value} />
</Box>
<Box css={{ width: '100%' }}> <Box css={{ width: '100%' }}>
<Label>Invoke on transactions</Label> <Label>Invoke on transactions</Label>
<Controller <Controller

View File

@@ -11,7 +11,7 @@ import {
} from './Dialog' } from './Dialog'
import { Plus, X } from 'phosphor-react' import { Plus, X } from 'phosphor-react'
import { styled } from '../stitches.config' import { styled } from '../stitches.config'
import { capitalize, getFileExtention } from '../utils/helpers' import { capitalize } from '../utils/helpers'
import ContextMenu, { ContentMenuOption } from './ContextMenu' import ContextMenu, { ContentMenuOption } from './ContextMenu'
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
@@ -97,7 +97,7 @@ export const Tabs = ({
if (!tabname) { if (!tabname) {
return { error: `Please enter ${label.toLocaleLowerCase()} name.` } return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
} }
let ext = getFileExtention(tabname) let ext = (tabname.includes('.') && tabname.split('.').pop()) || ''
if (!ext && defaultExtension) { if (!ext && defaultExtension) {
ext = defaultExtension ext = defaultExtension
@@ -109,7 +109,7 @@ export const Tabs = ({
if (extensionRequired && !ext) { if (extensionRequired && !ext) {
return { error: 'File extension is required!' } return { error: 'File extension is required!' }
} }
if (allowedExtensions && ext && !allowedExtensions.includes(ext)) { if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: 'This file extension is not allowed!' } return { error: 'This file extension is not allowed!' }
} }
if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) { if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {

View File

@@ -19,8 +19,6 @@ import { TxJson } from './json'
import { TxUI } from './ui' import { TxUI } from './ui'
import { default as _estimateFee } from '../../utils/estimateFee' import { default as _estimateFee } from '../../utils/estimateFee'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
import { SetHookData, toHex } from '../../utils/setHook'
export interface TransactionProps { export interface TransactionProps {
header: string header: string
@@ -41,40 +39,17 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => { (state: Partial<TransactionState> = txState) => {
const { const { selectedTransaction, selectedDestAccount, selectedAccount, txFields } = state
selectedTransaction,
selectedAccount,
txFields,
selectedFlags,
hookParameters,
memos
} = state
const TransactionType = selectedTransaction?.value || null const TransactionType = selectedTransaction?.value || null
const Destination = selectedDestAccount?.value || txFields?.Destination
const Account = selectedAccount?.value || null const Account = selectedAccount?.value || null
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
const HookParameters = Object.entries(hookParameters || {}).reduce<
SetHookData['HookParameters']
>((acc, [_, { label, value }]) => {
return acc.concat({
HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({ return prepareTransaction({
...txFields, ...txFields,
HookParameters,
Flags,
TransactionType, TransactionType,
Account, Destination,
Memos Account
}) })
}, },
[txState] [txState]
@@ -90,29 +65,15 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} }
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]) }, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const saveEditorState = useCallback(
(value: string = '', transactionType?: string) => {
const pTx = prepareState(value, transactionType)
if (pTx) {
pTx.editorValue = getJsonString(pTx)
return setState(pTx)
}
},
[getJsonString, setState]
)
const submitTest = useCallback(async () => { const submitTest = useCallback(async () => {
let st: TransactionState | undefined let st: TransactionState | undefined
const tt = txState.selectedTransaction?.value const tt = txState.selectedTransaction?.value
if (viewType === 'json') { if (viewType === 'json') {
st = saveEditorState(editorValue, tt) // save the editor state first
if (!st) return const pst = prepareState(editorValue || '', tt)
if (!pst) return
st = setState(pst)
} }
const account = accounts.find(acc => acc.address === selectedAccount?.value) const account = accounts.find(acc => acc.address === selectedAccount?.value)
@@ -125,12 +86,11 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
throw Error('Account must be selected from imported accounts!') throw Error('Account must be selected from imported accounts!')
} }
const options = prepareOptions(st) const options = prepareOptions(st)
// delete unnecessary fields
Object.keys(options).forEach(field => { const fields = getTxFields(options.TransactionType)
if (!options[field]) { if (fields.Destination && !options.Destination) {
delete options[field] throw Error('Destination account is required!')
} }
})
await sendTransaction(account, options, { logPrefix }) await sendTransaction(account, options, { logPrefix })
} catch (error) { } catch (error) {
@@ -144,18 +104,23 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} }
setState({ txIsLoading: false }) setState({ txIsLoading: false })
}, [ }, [
txState.selectedTransaction?.value,
viewType, viewType,
accounts, accounts,
txIsDisabled, txIsDisabled,
setState, setState,
header, header,
saveEditorState,
editorValue, editorValue,
txState,
selectedAccount?.value, selectedAccount?.value,
prepareOptions prepareOptions
]) ])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const resetState = useCallback( const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => { (transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value) const fields = getTxFields(transactionType?.value)
@@ -165,12 +130,14 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
selectedTransaction: transactionType selectedTransaction: transactionType
} }
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) { if (fields.Destination !== undefined) {
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags) nwState.selectedDestAccount = null
fields.Flags = undefined fields.Destination = ''
} else {
fields.Destination = undefined
} }
nwState.txFields = fields nwState.txFields = fields
const state = modifyTxState(header, nwState, { replaceState: true }) const state = modifyTxState(header, nwState, { replaceState: true })
const editorValue = getJsonString(state) const editorValue = getJsonString(state)
return setState({ editorValue }) return setState({ editorValue })
@@ -201,51 +168,40 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
[accounts, prepareOptions, setState, txState] [accounts, prepareOptions, setState, txState]
) )
const switchToJson = useCallback(() => {
const editorValue = getJsonString()
setState({ viewType: 'json', editorValue })
}, [getJsonString, setState])
const switchToUI = useCallback(() => {
setState({ viewType: 'ui' })
}, [setState])
return ( return (
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}> <Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === 'json' ? ( {viewType === 'json' ? (
<TxJson <TxJson
getJsonString={getJsonString} getJsonString={getJsonString}
saveEditorState={saveEditorState}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
estimateFee={estimateFee} estimateFee={estimateFee}
/> />
) : ( ) : (
<TxUI <TxUI state={txState} setState={setState} estimateFee={estimateFee} />
switchToJson={switchToJson}
state={txState}
resetState={resetState}
setState={setState}
estimateFee={estimateFee}
/>
)} )}
<Flex <Flex
row row
css={{ css={{
justifyContent: 'space-between', justifyContent: 'space-between',
position: 'absolute',
left: 0,
bottom: 0,
width: '100%', width: '100%',
mb: '$1' mb: '$2',
mt: '$1',
'@md': {
position: 'absolute',
left: 0,
bottom: 0,
mt: '$0',
mb: '$2'
}
}} }}
> >
<Button <Button
onClick={() => { onClick={() => {
if (viewType === 'ui') { if (viewType === 'ui') {
switchToJson() setState({ viewType: 'json' })
} else switchToUI() } else setState({ viewType: 'ui' })
}} }}
outline outline
> >

View File

@@ -1,6 +1,6 @@
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import state, { transactionsData, TransactionState } from '../../state' import state, { prepareState, transactionsData, TransactionState } from '../../state'
import Text from '../Text' import Text from '../Text'
import { Flex, Link } from '..' import { Flex, Link } from '..'
import { showAlert } from '../../state/actions/showAlert' import { showAlert } from '../../state/actions/showAlert'
@@ -11,27 +11,27 @@ import Monaco from '../Monaco'
import type monaco from 'monaco-editor' import type monaco from 'monaco-editor'
interface JsonProps { interface JsonProps {
getJsonString: (st?: Partial<TransactionState>) => string getJsonString?: (state?: Partial<TransactionState>) => string
saveEditorState: (val?: string, tt?: string) => TransactionState | undefined
header?: string header?: string
setState: (pTx?: Partial<TransactionState> | undefined) => void setState: (pTx?: Partial<TransactionState> | undefined) => void
state: TransactionState state: TransactionState
estimateFee?: () => Promise<string | undefined> estimateFee?: () => Promise<string | undefined>
} }
export const TxJson: FC<JsonProps> = ({ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, setState }) => {
getJsonString,
state: txState,
header,
setState,
saveEditorState
}) => {
const { editorSettings, accounts } = useSnapshot(state) const { editorSettings, accounts } = useSnapshot(state)
const { editorValue, estimatedFee, editorIsSaved } = txState const { editorValue, estimatedFee } = txState
const [currTxType, setCurrTxType] = useState<string | undefined>( const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value txState.selectedTransaction?.value
) )
useEffect(() => {
setState({
editorValue: getJsonString?.()
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => { useEffect(() => {
const parsed = parseJSON(editorValue) const parsed = parseJSON(editorValue)
if (!parsed) return if (!parsed) return
@@ -44,19 +44,29 @@ export const TxJson: FC<JsonProps> = ({
} }
}, [editorValue]) }, [editorValue])
const saveState = (value: string, transactionType?: string) => {
const tx = prepareState(value, transactionType)
if (tx) {
setState(tx)
setState({
editorValue: getJsonString?.(tx)
})
}
}
const discardChanges = () => { const discardChanges = () => {
showAlert('Confirm', { showAlert('Confirm', {
body: 'Are you sure to discard these changes?', body: 'Are you sure to discard these changes?',
confirmText: 'Yes', confirmText: 'Yes',
onCancel: () => {}, onCancel: () => {},
onConfirm: () => setState({ editorValue: getJsonString() }) onConfirm: () => setState({ editorValue: getJsonString?.() })
}) })
} }
const onExit = (value: string) => { const onExit = (value: string) => {
const options = parseJSON(value) const options = parseJSON(value)
if (options) { if (options) {
saveEditorState(value, currTxType) saveState(value, currTxType)
return return
} }
showAlert('Error!', { showAlert('Error!', {
@@ -153,6 +163,8 @@ export const TxJson: FC<JsonProps> = ({
}) })
}, [getSchemas, monacoInst]) }, [getSchemas, monacoInst])
const hasUnsaved = useMemo(() => editorValue !== getJsonString?.(), [editorValue, getJsonString])
return ( return (
<Monaco <Monaco
rootProps={{ rootProps={{
@@ -162,7 +174,7 @@ export const TxJson: FC<JsonProps> = ({
id={header} id={header}
height="100%" height="100%"
value={editorValue} value={editorValue}
onChange={val => setState({ editorValue: val, editorIsSaved: false })} onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
editor.updateOptions({ editor.updateOptions({
minimap: { enabled: false }, minimap: { enabled: false },
@@ -178,12 +190,12 @@ export const TxJson: FC<JsonProps> = ({
model?.onWillDispose(() => onExit(model.getValue())) model?.onWillDispose(() => onExit(model.getValue()))
}} }}
overlay={ overlay={
!editorIsSaved ? ( hasUnsaved ? (
<Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}> <Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
<Text muted small> <Text muted small>
This file has unsaved changes. This file has unsaved changes.
</Text> </Text>
<Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}> <Link css={{ ml: '$1' }} onClick={() => saveState(editorValue || '', currTxType)}>
save save
</Link> </Link>
<Link css={{ ml: '$1' }} onClick={discardChanges}> <Link css={{ ml: '$1' }} onClick={discardChanges}>

View File

@@ -1,59 +1,62 @@
import { FC, ReactNode, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import Container from '../Container' import Container from '../Container'
import Flex from '../Flex' import Flex from '../Flex'
import Input from '../Input' import Input from '../Input'
import Select, { CreatableSelect } from '../Select' import Select from '../Select'
import Text from '../Text' import Text from '../Text'
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsOptions, transactionsOptions,
TxFields, TxFields,
getTxFields,
defaultTransactionType defaultTransactionType
} from '../../state/transactions' } from '../../state/transactions'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import state from '../../state' import state from '../../state'
import { streamState } from '../DebugStream' import { streamState } from '../DebugStream'
import { Box, Button } from '..' import { Button } from '..'
import Textarea from '../Textarea' import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
import AccountSequence from '../Sequence'
import { capitalize, typeIs } from '../../utils/helpers'
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined> estimateFee?: (...arg: any) => Promise<string | undefined>
switchToJson: () => void
} }
export const TxUI: FC<UIProps> = ({ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) => {
state: txState,
setState,
resetState,
estimateFee,
switchToJson
}) => {
const { accounts } = useSnapshot(state) const { accounts } = useSnapshot(state)
const { selectedAccount, selectedTransaction, txFields, selectedFlags, hookParameters, memos } = const { selectedAccount, selectedDestAccount, selectedTransaction, txFields } = txState
txState
const accountOptions: SelectOption[] = accounts.map(acc => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name, label: acc.name,
value: acc.address value: acc.address
})) }))
const flagsOptions: SelectOption[] = Object.entries( const destAccountOptions: SelectOption[] = accounts
getFlags(selectedTransaction?.value) || {} .map(acc => ({
).map(([label, value]) => ({ label: acc.name,
label, value: acc.address
value }))
})) .filter(acc => acc.value !== selectedAccount?.value)
const [feeLoading, setFeeLoading] = useState(false) const [feeLoading, setFeeLoading] = useState(false)
const resetFields = useCallback(
(tt: string) => {
const fields = getTxFields(tt)
if (fields.Destination !== undefined) {
setState({ selectedDestAccount: null })
fields.Destination = ''
} else {
fields.Destination = undefined
}
return setState({ txFields: fields })
},
[setState]
)
const handleSetAccount = (acc: SelectOption) => { const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc }) setState({ selectedAccount: acc })
streamState.selectedAccount = acc streamState.selectedAccount = acc
@@ -73,22 +76,6 @@ export const TxUI: FC<UIProps> = ({
[setState, txFields] [setState, txFields]
) )
const setRawField = useCallback(
(field: keyof TxFields, type: string, value: any) => {
// TODO $type should be a narrowed type
setState({
txFields: {
...txFields,
[field]: {
$type: type,
$value: value
}
}
})
},
[setState, txFields]
)
const handleEstimateFee = useCallback( const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => { async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true) setFeeLoading(true)
@@ -105,13 +92,15 @@ export const TxUI: FC<UIProps> = ({
(tt: SelectOption) => { (tt: SelectOption) => {
setState({ selectedTransaction: tt }) setState({ selectedTransaction: tt })
const newState = resetState(tt) const newState = resetFields(tt.value)
handleEstimateFee(newState, true) handleEstimateFee(newState, true)
}, },
[handleEstimateFee, resetState, setState] [handleEstimateFee, resetFields, setState]
) )
const switchToJson = () => setState({ viewType: 'json' })
// default tx // default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return if (selectedTransaction?.value) return
@@ -121,23 +110,20 @@ export const TxUI: FC<UIProps> = ({
} }
}, [handleChangeTxType, selectedTransaction?.value]) }, [handleChangeTxType, selectedTransaction?.value])
const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos'] const fields = useMemo(
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
)
if (flagsOptions.length) { const specialFields = ['TransactionType', 'Account']
richFields.push('Flags') if (fields.Destination !== undefined) {
specialFields.push('Destination')
} }
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields] const otherFields = Object.keys(txFields).filter(k => !specialFields.includes(k)) as [
const amountOptions = [ keyof TxFields
{ label: 'XRP', value: 'xrp' }, ]
{ label: 'Token', value: 'token' }
] as const
const defaultTokenAmount = {
value: '0',
currency: '',
issuer: ''
}
return ( return (
<Container <Container
css={{ css={{
@@ -147,437 +133,222 @@ export const TxUI: FC<UIProps> = ({
}} }}
> >
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}> <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<TxField label="Transaction type"> <Flex
row
fluid
css={{
justifyContent: 'space-between',
alignItems: 'center',
mb: '$3',
mt: '1px',
pr: '1px',
'@xl': {
justifyContent: 'flex-end'
}
}}
>
<Text muted css={{ mr: '$3' }}>
Transaction type:{' '}
</Text>
<Select <Select
instanceId="transactionsType" instanceId="transactionsType"
placeholder="Select transaction type" placeholder="Select transaction type"
options={transactionsOptions} options={transactionsOptions}
hideSelectedOptions hideSelectedOptions
css={{
width: '60%',
minWidth: '200px',
'@lg': {
width: '70%'
}
}}
value={selectedTransaction} value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)} onChange={(tt: any) => handleChangeTxType(tt)}
/> />
</TxField> </Flex>
<TxField label="Account"> <Flex
row
fluid
css={{
justifyContent: 'space-between',
alignItems: 'center',
mb: '$3',
pr: '1px',
'@xl': {
justifyContent: 'flex-end'
}
}}
>
<Text muted css={{ mr: '$3' }}>
Account:{' '}
</Text>
<Select <Select
instanceId="from-account" instanceId="from-account"
placeholder="Select your account" placeholder="Select your account"
css={{
width: '60%',
minWidth: '200px',
'@lg': {
width: '70%'
}
}}
options={accountOptions} options={accountOptions}
value={selectedAccount} value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/> />
</TxField> </Flex>
<TxField label="Sequence"> {fields.Destination !== undefined && (
<AccountSequence address={selectedAccount?.value} /> <Flex
</TxField> row
{richFields.includes('Flags') && ( fluid
<TxField label="Flags"> css={{
<Select justifyContent: 'space-between',
isClearable alignItems: 'center',
instanceId="flags" mb: '$3',
placeholder="Select flags to apply" pr: '1px',
menuPosition="fixed" '@xl': {
value={selectedFlags} justifyContent: 'flex-end'
isMulti
options={flagsOptions}
onChange={flags => setState({ selectedFlags: flags as any })}
closeMenuOnSelect={
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
} }
}}
>
<Text muted css={{ mr: '$3' }}>
Destination account:{' '}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{
width: '60%',
minWidth: '200px',
'@lg': {
width: '70%'
}
}}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/> />
</TxField> </Flex>
)} )}
{otherFields.map(field => { {otherFields.map(field => {
let _value = txFields[field] let _value = txFields[field]
let value: string | undefined let value: string | undefined
if (typeIs(_value, 'object')) { if (typeof _value === 'object') {
if (_value.$type === 'json' && typeIs(_value.$value, ['object', 'array'])) { if (_value.$type === 'json' && typeof _value.$value === 'object') {
value = JSON.stringify(_value.$value, null, 2) value = JSON.stringify(_value.$value, null, 2)
} else { } else {
value = _value.$value?.toString() value = _value.$value.toString()
} }
} else { } else {
value = _value?.toString() value = _value?.toString()
} }
const isAccount = typeIs(_value, 'object') && _value.$type === 'account' const isXrp = typeof _value === 'object' && _value.$type === 'xrp'
const isXrpAmount = typeIs(_value, 'object') && _value.$type === 'amount.xrp'
const isTokenAmount = typeIs(_value, 'object') && _value.$type === 'amount.token'
const isJson = typeof _value === 'object' && _value.$type === 'json' const isJson = typeof _value === 'object' && _value.$type === 'json'
const isFee = field === 'Fee' const isFee = field === 'Fee'
let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
if (rows && rows > 5) rows = 5 if (rows && rows > 5) rows = 5
let tokenAmount = defaultTokenAmount
if (isTokenAmount && typeIs(_value, 'object') && typeIs(_value.$value, 'object')) {
tokenAmount = {
value: _value.$value.value,
currency: _value.$value.currency,
issuer: _value.$value.issuer
}
}
if (isXrpAmount || isTokenAmount) {
return (
<TxField key={field} label={field}>
<Flex fluid css={{ alignItems: 'center' }}>
{isTokenAmount ? (
<Flex
fluid
row
align="center"
justify="space-between"
css={{ position: 'relative' }}
>
{/* <Input
type="text"
placeholder="Issuer"
value={tokenAmount.issuer}
onChange={e =>
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: e.target.value
})
}
/> */}
<Input
type="text"
value={tokenAmount.currency}
placeholder="Currency"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
currency: e.target.value
})
}}
/>
<Input
css={{ mx: '$1' }}
type="number"
value={tokenAmount.value}
placeholder="Value"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
value: e.target.value
})
}}
/>
<Box css={{ width: '50%' }}>
<CreatableAccount
value={tokenAmount.issuer}
field={'Issuer' as any}
placeholder="Issuer"
setField={(_, value = '') => {
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: value
})
}}
/>
</Box>
</Flex>
) : (
<Input
css={{ flex: 'inherit' }}
type="number"
value={value}
onChange={e => handleSetField(field, e.target.value)}
/>
)}
<Box
css={{
ml: '$2',
width: '150px'
}}
>
<Select
instanceId="currency-type"
options={amountOptions}
value={isXrpAmount ? amountOptions['0'] : amountOptions['1']}
onChange={(e: any) => {
const opt = e as typeof amountOptions[number]
if (opt.value === 'xrp') {
setRawField(field, 'amount.xrp', '0')
} else {
setRawField(field, 'amount.token', defaultTokenAmount)
}
}}
/>
</Box>
</Flex>
</TxField>
)
}
if (isAccount) {
return (
<TxField key={field} label={field}>
<CreatableAccount value={value} field={field} setField={handleSetField} />
</TxField>
)
}
return ( return (
<TxField key={field} label={field}> <Flex column key={field} css={{ mb: '$2', pr: '1px' }}>
{isJson ? ( <Flex
<Textarea row
rows={rows} fluid
value={value} css={{
spellCheck={false} justifyContent: 'space-between',
onChange={switchToJson} alignItems: 'center',
css={{ position: 'relative',
flex: 'inherit', '@xl': {
resize: 'vertical' justifyContent: 'flex-end'
}}
/>
) : (
<Input
type={isFee ? 'number' : 'text'}
value={value}
onChange={e => {
if (isFee) {
const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
handleSetField(field, val)
} else {
handleSetField(field, e.target.value)
}
}}
onKeyPress={
isFee
? e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}
: undefined
} }
css={{ }}
flex: 'inherit', >
'-moz-appearance': 'textfield', <Text muted css={{ mr: '$3' }}>
'&::-webkit-outer-spin-button': { {field + (isXrp ? ' (XRP)' : '')}:{' '}
'-webkit-appearance': 'none', </Text>
margin: 0 {isJson ? (
}, <Textarea
'&::-webkit-inner-spin-button ': { rows={rows}
'-webkit-appearance': 'none', value={value}
margin: 0 spellCheck={false}
} onChange={switchToJson}
}} css={{
/> width: '60%',
)} minWidth: '200px',
{isFee && ( flex: 'inherit',
<Button resize: 'vertical',
size="xs" '@lg': {
variant="primary" width: '70%'
outline }
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}}
onClick={() => handleEstimateFee()}
>
Suggest
</Button>
)}
</TxField>
)
})}
<TxField multiLine label="Hook parameters">
<Flex column fluid>
{Object.entries(hookParameters).map(([id, { label, value }]) => (
<Flex column key={id} css={{ mb: '$2' }}>
<Flex row>
<Input
placeholder="Parameter name"
value={label}
onChange={e => {
setState({
hookParameters: {
...hookParameters,
[id]: { label: e.target.value, value }
}
})
}} }}
/> />
) : (
<Input <Input
css={{ mx: '$2' }} type={isFee ? 'number' : 'text'}
placeholder="Value"
value={value} value={value}
onChange={e => { onChange={e => {
setState({ if (isFee) {
hookParameters: { const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
...hookParameters, handleSetField(field, val)
[id]: { label, value: e.target.value } } else {
} handleSetField(field, e.target.value)
}) }
}}
onKeyPress={
isFee
? e => {
if (e.key === '.' || e.key === ',') {
e.preventDefault()
}
}
: undefined
}
css={{
width: '60%',
minWidth: '200px',
flex: 'inherit',
'-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': {
'-webkit-appearance': 'none',
margin: 0
},
'&::-webkit-inner-spin-button ': {
'-webkit-appearance': 'none',
margin: 0
},
'@lg': {
width: '70%'
}
}} }}
/> />
)}
{isFee && (
<Button <Button
onClick={() => { size="xs"
const { [id]: _, ...rest } = hookParameters variant="primary"
setState({ hookParameters: rest }) outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: 'absolute',
right: '$2',
fontSize: '$xs',
cursor: 'pointer',
alignContent: 'center',
display: 'flex'
}} }}
variant="destroy" onClick={() => handleEstimateFee()}
> >
<Trash weight="regular" size="16px" /> Suggest
</Button> </Button>
</Flex> )}
</Flex> </Flex>
))} </Flex>
<Button )
outline })}
fullWidth
type="button"
onClick={() => {
const id = Object.keys(hookParameters).length
setState({
hookParameters: { ...hookParameters, [id]: { label: '', value: '' } }
})
}}
>
<Plus size="16px" />
Add Hook Parameter
</Button>
</Flex>
</TxField>
<TxField multiLine label="Memos">
<Flex column fluid>
{Object.entries(memos).map(([id, memo]) => (
<Flex column key={id} css={{ mb: '$2' }}>
<Flex
row
css={{
flexWrap: 'wrap',
width: '100%'
}}
>
<Input
placeholder="Memo type"
value={memo.type}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, type: e.target.value }
}
})
}}
/>
<Input
placeholder="Data"
css={{ mx: '$2' }}
value={memo.data}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, data: e.target.value }
}
})
}}
/>
<Input
placeholder="Format"
value={memo.format}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, format: e.target.value }
}
})
}}
/>
<Button
css={{ ml: '$2' }}
onClick={() => {
const { [id]: _, ...rest } = memos
setState({ memos: rest })
}}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
</Flex>
))}
<Button
outline
fullWidth
type="button"
onClick={() => {
const id = Object.keys(memos).length
setState({
memos: { ...memos, [id]: { data: '', format: '', type: '' } }
})
}}
>
<Plus size="16px" />
Add Memo
</Button>
</Flex>
</TxField>
</Flex> </Flex>
</Container> </Container>
) )
} }
export const CreatableAccount: FC<{
value: string | undefined
field: keyof TxFields
placeholder?: string
setField: (field: keyof TxFields, value: string, opFields?: TxFields) => void
}> = ({ value, field, setField, placeholder }) => {
const { accounts } = useSnapshot(state)
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const label = accountOptions.find(a => a.value === value)?.label || value
const val = {
value,
label
}
placeholder = placeholder || `${capitalize(field)} account`
return (
<CreatableSelect
isClearable
instanceId={field}
placeholder={placeholder}
options={accountOptions}
value={value ? val : undefined}
onChange={(acc: any) => setField(field, acc?.value)}
/>
)
}
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
label,
children,
multiLine = false
}) => {
return (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: multiLine ? 'flex-start' : 'center',
position: 'relative',
mb: '$2',
mt: '1px',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
{label}:{' '}
</Text>
<Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
</Flex>
)
}

View File

@@ -2,14 +2,11 @@
{ {
"TransactionType": "AccountDelete", "TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": { "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe",
"$type": "account",
"$value": ""
},
"DestinationTag": 13, "DestinationTag": 13,
"Fee": "2000000", "Fee": "2000000",
"Sequence": 2470665, "Sequence": 2470665,
"Flags": "2147483648" "Flags": 2147483648
}, },
{ {
"TransactionType": "AccountSet", "TransactionType": "AccountSet",
@@ -31,11 +28,7 @@
"TransactionType": "CheckCash", "TransactionType": "CheckCash",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "amount.xrp" "$type": "xrp"
},
"DeliverMin": {
"$value": "",
"$type": "amount.xrp"
}, },
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12" "Fee": "12"
@@ -43,10 +36,7 @@
{ {
"TransactionType": "CheckCreate", "TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": { "Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy",
"$type": "account",
"$value": ""
},
"SendMax": "100000000", "SendMax": "100000000",
"Expiration": 570113521, "Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B", "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
@@ -58,16 +48,13 @@
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8", "Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de", "Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10", "Fee": "10",
"Flags": "2147483648", "Flags": 2147483648,
"Sequence": 2 "Sequence": 2
}, },
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": { "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Fee": "10" "Fee": "10"
}, },
@@ -76,12 +63,9 @@
"TransactionType": "EscrowCreate", "TransactionType": "EscrowCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "amount.xrp" "$type": "xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958, "CancelAfter": 533257958,
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
@@ -92,10 +76,7 @@
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish", "TransactionType": "EscrowFinish",
"Owner": { "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000", "Fulfillment": "A0028000",
@@ -128,9 +109,7 @@
"Fee": "10", "Fee": "10",
"NFTokenOffers": { "NFTokenOffers": {
"$type": "json", "$type": "json",
"$value": [ "$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
"4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
]
} }
}, },
{ {
@@ -139,24 +118,17 @@
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "amount.xrp" "$type": "xrp"
},
"Flags": "1",
"Destination": {
"$type": "account",
"$value": ""
},
"Owner": {
"$type": "account",
"$value": ""
}, },
"Flags": 1,
"Destination": "",
"Fee": "10" "Fee": "10"
}, },
{ {
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": "0", "Flags": 0,
"LastLedgerSequence": 7108629, "LastLedgerSequence": 7108629,
"OfferSequence": 6, "OfferSequence": 6,
"Sequence": 7 "Sequence": 7
@@ -165,35 +137,25 @@
"TransactionType": "OfferCreate", "TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": "0", "Flags": 0,
"LastLedgerSequence": 7108682, "LastLedgerSequence": 7108682,
"Sequence": 8, "Sequence": 8,
"TakerGets": { "TakerGets": "6000000",
"$type": "amount.xrp", "Amount": {
"$value": "6000000" "$value": "100",
}, "$type": "xrp"
"TakerPays": {
"$type": "amount.token",
"$value": {
"currency": "GKO",
"issuer": "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
"value": "2"
}
} }
}, },
{ {
"TransactionType": "Payment", "TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": { "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"$type": "account",
"$value": ""
},
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "amount.xrp" "$type": "xrp"
}, },
"Fee": "12", "Fee": "12",
"Flags": "2147483648", "Flags": 2147483648,
"Sequence": 2 "Sequence": 2
}, },
{ {
@@ -201,12 +163,9 @@
"TransactionType": "PaymentChannelCreate", "TransactionType": "PaymentChannelCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "amount.xrp" "$type": "xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400, "SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
@@ -220,20 +179,20 @@
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": { "Amount": {
"$value": "200", "$value": "200",
"$type": "amount.xrp" "$type": "xrp"
}, },
"Expiration": 543171558, "Expiration": 543171558,
"Fee": "10" "Fee": "10"
}, },
{ {
"Flags": "0", "Flags": 0,
"TransactionType": "SetRegularKey", "TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD" "RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
}, },
{ {
"Flags": "0", "Flags": 0,
"TransactionType": "SignerListSet", "TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
@@ -273,10 +232,10 @@
"TransactionType": "TrustSet", "TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": "262144", "Flags": 262144,
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"LimitAmount": { "LimitAmount": {
"$type": "amount.token", "$type": "json",
"$value": { "$value": {
"currency": "USD", "currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc", "issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
@@ -284,14 +243,5 @@
} }
}, },
"Sequence": 12 "Sequence": 12
},
{
"TransactionType": "Invoke",
"Fee": "12"
},
{
"TransactionType": "UriToken",
"Fee": "12",
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
} }
] ]

View File

@@ -12,7 +12,7 @@ module.exports = {
config.resolve.fallback.fs = false config.resolve.fallback.fs = false
} }
config.module.rules.push({ config.module.rules.push({
test: [/\.md$/, /hook-bundle\.js$/], test: /\.md$/,
use: 'raw-loader' use: 'raw-loader'
}) })
return config return config

View File

@@ -8,8 +8,7 @@
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write .", "format": "prettier --write .",
"postinstall": "patch-package && yarn run postinstall-postinstall", "postinstall": "patch-package"
"postinstall-postinstall": "./node_modules/.bin/browserify -r ripple-binary-codec -r ripple-keypairs -r ripple-address-codec -r ripple-secret-codec -r ./node_modules/xrpl-accountlib/dist/index.js:xrpl-accountlib -o node_modules/xrpl-accountlib/dist/browser.hook-bundle.js"
}, },
"dependencies": { "dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1", "@codingame/monaco-jsonrpc": "^0.3.1",
@@ -65,9 +64,9 @@
"valtio": "^1.2.5", "valtio": "^1.2.5",
"vscode-languageserver": "^7.0.0", "vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2", "vscode-uri": "^3.0.2",
"wabt": "^1.0.30", "wabt": "1.0.16",
"xrpl-accountlib": "^1.6.1", "xrpl-accountlib": "^1.5.2",
"xrpl-client": "^2.0.2" "xrpl-client": "^1.9.4"
}, },
"devDependencies": { "devDependencies": {
"@types/dinero.js": "^1.9.0", "@types/dinero.js": "^1.9.0",
@@ -76,7 +75,6 @@
"@types/lodash.xor": "^4.5.6", "@types/lodash.xor": "^4.5.6",
"@types/pako": "^1.0.2", "@types/pako": "^1.0.2",
"@types/react": "17.0.31", "@types/react": "17.0.31",
"browserify": "^17.0.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-next": "11.1.2", "eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",

View File

@@ -10,11 +10,10 @@ import Box from '../../components/Box'
import Button from '../../components/Button' import Button from '../../components/Button'
import Popover from '../../components/Popover' import Popover from '../../components/Popover'
import RunScript from '../../components/RunScript' import RunScript from '../../components/RunScript'
import state, { IFile } from '../../state' import state from '../../state'
import { compileCode } from '../../state/actions' import { compileCode } from '../../state/actions'
import { getSplit, saveSplit } from '../../state/actions/persistSplits' import { getSplit, saveSplit } from '../../state/actions/persistSplits'
import { styled } from '../../stitches.config' import { styled } from '../../stitches.config'
import { getFileExtention } from '../../utils/helpers'
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), { const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
ssr: false ssr: false
@@ -148,9 +147,6 @@ const CompilerSettings = () => {
const Home: NextPage = () => { const Home: NextPage = () => {
const snap = useSnapshot(state) const snap = useSnapshot(state)
const activeFile = snap.files[snap.active] as IFile | undefined
const activeFileExt = getFileExtention(activeFile?.name)
const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
return ( return (
<Split <Split
direction="vertical" direction="vertical"
@@ -163,7 +159,7 @@ const Home: NextPage = () => {
> >
<main style={{ display: 'flex', flex: 1, position: 'relative' }}> <main style={{ display: 'flex', flex: 1, position: 'relative' }}>
<HooksEditor /> <HooksEditor />
{canCompile && ( {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'c' && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
@@ -197,7 +193,7 @@ const Home: NextPage = () => {
</Flex> </Flex>
</Hotkeys> </Hotkeys>
)} )}
{activeFileExt === 'js' && ( {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'js' && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
@@ -213,7 +209,7 @@ const Home: NextPage = () => {
gap: '$2' gap: '$2'
}} }}
> >
<RunScript file={activeFile as IFile} /> <RunScript file={snap.files[snap.active]} />
</Flex> </Flex>
</Hotkeys> </Hotkeys>
)} )}
@@ -229,7 +225,7 @@ const Home: NextPage = () => {
> >
<LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} /> <LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
</Flex> </Flex>
{activeFileExt === 'js' && ( {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'js' && (
<Flex <Flex
css={{ css={{
flex: 1 flex: 1

View File

@@ -68,6 +68,7 @@ const Test = () => {
justifyContent: 'center', justifyContent: 'center',
p: '$3 $2' p: '$3 $2'
}} }}
className="split-mobile-forceAutoHeight"
> >
<Split <Split
direction="horizontal" direction="horizontal"
@@ -82,6 +83,7 @@ const Test = () => {
height: '100%' height: '100%'
}} }}
onDragEnd={e => saveSplit('testHorizontal', e)} onDragEnd={e => saveSplit('testHorizontal', e)}
className="split-mobile-forceVertical"
> >
<Box css={{ width: '55%', px: '$2' }}> <Box css={{ width: '55%', px: '$2' }}>
<Tabs <Tabs
@@ -105,7 +107,18 @@ const Test = () => {
))} ))}
</Tabs> </Tabs>
</Box> </Box>
<Box css={{ width: '45%', mx: '$2', height: '100%' }}> <Box
css={{
width: '45%',
mx: '$0',
mt: '$1',
height: '100%',
'@md': {
mx: '$2',
mt: '$0'
}
}}
>
<Accounts card hideDeployBtn showHookStats /> <Accounts card hideDeployBtn showHookStats />
</Box> </Box>
</Split> </Split>
@@ -131,6 +144,7 @@ const Test = () => {
<Flex> <Flex>
<Split <Split
direction="horizontal" direction="horizontal"
className="split-mobile-forceVertical"
sizes={[50, 50]} sizes={[50, 50]}
minSize={[320, 160]} minSize={[320, 160]}
gutterSize={4} gutterSize={4}

File diff suppressed because it is too large Load Diff

5
raw-loader.d.ts vendored
View File

@@ -2,8 +2,3 @@ declare module '*.md' {
const content: string const content: string
export default content export default content
} }
declare module '*.hook-bundle.js' {
const content: string
export default content
}

View File

@@ -1,6 +1,5 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import state, { FaucetAccountRes } from '../index' import state, { FaucetAccountRes } from '../index'
import fetchAccountInfo from '../../utils/accountInfo';
export const names = [ export const names = [
'Alice', 'Alice',
@@ -36,37 +35,40 @@ export const addFaucetAccount = async (name?: string, showToast: boolean = false
}) })
const json: FaucetAccountRes | { error: string } = await res.json() const json: FaucetAccountRes | { error: string } = await res.json()
if ('error' in json) { if ('error' in json) {
if (!showToast) return; if (showToast) {
return toast.error(json.error, { id: toastId }) return toast.error(json.error, { id: toastId })
} } else {
const currNames = state.accounts.map(acc => acc.name) return
const info = await fetchAccountInfo(json.address, { silent: true }) }
state.accounts.push({ } else {
name: name || names.filter(name => !currNames.includes(name))[0], if (showToast) {
xrp: (json.xrp || 0 * 1000000).toString(), toast.success('New account created', { id: toastId })
address: json.address, }
secret: json.secret, const currNames = state.accounts.map(acc => acc.name)
sequence: info?.Sequence || 1, state.accounts.push({
hooks: [], name: name || names.filter(name => !currNames.includes(name))[0],
isLoading: false, xrp: (json.xrp || 0 * 1000000).toString(),
version: '2' address: json.address,
}) secret: json.secret,
if (showToast) { sequence: 1,
toast.success('New account created', { id: toastId }) hooks: [],
isLoading: false,
version: '2'
})
} }
} }
// fetch initial faucets // fetch initial faucets
; (async function fetchFaucets() { ;(async function fetchFaucets() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
if (state.accounts.length === 0) { if (state.accounts.length === 0) {
await addFaucetAccount() await addFaucetAccount()
// setTimeout(() => { // setTimeout(() => {
// addFaucetAccount(); // addFaucetAccount();
// }, 10000); // }, 10000);
}
} }
})() }
})()
export const addFunds = async (address: string) => { export const addFunds = async (address: string) => {
const toastId = toast.loading('Requesting funds') const toastId = toast.loading('Requesting funds')

View File

@@ -15,11 +15,6 @@ import { ref } from 'valtio'
export const compileCode = async (activeId: number) => { export const compileCode = async (activeId: number) => {
// Save the file to global state // Save the file to global state
saveFile(false, activeId) saveFile(false, activeId)
const file = state.files[activeId]
if (file.name.endsWith('.wat')) {
return compileWat(activeId)
}
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) { if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error('Missing env!') throw Error('Missing env!')
} }
@@ -31,6 +26,7 @@ export const compileCode = async (activeId: number) => {
// Set loading state to true // Set loading state to true
state.compiling = true state.compiling = true
state.logs = [] state.logs = []
const file = state.files[activeId]
try { try {
file.containsErrors = false file.containsErrors = false
let res: Response let res: Response
@@ -76,7 +72,7 @@ export const compileCode = async (activeId: number) => {
// Import wabt from and create human readable version of wasm file and // Import wabt from and create human readable version of wasm file and
// put it into state // put it into state
const ww = await (await import('wabt')).default() const ww = (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), { const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true readDebugNames: true
}) })
@@ -126,46 +122,3 @@ export const compileCode = async (activeId: number) => {
file.containsErrors = true file.containsErrors = true
} }
} }
export const compileWat = async (activeId: number) => {
if (state.compiling) return;
const file = state.files[activeId]
state.compiling = true
state.logs = []
try {
const wabt = await (await import('wabt')).default()
const module = wabt.parseWat(file.name, file.content);
module.resolveNames();
module.validate();
const { buffer } = module.toBinary({
log: false,
write_debug_names: true,
});
file.compiledContent = ref(buffer)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = file.content
toast.success('Compiled successfully!', { position: 'bottom-center' })
state.logs.push({
type: 'success',
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace('develop', 'deploy'),
linkText: 'Go to deploy'
})
} catch (err) {
console.log(err)
let message = "Error compiling WAT file!"
if (err instanceof Error) {
message = err.message
}
state.logs.push({
type: 'error',
message
})
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
}
state.compiling = false
}

View File

@@ -1,19 +1,21 @@
import { getFileExtention } from '../../utils/helpers'
import state, { IFile } from '../index' import state, { IFile } from '../index'
const languageMapping: Record<string, string | undefined> = { const languageMapping = {
ts: 'typescript', ts: 'typescript',
js: 'javascript', js: 'javascript',
md: 'markdown', md: 'markdown',
c: 'c', c: 'c',
h: 'c', h: 'c',
txt: 'text' other: ''
} } /* Initializes empty file to global state */
export const createNewFile = (name: string) => { export const createNewFile = (name: string) => {
const ext = getFileExtention(name) || '' const tempName = name.split('.')
const fileExt = tempName[tempName.length - 1] || 'other'
const emptyFile: IFile = { name, language: languageMapping[ext] || 'text', content: '' } const emptyFile: IFile = {
name,
language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'],
content: ''
}
state.files.push(emptyFile) state.files.push(emptyFile)
state.active = state.files.length - 1 state.active = state.files.length - 1
} }
@@ -22,8 +24,5 @@ export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName) const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`) if (!file) throw Error(`No file exists with name ${oldName}`)
const ext = getFileExtention(nwName) || ''
const language = languageMapping[ext] || 'text'
file.name = nwName file.name = nwName
file.language = language
} }

View File

@@ -14,4 +14,11 @@ export const deleteAccount = (addr?: string) => {
if (!acc) return if (!acc) return
acc.label = acc.value acc.label = acc.value
}) })
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount
if (!acc) return
acc.label = acc.value
})
} }

View File

@@ -6,9 +6,8 @@ import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
import { Link } from '../../components' import { Link } from '../../components'
import { ref } from 'valtio' import { ref } from 'valtio'
import estimateFee from '../../utils/estimateFee' import estimateFee from '../../utils/estimateFee'
import { SetHookData, toHex } from '../../utils/setHook' import { SetHookData } from '../../utils/setHook'
import ResultLink from '../../components/ResultLink' import ResultLink from '../../components/ResultLink'
import { xrplSend } from './xrpl-client'
export const sha256 = async (string: string) => { export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string) const utf8 = new TextEncoder().encode(string)
@@ -18,6 +17,13 @@ export const sha256 = async (string: string) => {
return hashHex return hashHex
} }
function toHex(str: string) {
var result = ''
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16)
}
return result.toUpperCase()
}
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) { function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) { if (!arrayBuffer) {
@@ -58,6 +64,9 @@ export const prepareDeployHookTx = async (
if (!activeFile?.compiledContent) { if (!activeFile?.compiledContent) {
return return
} }
if (!state.client) {
return
}
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase() const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value) const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
const { HookParameters } = data const { HookParameters } = data
@@ -78,184 +87,196 @@ export const prepareDeployHookTx = async (
// } // }
// } // }
// }); // });
if (typeof window === 'undefined') return if (typeof window !== 'undefined') {
const tx = { const tx = {
Account: account.address, Account: account.address,
TransactionType: 'SetHook', TransactionType: 'SetHook',
Sequence: account.sequence, Sequence: account.sequence,
Fee: data.Fee, Fee: data.Fee,
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID, Hooks: [
Hooks: [ {
{ Hook: {
Hook: { CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(), HookOn: calculateHookOn(hookOnValues),
HookOn: calculateHookOn(hookOnValues), HookNamespace,
HookNamespace, HookApiVersion: 0,
HookApiVersion: 0, Flags: 1,
Flags: 1, // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }), ...(filteredHookParameters.length > 0 && {
...(filteredHookParameters.length > 0 && { HookParameters: filteredHookParameters
HookParameters: filteredHookParameters })
}) }
} }
} ]
] }
return tx
} }
return tx
} }
/* /* deployHook function turns the wasm binary into
* Turns the wasm binary into hex string, signs the transaction and deploys it to Hooks testnet. * hex string, signs the transaction and deploys it to
* Hooks testnet.
*/ */
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => { export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
const activeFile = state.files[state.active]?.compiledContent if (typeof window !== 'undefined') {
? state.files[state.active] const activeFile = state.files[state.active]?.compiledContent
: state.files.filter(file => file.compiledContent)[0] ? state.files[state.active]
state.deployValues[activeFile.name] = data : state.files.filter(file => file.compiledContent)[0]
state.deployValues[activeFile.name] = data
const tx = await prepareDeployHookTx(account, data)
if (!tx) {
return
}
if (!state.client) {
return
}
const keypair = derive.familySeed(account.secret)
const tx = await prepareDeployHookTx(account, data) const { signedTransaction } = sign(tx, keypair)
if (!tx) { const currentAccount = state.accounts.find(acc => acc.address === account.address)
return if (currentAccount) {
} currentAccount.isLoading = true
const keypair = derive.familySeed(account.secret) }
const { signedTransaction } = sign(tx, keypair) let submitRes
const currentAccount = state.accounts.find(acc => acc.address === account.address) try {
if (currentAccount) { submitRes = await state.client?.send({
currentAccount.isLoading = true command: 'submit',
} tx_blob: signedTransaction
let submitRes
try {
submitRes = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
const txHash = submitRes.tx_json?.hash
const resultMsg = ref(
<>
[<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
{txHash && (
<>
Transaction hash:{' '}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash}
</Link>
</>
)}
</>
)
if (submitRes.engine_result === 'tesSUCCESS') {
state.deployLogs.push({
type: 'success',
message: 'Hook deployed successfully ✅'
}) })
state.deployLogs.push({
type: 'success', const txHash = submitRes.tx_json?.hash
message: resultMsg const resultMsg = ref(
}) <>
} else if (submitRes.engine_result) { [<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
{txHash && (
<>
Transaction hash:{' '}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
target="_blank"
rel="noopener noreferrer"
>
{txHash}
</Link>
</>
)}
</>
)
if (submitRes.engine_result === 'tesSUCCESS') {
state.deployLogs.push({
type: 'success',
message: 'Hook deployed successfully ✅'
})
state.deployLogs.push({
type: 'success',
message: resultMsg
})
} else if (submitRes.engine_result) {
state.deployLogs.push({
type: 'error',
message: resultMsg
})
} else {
state.deployLogs.push({
type: 'error',
message: `[${submitRes.error}] ${submitRes.error_exception}`
})
}
} catch (err) {
console.log(err)
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: 'error',
message: resultMsg message: 'Error occurred while deploying'
})
} else {
state.deployLogs.push({
type: 'error',
message: `[${submitRes.error}] ${submitRes.error_exception}`
}) })
} }
} catch (err) { if (currentAccount) {
console.error(err) currentAccount.isLoading = false
state.deployLogs.push({ }
type: 'error', return submitRes
message: 'Error occurred while deploying'
})
} }
if (currentAccount) {
currentAccount.isLoading = false
}
return submitRes
} }
export const deleteHook = async (account: IAccount & { name?: string }) => { export const deleteHook = async (account: IAccount & { name?: string }) => {
if (!state.client) {
return
}
const currentAccount = state.accounts.find(acc => acc.address === account.address) const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount?.isLoading || !currentAccount?.hooks.length) { if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return return
} }
const tx = { if (typeof window !== 'undefined') {
Account: account.address, const tx = {
TransactionType: 'SetHook', Account: account.address,
Sequence: account.sequence, TransactionType: 'SetHook',
Fee: '100000', Sequence: account.sequence,
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID, Fee: '100000',
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
CreateCode: '', CreateCode: '',
Flags: 1 Flags: 1
}
} }
} ]
] }
}
const keypair = derive.familySeed(account.secret)
try {
// Update tx Fee value with network estimation
const res = await estimateFee(tx, account)
tx['Fee'] = res?.base_fee || '1000'
} catch (err) {
console.error(err)
}
const { signedTransaction } = sign(tx, keypair)
if (currentAccount) {
currentAccount.isLoading = true
}
let submitRes
const toastId = toast.loading('Deleting hook...')
try {
submitRes = await xrplSend({
command: 'submit',
tx_blob: signedTransaction
})
if (submitRes.engine_result === 'tesSUCCESS') { const keypair = derive.familySeed(account.secret)
toast.success('Hook deleted successfully ✅', { id: toastId }) try {
state.deployLogs.push({ // Update tx Fee value with network estimation
type: 'success', const res = await estimateFee(tx, account)
message: 'Hook deleted successfully ✅' tx['Fee'] = res?.base_fee ? res?.base_fee : '1000'
}) } catch (err) {
state.deployLogs.push({ // use default value what you defined earlier
type: 'success', console.log(err)
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}` }
}) const { signedTransaction } = sign(tx, keypair)
currentAccount.hooks = []
} else { if (currentAccount) {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, { currentAccount.isLoading = true
id: toastId }
let submitRes
const toastId = toast.loading('Deleting hook...')
try {
submitRes = await state.client.send({
command: 'submit',
tx_blob: signedTransaction
}) })
if (submitRes.engine_result === 'tesSUCCESS') {
toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({
type: 'success',
message: 'Hook deleted successfully ✅'
})
state.deployLogs.push({
type: 'success',
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
})
currentAccount.hooks = []
} else {
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
id: toastId
})
state.deployLogs.push({
type: 'error',
message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`
})
}
} catch (err) {
console.log(err)
toast.error('Error occurred while deleting hook', { id: toastId })
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: 'error',
message: `[${submitRes.engine_result || submitRes.error}] ${ message: 'Error occurred while deleting hook'
submitRes.engine_result_message || submitRes.error_exception
}`
}) })
} }
} catch (err) { if (currentAccount) {
console.log(err) currentAccount.isLoading = false
toast.error('Error occurred while deleting hook', { id: toastId }) }
state.deployLogs.push({ return submitRes
type: 'error',
message: 'Error occurred while deleting hook'
})
} }
if (currentAccount) {
currentAccount.isLoading = false
}
return submitRes
} }

View File

@@ -61,7 +61,6 @@ export const fetchFiles = async (gistId: string) => {
// default priority is undefined == 0 // default priority is undefined == 0
const extPriority: Record<string, number> = { const extPriority: Record<string, number> = {
c: 3, c: 3,
wat: 3,
md: 2, md: 2,
h: -1 h: -1
} }

View File

@@ -0,0 +1,66 @@
import { derive, sign } from 'xrpl-accountlib'
import state from '..'
import type { IAccount } from '..'
interface TransactionOptions {
TransactionType: string
Account?: string
Fee?: string
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (
account: IAccount,
txOptions: TransactionOptions,
options?: OtherOptions
) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence,
Fee, // TODO auto-fillable default
...opts
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount)
const response = await state.client.send({
command: 'submit',
tx_blob: signedTransaction
})
if (response.engine_result === 'tesSUCCESS') {
state.transactionLogs.push({
type: 'success',
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}[${response.error || response.engine_result}] ${
response.error_exception || response.engine_result_message
}`
})
}
const currAcc = state.accounts.find(acc => acc.address === account.address)
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next
}
} catch (err) {
console.error(err)
state.transactionLogs.push({
type: 'error',
message:
err instanceof Error
? `${logPrefix}Error: ${err.message}`
: `${logPrefix}Something went wrong, try again later`
})
}
}

View File

@@ -4,12 +4,12 @@ import state from '..'
import type { IAccount } from '..' import type { IAccount } from '..'
import ResultLink from '../../components/ResultLink' import ResultLink from '../../components/ResultLink'
import { ref } from 'valtio' import { ref } from 'valtio'
import { xrplSend } from './xrpl-client'
interface TransactionOptions { interface TransactionOptions {
TransactionType: string TransactionType: string
Account?: string Account?: string
Fee?: string Fee?: string
Destination?: string
[index: string]: any [index: string]: any
} }
interface OtherOptions { interface OtherOptions {
@@ -21,23 +21,20 @@ export const sendTransaction = async (
txOptions: TransactionOptions, txOptions: TransactionOptions,
options?: OtherOptions options?: OtherOptions
) => { ) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = '1000', ...opts } = txOptions const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = { const tx: TransactionOptions = {
Account: account.address, Account: account.address,
Sequence: account.sequence, Sequence: account.sequence,
Fee, Fee, // TODO auto-fillable default
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
...opts ...opts
} }
const { logPrefix = '' } = options || {} const { logPrefix = '' } = options || {}
state.transactionLogs.push({
type: 'log',
message: `${logPrefix}${JSON.stringify(tx, null, 2)}`
})
try { try {
const signedAccount = derive.familySeed(account.secret) const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount) const { signedTransaction } = sign(tx, signedAccount)
const response = await xrplSend({ const response = await state.client.send({
command: 'submit', command: 'submit',
tx_blob: signedTransaction tx_blob: signedTransaction
}) })

View File

@@ -1,7 +0,0 @@
import { XrplClient } from 'xrpl-client';
import state from '..';
export const xrplSend = async(...params: Parameters<XrplClient['send']>) => {
const client = await state.client.ready()
return client.send(...params);
}

View File

@@ -1,79 +0,0 @@
import { SelectOption } from '../transactions';
interface Flags {
[key: string]: string;
}
export const transactionFlags: { [key: /* TransactionType */ string]: Flags } = {
"*": {
tfFullyCanonicalSig: '0x80000000'
},
Payment: {
tfNoDirectRipple: '0x00010000',
tfPartialPayment: '0x00020000',
tfLimitQuality: '0x00040000',
},
AccountSet: {
tfRequireDestTag: '0x00010000',
tfOptionalDestTag: '0x00020000',
tfRequireAuth: '0x00040000',
tfOptionalAuth: '0x00080000',
tfDisallowXRP: '0x00100000',
tfAllowXRP: '0x00200000',
},
NFTokenCreateOffer: {
tfSellNFToken: '0x00000001',
},
NFTokenMint: {
tfBurnable: '0x00000001',
tfOnlyXRP: '0x00000002',
tfTrustLine: '0x00000004',
tfTransferable: '0x00000008',
},
OfferCreate: {
tfPassive: '0x00010000',
tfImmediateOrCancel: '0x00020000',
tfFillOrKill: '0x00040000',
tfSell: '0x00080000',
},
PaymentChannelClaim: {
tfRenew: '0x00010000',
tfClose: '0x00020000',
},
TrustSet: {
tfSetfAuth: '0x00010000',
tfSetNoRipple: '0x00020000',
tfClearNoRipple: '0x00040000',
tfSetFreeze: '0x00100000',
tfClearFreeze: '0x00200000',
},
}
export const getFlags = (tt?: string) => {
if (!tt) return
const flags = {
...transactionFlags['*'],
...transactionFlags[tt]
}
return flags
}
export function combineFlags(flags?: string[]): string | undefined {
if (!flags) return
const num = flags.reduce((cumm, curr) => cumm | BigInt(curr), BigInt(0))
return num.toString()
}
export function extractFlags(transactionType: string, flags?: string | number,): SelectOption[] {
const flagsObj = getFlags(transactionType)
if (!flags || !flagsObj) return []
const extracted = Object.entries(flagsObj).reduce((cumm, [label, value]) => {
return (BigInt(flags) & BigInt(value)) ? cumm.concat({ label, value }) : cumm
}, [] as SelectOption[])
return extracted
}

View File

@@ -78,7 +78,7 @@ export interface IState {
splits: { splits: {
[id: string]: SplitSize [id: string]: SplitSize
} }
client: XrplClient client: XrplClient | null
clientStatus: 'offline' | 'online' clientStatus: 'offline' | 'online'
mainModalOpen: boolean mainModalOpen: boolean
mainModalShowed: boolean mainModalShowed: boolean
@@ -113,7 +113,7 @@ let initialState: IState = {
tabSize: 2 tabSize: 2
}, },
splits: {}, splits: {},
client: undefined!, // set below only. client: null,
clientStatus: 'offline' as 'offline', clientStatus: 'offline' as 'offline',
mainModalOpen: false, mainModalOpen: false,
mainModalShowed: false, mainModalShowed: false,
@@ -153,9 +153,9 @@ const state = proxy<IState>({
}) })
// Initialize socket connection // Initialize socket connection
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`) const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`)
state.client = ref(client);
client.on('online', () => { client.on('online', () => {
state.client = ref(client)
state.clientStatus = 'online' state.clientStatus = 'online'
}) })

View File

@@ -4,56 +4,33 @@ import transactionsData from '../content/transactions.json'
import state from '.' import state from '.'
import { showAlert } from '../state/actions/showAlert' import { showAlert } from '../state/actions/showAlert'
import { parseJSON } from '../utils/json' import { parseJSON } from '../utils/json'
import { extractFlags, getFlags } from './constants/flags'
import { fromHex } from '../utils/setHook'
import { typeIs } from '../utils/helpers'
export type SelectOption = { export type SelectOption = {
value: string value: string
label: string label: string
} }
export type HookParameters = {
[key: string]: SelectOption
}
export type Memos = {
[key: string]: {
type: string
format: string
data: string
}
}
export interface TransactionState { export interface TransactionState {
selectedTransaction: SelectOption | null selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null selectedAccount: SelectOption | null
selectedFlags: SelectOption[] | null selectedDestAccount: SelectOption | null
hookParameters: HookParameters
memos: Memos
txIsLoading: boolean txIsLoading: boolean
txIsDisabled: boolean txIsDisabled: boolean
txFields: TxFields txFields: TxFields
viewType: 'json' | 'ui' viewType: 'json' | 'ui'
editorValue?: string editorValue?: string
editorIsSaved: boolean
estimatedFee?: string estimatedFee?: string
} }
const commonFields = ['TransactionType', 'Account', 'Sequence', "HookParameters"] as const;
export type TxFields = Omit< export type TxFields = Omit<
Partial<typeof transactionsData[0]>, Partial<typeof transactionsData[0]>,
typeof commonFields[number] 'Account' | 'Sequence' | 'TransactionType'
> >
export const defaultTransaction: TransactionState = { export const defaultTransaction: TransactionState = {
selectedTransaction: null, selectedTransaction: null,
selectedAccount: null, selectedAccount: null,
selectedFlags: null, selectedDestAccount: null,
hookParameters: {},
memos: {},
editorIsSaved: true,
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
@@ -129,51 +106,47 @@ export const modifyTxState = (
return tx.state return tx.state
} }
// state to tx options
export const prepareTransaction = (data: any) => { export const prepareTransaction = (data: any) => {
let options = { ...data } let options = { ...data }
Object.keys(options).forEach(field => { Object.keys(options).forEach(field => {
let _value = options[field] let _value = options[field]
if (!typeIs(_value, 'object')) return // convert xrp
// amount.xrp if (_value && typeof _value === 'object' && _value.$type === 'xrp') {
if (_value.$type === 'amount.xrp') { if (+_value.$value) {
if (_value.$value) { options[field] = (+_value.$value * 1000000 + '') as any
options[field] = (+(_value as any).$value * 1000000 + '')
} else { } else {
options[field] = "" options[field] = undefined // 👇 💀
} }
} }
// amount.token // handle type: `json`
if (_value.$type === 'amount.token') { if (_value && typeof _value === 'object' && _value.$type === 'json') {
if (typeIs(_value.$value, 'string')) { if (typeof _value.$value === 'object') {
options[field] = parseJSON(_value.$value)
} else if (typeIs(_value.$value, 'object')) {
options[field] = _value.$value options[field] = _value.$value
} else { } else {
options[field] = undefined try {
options[field] = JSON.parse(_value.$value)
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ''
}`
console.error(message)
options[field] = _value.$value
}
} }
} }
// account
if (_value.$type === 'account') { // delete unnecessary fields
options[field] = (_value.$value as any)?.toString() || "" if (!options[field]) {
} delete options[field]
// json
if (_value.$type === 'json') {
const val = _value.$value;
let res: any = val;
if (typeIs(val, ["object", "array"])) {
options[field] = res
} else if (typeIs(val, "string") && (res = parseJSON(val))) {
options[field] = res;
} else {
options[field] = res;
}
} }
}) })
return options return options
} }
// editor value to state
export const prepareState = (value: string, transactionType?: string) => { export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value) const options = parseJSON(value)
if (!options) { if (!options) {
@@ -183,7 +156,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return return
} }
const { Account, TransactionType, HookParameters, Memos, ...rest } = options const { Account, TransactionType, Destination, ...rest } = options
let tx: Partial<TransactionState> = {} let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType) const schema = getTxFields(transactionType)
@@ -213,58 +186,39 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null tx.selectedTransaction = null
} }
if (HookParameters && HookParameters instanceof Array) { if (schema.Destination !== undefined) {
tx.hookParameters = HookParameters.reduce<TransactionState["hookParameters"]>((acc, cur, idx) => { const dest = state.accounts.find(acc => acc.address === Destination)
const param = { label: fromHex(cur.HookParameter?.HookParameterName || ""), value: fromHex(cur.HookParameter?.HookParameterValue || "") } if (dest) {
acc[idx] = param; tx.selectedDestAccount = {
return acc; label: dest.name,
}, {}) value: dest.address
} }
} else if (Destination) {
if (Memos && Memos instanceof Array) { tx.selectedDestAccount = {
tx.memos = Memos.reduce<TransactionState["memos"]>((acc, cur, idx) => { label: Destination,
const memo = { data: fromHex(cur.Memo?.MemoData || ""), type: fromHex(cur.Memo?.MemoType || ""), format: fromHex(cur.Memo?.MemoFormat || "") } value: Destination
acc[idx] = memo; }
return acc; } else {
}, {}) tx.selectedDestAccount = null
} }
} else if (Destination) {
rest.Destination = Destination
if (getFlags(TransactionType) && rest.Flags) {
const flags = extractFlags(TransactionType, rest.Flags)
rest.Flags = undefined
tx.selectedFlags = flags
} }
Object.keys(rest).forEach(field => { Object.keys(rest).forEach(field => {
const value = rest[field] const value = rest[field]
const schemaVal = schema[field as keyof TxFields] const schemaVal = schema[field as keyof TxFields]
const isXrp =
const isAmount = schemaVal && typeof value !== 'object' &&
typeIs(schemaVal, "object") && schemaVal &&
schemaVal.$type.startsWith('amount.'); typeof schemaVal === 'object' &&
const isAccount = schemaVal && schemaVal.$type === 'xrp'
typeIs(schemaVal, "object") && if (isXrp) {
schemaVal.$type.startsWith("account");
if (isAmount && ["number", "string"].includes(typeof value)) {
rest[field] = { rest[field] = {
$type: 'amount.xrp', // TODO narrow typed $type. $type: 'xrp',
$value: +value / 1000000 // ! maybe use bigint? $value: +value / 1000000 // ! maybe use bigint?
} }
} else if (isAmount && typeof value === 'object') { } else if (typeof value === 'object') {
rest[field] = {
$type: 'amount.token',
$value: value
}
} else if (isAccount) {
rest[field] = {
$type: "account",
$value: value?.toString() || ""
}
}
else if (typeof value === 'object') {
rest[field] = { rest[field] = {
$type: 'json', $type: 'json',
$value: value $value: value
@@ -273,7 +227,6 @@ export const prepareState = (value: string, transactionType?: string) => {
}) })
tx.txFields = rest tx.txFields = rest
tx.editorIsSaved = true;
return tx return tx
} }
@@ -284,12 +237,12 @@ export const getTxFields = (tt?: string) => {
if (!txFields) return {} if (!txFields) return {}
let _txFields = Object.keys(txFields) let _txFields = Object.keys(txFields)
.filter(key => !commonFields.includes(key as any)) .filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key))
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {}) .reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {})
return _txFields return _txFields
} }
export { transactionsData, commonFields } export { transactionsData }
export const transactionsOptions = transactionsData.map(tx => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,

View File

@@ -54,3 +54,15 @@ html.light .gutter-horizontal:hover {
.monaco-editor .monaco-hover { .monaco-editor .monaco-hover {
z-index: 9999; z-index: 9999;
} }
@media screen and (max-width: 48rem) {
.split-mobile-forceAutoHeight {
height: auto !important;
}
.split-mobile-forceVertical {
flex-direction: column !important;
}
.split-mobile-forceVertical > div {
width: 100% !important;
}
}

View File

@@ -1,31 +0,0 @@
import toast from 'react-hot-toast'
import { xrplSend } from '../state/actions/xrpl-client'
interface AccountInfo {
Account: string,
Sequence: number,
Flags: number,
Balance?: string,
}
const fetchAccountInfo = async (
address: string,
opts: { silent?: boolean } = {}
): Promise<AccountInfo | undefined> => {
try {
const res = await xrplSend({
id: `hooks-builder-req-info-${address}`,
command: 'account_info',
account: address
})
return res.account_data;
} catch (err) {
if (!opts.silent) {
console.error(err)
toast.error('Could not fetch account info!')
}
}
}
export default fetchAccountInfo

View File

@@ -1,7 +1,6 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { derive, sign } from 'xrpl-accountlib' import { derive, sign } from 'xrpl-accountlib'
import { IAccount } from '../state' import state, { IAccount } from '../state'
import { xrplSend } from '../state/actions/xrpl-client'
const estimateFee = async ( const estimateFee = async (
tx: Record<string, unknown>, tx: Record<string, unknown>,
@@ -23,10 +22,7 @@ const estimateFee = async (
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(copyTx, keypair) const { signedTransaction } = sign(copyTx, keypair)
const res = await xrplSend({ command: 'fee', tx_blob: signedTransaction }) const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction })
if (res.error) {
throw new Error(`[${res.error}] ${res.error_exception}.`);
}
if (res && res.drops) { if (res && res.drops) {
return res.drops return res.drops
} }
@@ -34,8 +30,7 @@ const estimateFee = async (
} catch (err) { } catch (err) {
if (!opts.silent) { if (!opts.silent) {
console.error(err) console.error(err)
const msg = err instanceof Error ? err.message : 'Error estimating fee!'; toast.error('Cannot estimate fee.') // ? Some better msg
toast.error(msg);
} }
return null return null
} }

View File

@@ -13,23 +13,3 @@ export const capitalize = (value?: string) => {
return value[0].toLocaleUpperCase() + value.slice(1) return value[0].toLocaleUpperCase() + value.slice(1)
} }
export const getFileExtention = (filename?: string): string | undefined => {
if (!filename) return
const ext = (filename.includes('.') && filename.split('.').pop()) || undefined
return ext
}
type Type = "array" | "undefined" | "object" | "string" | "number" | "bigint" | "boolean" | "symbol" | "function"
type obj = Record<string | number | symbol, unknown>
type arr = unknown[]
export const typeIs = <T extends Type>(arg: any, t: T | T[]): arg is unknown & (T extends "array" ? arr : T extends "undefined" ? undefined | null : T extends "object" ? obj : T extends "string" ? string : T extends "number" ? number : T extends "bigint" ? bigint : T extends "boolean" ? boolean : T extends "symbol" ? symbol : T extends "function" ? Function : never) => {
const types = Array.isArray(t) ? t : [t]
return types.includes(typeOf(arg) as T);
}
export const typeOf = (arg: any): Type => {
const type = arg instanceof Array ? 'array' : arg === null ? 'undefined' : typeof arg
return type;
}

View File

@@ -23,22 +23,23 @@ export const tts = {
ttNFTOKEN_BURN: 26, ttNFTOKEN_BURN: 26,
ttNFTOKEN_CREATE_OFFER: 27, ttNFTOKEN_CREATE_OFFER: 27,
ttNFTOKEN_CANCEL_OFFER: 28, ttNFTOKEN_CANCEL_OFFER: 28,
ttNFTOKEN_ACCEPT_OFFER: 29, ttNFTOKEN_ACCEPT_OFFER: 29
ttINVOKE: 99,
} }
export type TTS = typeof tts export type TTS = typeof tts
const calculateHookOn = (arr: (keyof TTS)[]) => { const calculateHookOn = (arr: (keyof TTS)[]) => {
let s = '0x3e3ff5bf' let start = '0x000000003e3ff5bf'
arr.forEach(n => { arr.forEach(n => {
let v = BigInt(s) let v = BigInt(start)
v ^= BigInt(1) << BigInt(tts[n]) v ^= BigInt(1) << BigInt(tts[n as keyof TTS])
s = "0x" + v.toString(16) let s = v.toString(16)
let l = s.length
if (l < 16) s = '0'.repeat(16 - l) + s
s = '0x' + s
start = s
}) })
s = s.replace('0x', '') return start.substring(2)
s = s.padStart(64, '0')
return s
} }
export default calculateHookOn export default calculateHookOn

59
utils/libwabt.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,9 @@
import { typeIs, typeOf } from './helpers'
export const extractSchemaProps = <O extends object>(obj: O) => export const extractSchemaProps = <O extends object>(obj: O) =>
Object.entries(obj).reduce((prev, [key, val]) => { Object.entries(obj).reduce((prev, [key, val]) => {
const value = typeIs(val, "object") && '$type' in val && '$value' in val ? val?.$value : val const typeOf = <T>(arg: T) =>
arg instanceof Array ? 'array' : arg === null ? 'undefined' : typeof arg
const value = typeOf(val) === 'object' && '$type' in val && '$value' in val ? val?.$value : val
const type = typeOf(value) const type = typeOf(value)
let schema: any = { let schema: any = {
@@ -11,19 +12,19 @@ export const extractSchemaProps = <O extends object>(obj: O) =>
default: value default: value
} }
if (typeIs(value, "array")) { if (typeOf(value) === 'array') {
const item = value[0] // TODO merge other item schema's into one const item = value[0] // TODO merge other item schema's into one
if (typeIs(item, "object")) { if (typeOf(item) !== 'object') {
schema.items = { schema.items = {
type: 'object', type: 'object',
properties: extractSchemaProps(item), properties: extractSchemaProps(item),
default: item default: item
} }
} }
// TODO primitive-value arrays // TODO support primitive-value arrays
} }
if (typeIs(value, "object")) { if (typeOf(value) === 'object') {
schema.properties = extractSchemaProps(value) schema.properties = extractSchemaProps(value)
} }
return { return {

View File

@@ -20,13 +20,6 @@ export type SetHookData = {
} }
$metaData?: any $metaData?: any
}[] }[]
Memos?: {
Memo: {
MemoType: string,
MemoData: string
MemoFormat: string
}
}[]
// HookGrants: { // HookGrants: {
// HookGrant: { // HookGrant: {
// Authorize: string; // Authorize: string;
@@ -81,19 +74,3 @@ export const getInvokeOptions = (content?: string) => {
return invokeOptions return invokeOptions
} }
export function toHex(str: string) {
var result = ''
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16)
}
return result.toUpperCase()
}
export function fromHex(hex: string) {
var str = ''
for (var i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16))
}
return str
}

View File

@@ -22,7 +22,6 @@ import hooksFloatManipPure from './md/hooks-float-manip-pure.md'
import hooksFloatOnePure from './md/hooks-float-one-pure.md' import hooksFloatOnePure from './md/hooks-float-one-pure.md'
import hooksFloatPure from './md/hooks-float-pure.md' import hooksFloatPure from './md/hooks-float-pure.md'
import hooksGuardCalled from './md/hooks-guard-called.md' import hooksGuardCalled from './md/hooks-guard-called.md'
import hooksGuardCallNonConst from './md/hooks-guard-call-non-const.md'
import hooksGuardInFor from './md/hooks-guard-in-for.md' import hooksGuardInFor from './md/hooks-guard-in-for.md'
import hooksGuardInWhile from './md/hooks-guard-in-while.md' import hooksGuardInWhile from './md/hooks-guard-in-while.md'
import hooksHashBufLen from './md/hooks-hash-buf-len.md' import hooksHashBufLen from './md/hooks-hash-buf-len.md'
@@ -71,7 +70,6 @@ const docs: { [key: string]: string } = {
'hooks-float-one-pure': hooksFloatOnePure, 'hooks-float-one-pure': hooksFloatOnePure,
'hooks-float-pure': hooksFloatPure, 'hooks-float-pure': hooksFloatPure,
'hooks-guard-called': hooksGuardCalled, 'hooks-guard-called': hooksGuardCalled,
'hooks-guard-call-non-const': hooksGuardCallNonConst,
'hooks-guard-in-for': hooksGuardInFor, 'hooks-guard-in-for': hooksGuardInFor,
'hooks-guard-in-while': hooksGuardInWhile, 'hooks-guard-in-while': hooksGuardInWhile,
'hooks-hash-buf-len': hooksHashBufLen, 'hooks-hash-buf-len': hooksHashBufLen,

View File

@@ -1,6 +0,0 @@
# hooks-guard-call-non-const
Only compile-time constants can be used as an argument in loop GUARD call. This check warns if a non compile-time constant is used.
It also checks whether a compile-time constant is used as a first argument of `_g()` call and whether it is a unique value. If not - it warns.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/loops-and-guarding)

View File

@@ -1,35 +1,14 @@
# hooks-guard-in-for # hooks-guard-in-for
Consider the following for-loop in C: A guard is a marker that must be placed in your code at the top of each loop. Consider the following for-loop in C:
```c ```c
#define GUARD(maxiter) _g(__LINE__, (maxiter)+1) #define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
for (int i = 0; GUARD(3), i < 3; ++i)
for (int i = 0; GUARD(3), i < 3; ++i)
``` ```
To satisfy the guard rule when using a for-loop in C guard should be <BR/>
placed either in the condition part of the loop, or as a first call in loop body, e.g. This is the only way to satisfy the guard rule when using a for-loop in C.
```c
for(int i = 0; i < 3; ++i) {
GUARD(3);
}
```
In case of nested loops, the guard limit value should be
multiplied by a number of iterations in each loop, e.g.
```c
for(int i = 0; GUARD(3), i < 3; ++i) {
for (int j = 0; GUARD(17), j < 5; ++j)
}
```
```
(most descendant loop iterations + 1) * (each parent loops iterations) - 1
```
This check will warn if the GUARD call is missing and also it will propose a GUARD value based on the for loop initial value,
the increment and loop condition.
[Read more](https://xrpl-hooks.readme.io/v2.0/docs/loops-and-guarding) [Read more](https://xrpl-hooks.readme.io/v2.0/docs/loops-and-guarding)

913
yarn.lock

File diff suppressed because it is too large Load Diff