Compare commits

...

71 Commits

Author SHA1 Message Date
muzam1l
513db7bf09 Update asc. 2023-03-31 14:22:33 +05:30
muzam1l
969fd0f63c Merge branch 'main' into feat/ts-support. 2023-03-31 14:20:04 +05:30
muzamil
412e3f2bbf Merge pull request #303 from XRPLF/fix/hex-value
Take tx memo data and parameter value as hex.
2023-03-29 17:43:11 +05:30
muzam1l
dc37b1911a Take tx memo data and parameter value as hex. 2023-03-29 15:04:14 +05:30
muzamil
c348868c89 Merge pull request #302 from XRPLF/fix/hook-on
Fix HookOn initial value.
2023-03-28 16:28:35 +05:30
muzam1l
2c3cfebe3a Fix HookOn initial value. 2023-03-28 15:14:43 +05:30
muzamil
6265a9cdbf Merge pull request #300 from XRPLF/fix/to-hex
Fix hex logic.
2023-03-27 21:20:08 +05:30
muzamil
1321b498cf Merge pull request #299 from XRPLF/fix/invoke-tx
Add `Destination` field to Invoke transaction.
2023-03-27 21:18:35 +05:30
muzam1l
801d9778cb Fix hex logic. 2023-03-27 19:21:14 +05:30
muzam1l
2cf18ef61c Add Destination field to Invoke transaction. 2023-03-27 15:32:25 +05:30
muzamil
4d2dc16ce5 Merge pull request #296 from XRPLF/feat/account-ui
Extend transactions Account UI.
2023-03-23 20:45:27 +05:30
muzam1l
9ecf5478e6 Remove tx Destination default values. 2023-03-23 19:39:56 +05:30
muzam1l
6d1ef110b7 Token Issuer account UI. 2023-03-23 17:52:17 +05:30
muzam1l
b9edfcd63b Fix: nextjs build. 2023-03-23 16:55:32 +05:30
muzam1l
b653d9a9cb Fix: Remove last traces of custom Destination handling! 2023-03-23 16:47:25 +05:30
muzam1l
da28e0a7d1 Use "creatable select" for accounts. 2023-03-23 16:45:24 +05:30
muzam1l
8a5b83d57f Fix: Don't remove fields in JSON mode if empty. 2023-03-23 16:26:29 +05:30
muzam1l
025eff6cf2 Add Select UI for account. 2023-03-23 16:12:35 +05:30
muzam1l
62d521b2cc Add owner field to NFTokenCreateOffer. 2023-03-23 15:51:36 +05:30
muzam1l
7aafca21df Remove Destination as special field. 2023-03-23 15:49:17 +05:30
muzamil
80f58e903c Merge pull request #294 from XRPLF/feat/amount-ui
Transaction amount UI.
2023-03-20 14:04:30 +05:30
muzam1l
c4af3df017 Add DeliverMin field to tx CheckCash. 2023-03-17 20:10:57 +05:30
muzam1l
5d8d142bc4 Catch all estimate fee errors. 2023-03-17 19:01:32 +05:30
muzam1l
e27a71d713 Estimate fee correct error message and amount input type. 2023-03-17 18:57:03 +05:30
muzam1l
e08b07cbeb Update tx OfferCreate. 2023-03-17 18:03:37 +05:30
muzam1l
e4936c03ef Css changes. 2023-03-17 17:17:29 +05:30
muzam1l
21a69ac8ea minor type fix. 2023-03-17 16:59:12 +05:30
muzam1l
52e4f219f7 Transaction amount UI. 2023-03-17 16:49:00 +05:30
muzamil
e1f34c4beb Merge pull request #291 from XRPLF/fix/sequence-ui
Fix Account sequence UI reset.
2023-03-14 21:30:35 +05:30
muzam1l
54a89c969e Fix account sequence reset. 2023-03-14 15:38:09 +05:30
muzamil
ded867d997 Merge pull request #289 from XRPLF/fix/tx
Account sequence.
2023-03-10 17:31:55 +05:30
muzam1l
0fce9af77c Fetch sequence on account creation. 2023-03-10 16:18:35 +05:30
muzam1l
55c68c580a Account Sequence UI. 2023-03-10 16:02:32 +05:30
muzamil
832a7997d1 Merge pull request #288 from XRPLF/fix/tx
Fix tx json saving and discarding.
2023-03-08 21:32:36 +05:30
muzam1l
4528e5a16e Actually fix Json 'save'. 2023-03-08 20:33:23 +05:30
muzam1l
38f064c6d8 Fix json saving and discarding. 2023-03-08 17:13:36 +05:30
muzamil
fbf4565dbc Merge pull request #287 from XRPLF/feat/memos-ui
Memos UI
2023-03-07 17:32:32 +05:30
muzam1l
9001c64fed minor label changes. 2023-03-07 16:58:32 +05:30
muzam1l
03b768db4e UI for memos fields. 2023-03-07 16:03:47 +05:30
muzam1l
825af0db89 Add memos field in transactions. 2023-03-06 21:14:14 +05:30
muzamil
31043f33ab Merge pull request #286 from XRPLF/feat/tx-params-ui
HookParameters UI for transactions.
2023-03-06 16:06:19 +05:30
muzam1l
39699a1cb9 HookParameters UI for transactions. 2023-03-03 18:32:03 +05:30
muzam1l
b50b300307 Refactor tx. 2023-03-03 16:00:43 +05:30
muzamil
82c06cbb12 Merge pull request #285 from XRPLF/fix/params
Add invoke hookon option.
2023-03-02 19:17:38 +05:30
muzam1l
423ee18e6a Add Invoke HookOn option. 2023-03-02 15:11:44 +05:30
muzam1l
bd1a0c9836 Remove some logs. 2023-02-21 16:30:01 +05:30
muzam1l
ba95eb5be2 Merge branch 'main' into feat/ts-support 2023-02-20 19:52:33 +05:30
muzam1l
6911bcc0f3 Upgrade to official @eqlabs/assemblyscript. 2023-02-20 19:33:54 +05:30
muzamil
3bb26d0c9b Merge pull request #281 from XRPLF/feat/testnet-v3
Testnet v3.
2023-02-10 16:12:22 +05:30
muzam1l
43c83d0de6 Fix delete hook and some refactor. 2023-02-10 14:43:51 +05:30
muzam1l
6bb407cb0f Better package loading experience. 2023-02-09 22:15:22 +05:30
muzamil
d7b29ba809 Merge pull request #282 from XRPLF/feat/bundle-xrpl
Bundle patched `xrpl-accountlib` and expose through require syntax.
2023-02-09 20:22:43 +05:30
muzam1l
ca81d8ad41 Bundle xrpl-accountlib and expose throw require syntax. 2023-02-09 15:41:06 +05:30
muzam1l
a7e59d7b73 Add UriToken transaction. 2023-02-07 14:32:06 +05:30
muzam1l
8f6b28cef5 Add Invoke transaction type. 2023-02-07 14:15:07 +05:30
muzam1l
c1815f272b refactor. 2023-02-07 14:00:57 +05:30
muzam1l
76871a8041 Testnet v3. 2023-02-06 21:09:11 +05:30
muzam1l
a6055feb1f update asc (top level return) 2023-02-03 15:26:24 +05:30
muzam1l
31a185f49f Enable top level api. 2023-02-02 17:44:15 +05:30
muzam1l
d693458421 fix wat state not updating. 2023-02-01 21:58:25 +05:30
muzam1l
692bbeb9b2 minor fixes. 2023-02-01 04:27:10 +05:30
muzam1l
a29368d633 update wabt 1.0.30 -> 1.0.32 2023-01-30 15:41:42 +05:30
muzam1l
0f32fcb71c asc 0.0.8. 2023-01-19 20:05:07 +05:30
muzam1l
90db6b1f12 assemblyscript 0.0.6. 2023-01-16 18:36:25 +05:30
muzam1l
dde0317544 Update assemblyscript. 2023-01-13 14:36:26 +05:30
muzam1l
a6ad39f6e8 Update nextjs. 2023-01-11 21:08:17 +05:30
muzam1l
0536e48cfe Fix some type errors. 2023-01-11 20:17:21 +05:30
muzam1l
a08823a38b Better lang warning. 2023-01-11 20:01:02 +05:30
muzam1l
653e28583d Compile ts files. 2023-01-11 19:52:45 +05:30
mariopil
d2033c8035 Hooks docs update (#279)
* Added hooks-guard-call-non-const checker doc, updated hooks-guard-in-for doc.
2023-01-09 13:43:32 +01:00
muzam1l
e75443c8e2 Detect ts files. 2023-01-06 19:58:03 +05:30
49 changed files with 6162 additions and 2974 deletions

View File

@@ -5,7 +5,8 @@ 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-v2.xrpl-labs.com" NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com"
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com" NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com"
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com" NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com"
NEXT_PUBLIC_SITE_URL=http://localhost:3000 NEXT_PUBLIC_NETWORK_ID="21338"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"

5
.gitignore vendored
View File

@@ -23,6 +23,7 @@
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.yarn
# local env files # local env files
.env.local .env.local
@@ -33,3 +34,7 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.vscode .vscode
# yarn
.yarnrc.yml
.yarn/

View File

@@ -33,6 +33,7 @@ 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,
@@ -301,7 +302,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 =>
snap.client?.send({ xrplSend({
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
@@ -329,7 +330,7 @@ const Accounts: FC<AccountProps> = props => {
} }
}) })
const objectRequests = snap.accounts.map(acc => { const objectRequests = snap.accounts.map(acc => {
return snap.client?.send({ return xrplSend({
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

@@ -94,7 +94,7 @@ const DeployEditor = () => {
}} }}
> >
{`You haven't compiled any files yet, compile files on `} {`You haven't compiled any files yet, compile files on `}
<NextLink shallow href={`/develop/${router.query.slug}`} passHref> <NextLink legacyBehavior shallow href={`/develop/${router.query.slug}`} passHref>
<Link as="a">develop view</Link> <Link as="a">develop view</Link>
</NextLink> </NextLink>
</Text> </Text>

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: 10000, zIndex: 3000,
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,

View File

@@ -155,9 +155,11 @@ const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
> >
<Image <Image
src={session?.user?.image || ''} src={session?.user?.image || ''}
width="30px" width="30"
height="30px" height="30"
objectFit="cover" style={{
objectFit: 'cover'
}}
alt="User avatar" alt="User avatar"
/> />
</Box> </Box>

View File

@@ -40,7 +40,6 @@ const EnrichLog: FC<EnrichLogProps> = ({ str }) => {
if (match.startsWith('HookSet')) { if (match.startsWith('HookSet')) {
const code = match.match(/^HookSet\((\d+)\)/)?.[1] const code = match.match(/^HookSet\((\d+)\)/)?.[1]
const val = hookSetCodes.find(v => code && v.code === +code) const val = hookSetCodes.find(v => code && v.code === +code)
console.log({ code, val })
if (!val) return match if (!val) return match
const content = capitalize(val.description) || 'No hint available!' const content = capitalize(val.description) || 'No hint available!'

View File

@@ -94,6 +94,14 @@ const setMarkers = (monacoE: typeof monaco) => {
}) })
} }
const langWarnings: Record<string, { shown: boolean; message: string }> = {
ts: {
shown: false,
message:
'Typescript suppport for hooks is still in early planning stage, write actual hooks in C only for now!'
}
}
const HooksEditor = () => { const HooksEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>() const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>()
const monacoRef = useRef<typeof monaco>() const monacoRef = useRef<typeof monaco>()
@@ -125,6 +133,14 @@ const HooksEditor = () => {
const file = snap.files[snap.active] const file = snap.files[snap.active]
useEffect(() => {
let warning = langWarnings[file?.language || '']
if (warning && !warning.shown) {
alert(warning.message) // TODO Custom dialog.
warning.shown = true
}
}, [file])
const renderNav = () => ( const renderNav = () => (
<Tabs <Tabs
label="File" label="File"
@@ -221,7 +237,7 @@ const HooksEditor = () => {
monaco.languages.register({ monaco.languages.register({
id: 'text', id: 'text',
extensions: ['.txt'], extensions: ['.txt'],
mimetypes: ['text/plain'], mimetypes: ['text/plain']
}) })
MonacoServices.install(monaco) MonacoServices.install(monaco)
const webSocket = createWebSocket( const webSocket = createWebSocket(
@@ -240,7 +256,7 @@ const HooksEditor = () => {
try { try {
disposable.dispose() disposable.dispose()
} catch (err) { } catch (err) {
console.log('err', err) console.error('err', err)
} }
}) })
} }

View File

@@ -160,7 +160,7 @@ export const Log: FC<ILog> = ({
)} )}
<Pre>{message}</Pre> <Pre>{message}</Pre>
{link && ( {link && (
<NextLink href={link} shallow passHref> <NextLink legacyBehavior href={link} shallow passHref>
<Link as="a">{linkText}</Link> <Link as="a">{linkText}</Link>
</NextLink> </NextLink>
)} )}

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import Link from 'next/link' import NextLink from 'next/link'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@@ -78,7 +78,7 @@ const Navigation = () => {
pr: '$4' pr: '$4'
}} }}
> >
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref> <NextLink legacyBehavior href={gistId ? `/develop/${gistId}` : '/develop'} passHref>
<Box <Box
as="a" as="a"
css={{ css={{
@@ -89,7 +89,7 @@ const Navigation = () => {
> >
<Logo width="32px" height="32px" /> <Logo width="32px" height="32px" />
</Box> </Box>
</Link> </NextLink>
<Flex <Flex
css={{ css={{
ml: '$5', ml: '$5',
@@ -105,7 +105,8 @@ const Navigation = () => {
<Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}> <Text css={{ fontSize: '$xs', color: '$mauve10', lineHeight: 1 }}>
{snap.files.length > 0 ? 'Gist: ' : 'Builder'} {snap.files.length > 0 ? 'Gist: ' : 'Builder'}
{snap.files.length > 0 && ( {snap.files.length > 0 && (
<Link <NextLink
legacyBehavior
href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`} href={`https://gist.github.com/${snap.gistOwner || ''}/${snap.gistId || ''}`}
passHref passHref
> >
@@ -117,7 +118,7 @@ const Navigation = () => {
> >
{`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`} {`${snap.gistOwner || '-'}/${truncate(snap.gistId || '')}`}
</Text> </Text>
</Link> </NextLink>
)} )}
</Text> </Text>
</> </>
@@ -336,29 +337,39 @@ const Navigation = () => {
}} }}
> >
<ButtonGroup> <ButtonGroup>
<Link href={gistId ? `/develop/${gistId}` : '/develop'} passHref shallow> <NextLink
legacyBehavior
href={gistId ? `/develop/${gistId}` : '/develop'}
passHref
shallow
>
<Button as="a" outline={!router.pathname.includes('/develop')} uppercase> <Button as="a" outline={!router.pathname.includes('/develop')} uppercase>
Develop Develop
</Button> </Button>
</Link> </NextLink>
<Link href={gistId ? `/deploy/${gistId}` : '/deploy'} passHref shallow> <NextLink
legacyBehavior
href={gistId ? `/deploy/${gistId}` : '/deploy'}
passHref
shallow
>
<Button as="a" outline={!router.pathname.includes('/deploy')} uppercase> <Button as="a" outline={!router.pathname.includes('/deploy')} uppercase>
Deploy Deploy
</Button> </Button>
</Link> </NextLink>
<Link href={gistId ? `/test/${gistId}` : '/test'} passHref shallow> <NextLink legacyBehavior href={gistId ? `/test/${gistId}` : '/test'} passHref shallow>
<Button as="a" outline={!router.pathname.includes('/test')} uppercase> <Button as="a" outline={!router.pathname.includes('/test')} uppercase>
Test Test
</Button> </Button>
</Link> </NextLink>
</ButtonGroup> </ButtonGroup>
<Link href="https://xrpl-hooks.readme.io/v2.0" passHref> <NextLink legacyBehavior href="https://xrpl-hooks.readme.io/v2.0" passHref>
<a target="_blank" rel="noreferrer noopener"> <a target="_blank" rel="noreferrer noopener">
<Button outline> <Button outline>
<BookOpen size="15px" /> <BookOpen size="15px" />
</Button> </Button>
</a> </a>
</Link> </NextLink>
</Stack> </Stack>
</Flex> </Flex>
</Container> </Container>

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 = (code: string, data?: Record<string, any>) => { const generateHtmlTemplate = async (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,8 +29,10 @@ const generateHtmlTemplate = (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>
@@ -72,7 +74,9 @@ const generateHtmlTemplate = (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>
@@ -100,6 +104,7 @@ 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']
@@ -127,16 +132,31 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
return fields return fields
}, [content]) }, [content])
const runScript = useCallback(() => { const runScript = useCallback(async () => {
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 = generateHtmlTemplate(content, data) const template = await 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 = [
@@ -145,6 +165,7 @@ 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(() => {
@@ -174,11 +195,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(() => { const handleRun = useCallback(async () => {
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 = []
runScript() await runScript();
setIsDialogOpen(false) setIsDialogOpen(false)
}, [isDisabled, runScript]) }, [isDisabled, runScript])
@@ -279,7 +300,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} onClick={handleRun}> <Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
Run script Run script
</Button> </Button>
</Flex> </Flex>

View File

@@ -3,13 +3,11 @@ 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 } from 'react-select' import type { Props, StylesConfig } 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 })
// eslint-disable-next-line react/display-name const getColors = (isDark: boolean) => {
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,
@@ -30,15 +28,17 @@ const Select = forwardRef<any, Props>((props, ref) => {
} }
colors.outline = colors.background colors.outline = colors.background
colors.selected = colors.secondary colors.selected = colors.secondary
return ( return colors
<SelectInput }
ref={ref}
menuPosition={props.menuPosition || 'fixed'} const getStyles = (isDark: boolean) => {
styles={{ const colors = getColors(isDark)
const styles: StylesConfig = {
container: provided => { container: provided => {
return { return {
...provided, ...provided,
position: 'relative' position: 'relative',
width: '100%'
} }
}, },
singleValue: provided => ({ singleValue: provided => ({
@@ -107,16 +107,57 @@ const Select = forwardRef<any, Props>((props, ref) => {
dropdownIndicator: (provided, state) => { dropdownIndicator: (provided, state) => {
return { return {
...provided, ...provided,
padding: 6,
color: state.isFocused ? colors.border : colors.secondary, color: state.isFocused ? colors.border : colors.secondary,
':hover': { ':hover': {
color: colors.border 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 (
<SelectInput
ref={ref}
menuPosition={props.menuPosition || 'fixed'}
styles={styles}
{...props}
/>
)
})
// eslint-disable-next-line react/display-name
const Creatable = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return (
<CreatableSelectInput
ref={ref}
formatCreateLabel={label => `Enter "${label}"`}
menuPosition={props.menuPosition || 'fixed'}
styles={styles}
{...props} {...props}
/> />
) )
}) })
export default styled(Select, {}) export default styled(Select, {})
export const CreatableSelect = styled(Creatable, {})

71
components/Sequence.tsx Normal file
View File

@@ -0,0 +1,71 @@
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,6 +21,7 @@ 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 }) => {
@@ -190,6 +191,10 @@ 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

@@ -20,6 +20,7 @@ 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 { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
import { SetHookData, toHex } from '../../utils/setHook'
export interface TransactionProps { export interface TransactionProps {
header: string header: string
@@ -40,20 +41,40 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => { (state: Partial<TransactionState> = txState) => {
const { selectedTransaction, selectedDestAccount, selectedAccount, txFields, selectedFlags } = const {
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 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: value }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: data, MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({ return prepareTransaction({
...txFields, ...txFields,
HookParameters,
Flags, Flags,
TransactionType, TransactionType,
Destination, Account,
Account Memos
}) })
}, },
[txState] [txState]
@@ -69,15 +90,29 @@ 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') {
// save the editor state first st = saveEditorState(editorValue, tt)
const pst = prepareState(editorValue || '', tt) if (!st) return
if (!pst) return
st = setState(pst)
} }
const account = accounts.find(acc => acc.address === selectedAccount?.value) const account = accounts.find(acc => acc.address === selectedAccount?.value)
@@ -90,11 +125,12 @@ 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
const fields = getTxFields(options.TransactionType) Object.keys(options).forEach(field => {
if (fields.Destination && !options.Destination) { if (!options[field]) {
throw Error('Destination account is required!') delete options[field]
} }
})
await sendTransaction(account, options, { logPrefix }) await sendTransaction(account, options, { logPrefix })
} catch (error) { } catch (error) {
@@ -108,23 +144,18 @@ 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)
@@ -134,13 +165,6 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
selectedTransaction: transactionType selectedTransaction: transactionType
} }
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null
fields.Destination = ''
} else {
fields.Destination = undefined
}
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) { if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags) nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
fields.Flags = undefined fields.Flags = undefined
@@ -177,11 +201,21 @@ 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}
@@ -189,6 +223,7 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
/> />
) : ( ) : (
<TxUI <TxUI
switchToJson={switchToJson}
state={txState} state={txState}
resetState={resetState} resetState={resetState}
setState={setState} setState={setState}
@@ -209,8 +244,8 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
<Button <Button
onClick={() => { onClick={() => {
if (viewType === 'ui') { if (viewType === 'ui') {
setState({ viewType: 'json' }) switchToJson()
} else setState({ viewType: 'ui' }) } else switchToUI()
}} }}
outline outline
> >

View File

@@ -1,6 +1,6 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import state, { prepareState, transactionsData, TransactionState } from '../../state' import state, { 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?: (state?: Partial<TransactionState>) => string getJsonString: (st?: 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> = ({ getJsonString, state: txState, header, setState }) => { export const TxJson: FC<JsonProps> = ({
getJsonString,
state: txState,
header,
setState,
saveEditorState
}) => {
const { editorSettings, accounts } = useSnapshot(state) const { editorSettings, accounts } = useSnapshot(state)
const { editorValue, estimatedFee } = txState const { editorValue, estimatedFee, editorIsSaved } = 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,29 +44,19 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
} }
}, [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) {
saveState(value, currTxType) saveEditorState(value, currTxType)
return return
} }
showAlert('Error!', { showAlert('Error!', {
@@ -163,8 +153,6 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
}) })
}, [getSchemas, monacoInst]) }, [getSchemas, monacoInst])
const hasUnsaved = useMemo(() => editorValue !== getJsonString?.(), [editorValue, getJsonString])
return ( return (
<Monaco <Monaco
rootProps={{ rootProps={{
@@ -174,7 +162,7 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
id={header} id={header}
height="100%" height="100%"
value={editorValue} value={editorValue}
onChange={val => setState({ editorValue: val })} onChange={val => setState({ editorValue: val, editorIsSaved: false })}
onMount={(editor, monaco) => { onMount={(editor, monaco) => {
editor.updateOptions({ editor.updateOptions({
minimap: { enabled: false }, minimap: { enabled: false },
@@ -190,12 +178,12 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
model?.onWillDispose(() => onExit(model.getValue())) model?.onWillDispose(() => onExit(model.getValue()))
}} }}
overlay={ overlay={
hasUnsaved ? ( !editorIsSaved ? (
<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={() => saveState(editorValue || '', currTxType)}> <Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}>
save save
</Link> </Link>
<Link css={{ ml: '$1' }} onClick={discardChanges}> <Link css={{ ml: '$1' }} onClick={discardChanges}>

View File

@@ -1,34 +1,43 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { FC, ReactNode, useCallback, useEffect, 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 from '../Select' import Select, { CreatableSelect } 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 { Button } from '..' import { Box, Button } from '..'
import Textarea from '../Textarea' import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags' 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 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> = ({ state: txState, setState, resetState, estimateFee }) => { export const TxUI: FC<UIProps> = ({
state: txState,
setState,
resetState,
estimateFee,
switchToJson
}) => {
const { accounts } = useSnapshot(state) const { accounts } = useSnapshot(state)
const { selectedAccount, selectedDestAccount, selectedTransaction, txFields, selectedFlags } = const { selectedAccount, selectedTransaction, txFields, selectedFlags, hookParameters, memos } =
txState txState
const accountOptions: SelectOption[] = accounts.map(acc => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
@@ -36,13 +45,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
value: acc.address value: acc.address
})) }))
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address
}))
.filter(acc => acc.value !== selectedAccount?.value)
const flagsOptions: SelectOption[] = Object.entries( const flagsOptions: SelectOption[] = Object.entries(
getFlags(selectedTransaction?.value) || {} getFlags(selectedTransaction?.value) || {}
).map(([label, value]) => ({ ).map(([label, value]) => ({
@@ -71,6 +73,22 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[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)
@@ -94,8 +112,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[handleEstimateFee, resetState, setState] [handleEstimateFee, resetState, setState]
) )
const switchToJson = () => setState({ viewType: 'json' })
// default tx // default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return if (selectedTransaction?.value) return
@@ -105,22 +121,23 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
} }
}, [handleChangeTxType, selectedTransaction?.value]) }, [handleChangeTxType, selectedTransaction?.value])
const fields = useMemo( const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
)
const richFields = ['TransactionType', 'Account']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
if (flagsOptions.length) { if (flagsOptions.length) {
richFields.push('Flags') richFields.push('Flags')
} }
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields] const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
const amountOptions = [
{ label: 'XRP', value: 'xrp' },
{ label: 'Token', value: 'token' }
] as const
const defaultTokenAmount = {
value: '0',
currency: '',
issuer: ''
}
return ( return (
<Container <Container
css={{ css={{
@@ -130,94 +147,32 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
}} }}
> >
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}> <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<Flex <TxField label="Transaction type">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
mt: '1px',
pr: '1px'
}}
>
<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: '70%' }}
value={selectedTransaction} value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)} onChange={(tt: any) => handleChangeTxType(tt)}
/> />
</Flex> </TxField>
<Flex <TxField label="Account">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<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: '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
/> />
</Flex> </TxField>
{richFields.includes('Destination') && ( <TxField label="Sequence">
<Flex <AccountSequence address={selectedAccount?.value} />
row </TxField>
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', textAlign: 'end' }}>
Destination account:{' '}
</Text>
<Select
instanceId="to-account"
placeholder="Select the destination account"
css={{ width: '70%' }}
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</Flex>
)}
{richFields.includes('Flags') && ( {richFields.includes('Flags') && (
<Flex <TxField label="Flags">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Flags:{' '}
</Text>
<Select <Select
isClearable isClearable
css={{ width: '70%' }}
instanceId="flags" instanceId="flags"
placeholder="Select flags to apply" placeholder="Select flags to apply"
menuPosition="fixed" menuPosition="fixed"
@@ -229,41 +184,139 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
} }
/> />
</Flex> </TxField>
)} )}
{otherFields.map(field => { {otherFields.map(field => {
let _value = txFields[field] let _value = txFields[field]
let value: string | undefined let value: string | undefined
if (typeof _value === 'object') { if (typeIs(_value, 'object')) {
if (_value.$type === 'json' && typeof _value.$value === 'object') { if (_value.$type === 'json' && typeIs(_value.$value, ['object', 'array'])) {
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 isXrp = typeof _value === 'object' && _value.$type === 'xrp' const isAccount = typeIs(_value, 'object') && _value.$type === 'account'
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 ( return (
<Flex column key={field} css={{ mb: '$2', pr: '1px' }}> <TxField key={field} label={field}>
<Flex fluid css={{ alignItems: 'center' }}>
{isTokenAmount ? (
<Flex <Flex
row
fluid 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={{ css={{
justifyContent: 'flex-end', ml: '$2',
alignItems: 'center', width: '150px'
position: 'relative'
}} }}
> >
<Text muted css={{ mr: '$3' }}> <Select
{field + (isXrp ? ' (XRP)' : '')}:{' '} instanceId="currency-type"
</Text> 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 (
<TxField key={field} label={field}>
{isJson ? ( {isJson ? (
<Textarea <Textarea
rows={rows} rows={rows}
@@ -271,7 +324,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
spellCheck={false} spellCheck={false}
onChange={switchToJson} onChange={switchToJson}
css={{ css={{
width: '70%',
flex: 'inherit', flex: 'inherit',
resize: 'vertical' resize: 'vertical'
}} }}
@@ -298,7 +350,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
: undefined : undefined
} }
css={{ css={{
width: '70%',
flex: 'inherit', flex: 'inherit',
'-moz-appearance': 'textfield', '-moz-appearance': 'textfield',
'&::-webkit-outer-spin-button': { '&::-webkit-outer-spin-button': {
@@ -333,11 +384,200 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
Suggest Suggest
</Button> </Button>
)} )}
</Flex> </TxField>
</Flex>
) )
})} })}
<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
css={{ mx: '$2' }}
placeholder="Value (hex-quoted)"
value={value}
onChange={e => {
setState({
hookParameters: {
...hookParameters,
[id]: { label, value: e.target.value }
}
})
}}
/>
<Button
onClick={() => {
const { [id]: _, ...rest } = hookParameters
setState({ hookParameters: rest })
}}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</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 (hex-quoted)"
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,7 +2,10 @@
{ {
"TransactionType": "AccountDelete", "TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", "Destination": {
"$type": "account",
"$value": ""
},
"DestinationTag": 13, "DestinationTag": 13,
"Fee": "2000000", "Fee": "2000000",
"Sequence": 2470665, "Sequence": 2470665,
@@ -28,7 +31,11 @@
"TransactionType": "CheckCash", "TransactionType": "CheckCash",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"DeliverMin": {
"$value": "",
"$type": "amount.xrp"
}, },
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12" "Fee": "12"
@@ -36,7 +43,10 @@
{ {
"TransactionType": "CheckCreate", "TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", "Destination": {
"$type": "account",
"$value": ""
},
"SendMax": "100000000", "SendMax": "100000000",
"Expiration": 570113521, "Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B", "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
@@ -54,7 +64,10 @@
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Fee": "10" "Fee": "10"
}, },
@@ -63,9 +76,12 @@
"TransactionType": "EscrowCreate", "TransactionType": "EscrowCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958, "CancelAfter": 533257958,
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
@@ -76,7 +92,10 @@
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish", "TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000", "Fulfillment": "A0028000",
@@ -120,10 +139,17 @@
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Flags": "1", "Flags": "1",
"Destination": "", "Destination": {
"$type": "account",
"$value": ""
},
"Owner": {
"$type": "account",
"$value": ""
},
"Fee": "10" "Fee": "10"
}, },
{ {
@@ -142,19 +168,29 @@
"Flags": "0", "Flags": "0",
"LastLedgerSequence": 7108682, "LastLedgerSequence": 7108682,
"Sequence": 8, "Sequence": 8,
"TakerGets": "6000000", "TakerGets": {
"Amount": { "$type": "amount.xrp",
"$value": "100", "$value": "6000000"
"$type": "xrp" },
"TakerPays": {
"$type": "amount.token",
"$value": {
"currency": "GKO",
"issuer": "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
"value": "2"
}
} }
}, },
{ {
"TransactionType": "Payment", "TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Destination": {
"$type": "account",
"$value": ""
},
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Fee": "12", "Fee": "12",
"Flags": "2147483648", "Flags": "2147483648",
@@ -165,9 +201,12 @@
"TransactionType": "PaymentChannelCreate", "TransactionType": "PaymentChannelCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400, "SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
@@ -181,7 +220,7 @@
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": { "Amount": {
"$value": "200", "$value": "200",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Expiration": 543171558, "Expiration": 543171558,
"Fee": "10" "Fee": "10"
@@ -237,7 +276,7 @@
"Flags": "262144", "Flags": "262144",
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"LimitAmount": { "LimitAmount": {
"$type": "json", "$type": "amount.token",
"$value": { "$value": {
"currency": "USD", "currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc", "issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
@@ -245,5 +284,18 @@
} }
}, },
"Sequence": 12 "Sequence": 12
},
{
"TransactionType": "Invoke",
"Destination": {
"$type": "account",
"$value": ""
},
"Fee": "12"
},
{
"TransactionType": "UriToken",
"Fee": "12",
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
} }
] ]

View File

@@ -1,7 +1,7 @@
import type { NextRequest, NextFetchEvent } from 'next/server' import type { NextRequest } from 'next/server'
import { NextResponse as Response } from 'next/server' import { NextResponse as Response } from 'next/server'
export default function middleware(req: NextRequest, ev: NextFetchEvent) { export default function middleware(req: NextRequest) {
if (req.nextUrl.pathname === '/') { if (req.nextUrl.pathname === '/') {
const url = req.nextUrl.clone() const url = req.nextUrl.clone()
url.pathname = '/develop' url.pathname = '/develop'

View File

@@ -8,11 +8,19 @@ module.exports = {
config.resolve.alias['vscode'] = require.resolve( config.resolve.alias['vscode'] = require.resolve(
'@codingame/monaco-languageclient/lib/vscode-compatibility' '@codingame/monaco-languageclient/lib/vscode-compatibility'
) )
config.experiments = {
topLevelAwait: true,
layers: true
}
if (!isServer) { if (!isServer) {
config.resolve.fallback.fs = false config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
module: false
}
} }
config.module.rules.push({ config.module.rules.push({
test: /\.md$/, test: [/\.md$/, /hook-bundle\.js$/],
use: 'raw-loader' use: 'raw-loader'
}) })
return config return config

View File

@@ -8,11 +8,13 @@
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write .", "format": "prettier --write .",
"postinstall": "patch-package" "postinstall": "patch-package && yarn run postinstall-postinstall",
"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",
"@codingame/monaco-languageclient": "^0.17.0", "@codingame/monaco-languageclient": "^0.17.0",
"@eqlabs/assemblyscript": "^0.0.0-alpha.1680097351",
"@monaco-editor/react": "^4.4.5", "@monaco-editor/react": "^4.4.5",
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
@@ -36,7 +38,7 @@
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"lodash.xor": "^4.5.0", "lodash.xor": "^4.5.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^13.1.1",
"next-auth": "^4.10.3", "next-auth": "^4.10.3",
"next-plausible": "^3.2.0", "next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
@@ -48,8 +50,8 @@
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"re-resizable": "^6.9.1", "re-resizable": "^6.9.1",
"react": "17.0.2", "react": "^18.2.0",
"react-dom": "17.0.2", "react-dom": "^18.2.0",
"react-hook-form": "^7.28.0", "react-hook-form": "^7.28.0",
"react-hot-keys": "^2.7.1", "react-hot-keys": "^2.7.1",
"react-hot-toast": "^2.1.1", "react-hot-toast": "^2.1.1",
@@ -64,9 +66,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.32",
"xrpl-accountlib": "^1.5.2", "xrpl-accountlib": "^1.6.1",
"xrpl-client": "^1.9.4" "xrpl-client": "^2.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dinero.js": "^1.9.0", "@types/dinero.js": "^1.9.0",
@@ -75,8 +77,9 @@
"@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": "^13.1.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.4.4" "typescript": "4.4.4"
}, },

View File

@@ -24,7 +24,7 @@ import { ChatCircleText } from 'phosphor-react'
TimeAgo.setDefaultLocale(en.locale) TimeAgo.setDefaultLocale(en.locale)
TimeAgo.addLocale(en) TimeAgo.addLocale(en)
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps<{ session?: any }>) {
const router = useRouter() const router = useRouter()
const slug = router.query?.slug const slug = router.query?.slug
const gistId = (Array.isArray(slug) && slug[0]) ?? null const gistId = (Array.isArray(slug) && slug[0]) ?? null

View File

@@ -40,7 +40,7 @@ export default async function handler(
} }
return res.status(200).json(json) return res.status(200).json(json)
} catch (err) { } catch (err) {
console.log(err) console.error(err)
return res.status(500).json({ error: 'Server error' }) return res.status(500).json({ error: 'Server error' })
} }
return res.status(500).json({ error: 'Not able to create faucet, try again' }) return res.status(500).json({ error: 'Not able to create faucet, try again' })

View File

@@ -150,7 +150,9 @@ const Home: NextPage = () => {
const activeFile = snap.files[snap.active] as IFile | undefined const activeFile = snap.files[snap.active] as IFile | undefined
const activeFileExt = getFileExtention(activeFile?.name) const activeFileExt = getFileExtention(activeFile?.name)
const canCompile = activeFileExt === 'c' || activeFileExt === 'wat' const canCompile = activeFileExt === 'c' || activeFileExt === 'wat' || activeFileExt === 'ts'
const isCompiling = snap.compiling.includes(snap.active);
return ( return (
<Split <Split
direction="vertical" direction="vertical"
@@ -166,7 +168,9 @@ const Home: NextPage = () => {
{canCompile && ( {canCompile && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() =>
snap.compiling === undefined && snap.files.length && compileCode(snap.active)
}
> >
<Flex <Flex
css={{ css={{
@@ -183,7 +187,7 @@ const Home: NextPage = () => {
variant="primary" variant="primary"
uppercase uppercase
disabled={!snap.files.length} disabled={!snap.files.length}
isLoading={snap.compiling} isLoading={isCompiling}
onClick={() => compileCode(snap.active)} onClick={() => compileCode(snap.active)}
> >
<Play weight="bold" size="16px" /> <Play weight="bold" size="16px" />
@@ -200,7 +204,9 @@ const Home: NextPage = () => {
{activeFileExt === 'js' && ( {activeFileExt === 'js' && (
<Hotkeys <Hotkeys
keyName="command+b,ctrl+b" keyName="command+b,ctrl+b"
onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)} onKeyDown={() =>
!isCompiling && snap.files.length && compileCode(snap.active)
}
> >
<Flex <Flex
css={{ css={{

File diff suppressed because it is too large Load Diff

5
raw-loader.d.ts vendored
View File

@@ -2,3 +2,8 @@ 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,5 +1,6 @@
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',
@@ -35,26 +36,23 @@ 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) { if (!showToast) return;
return toast.error(json.error, { id: toastId }) return toast.error(json.error, { id: toastId })
} else {
return
}
} else {
if (showToast) {
toast.success('New account created', { id: toastId })
} }
const currNames = state.accounts.map(acc => acc.name) const currNames = state.accounts.map(acc => acc.name)
const info = await fetchAccountInfo(json.address, { silent: true })
state.accounts.push({ state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0], name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(), xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address, address: json.address,
secret: json.secret, secret: json.secret,
sequence: 1, sequence: info?.Sequence || 1,
hooks: [], hooks: [],
isLoading: false, isLoading: false,
version: '2' version: '2'
}) })
if (showToast) {
toast.success('New account created', { id: toastId })
} }
} }

View File

@@ -1,40 +1,91 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Router from 'next/router' import Router from 'next/router'
import state from '../index' import state, { IFile } from '../index'
import { saveFile } from './saveFile' import { saveFile } from './saveFile'
import { decodeBinary } from '../../utils/decodeBinary' import { decodeBinary } from '../../utils/decodeBinary'
import { ref } from 'valtio' import { ref } from 'valtio'
import asc from "@eqlabs/assemblyscript/dist/asc"
import { getFileExtention } from '../../utils/helpers'
/* compileCode sends the code of the active file to compile endpoint type CompilationResult = Pick<IFile, "compiledContent" | "compiledWatContent">
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false, activeId)
const file = state.files[activeId]
// Bail out if we're already compiling the file.
if (!file || state.compiling.includes(activeId)) {
return
}
try {
state.compiling.push(activeId)
state.logs = []
file.containsErrors = false
let result: CompilationResult;
if (file.name.endsWith('.wat')) {
result = await compileWat(file);
}
else if (file.language === "ts") {
result = await compileTs(file);
}
else if (navigator?.onLine === false) {
throw Error('You seem offline, check you internet connection and try again!')
}
else if (file.language === 'c') {
result = await compileC(file)
}
else throw Error("Unknown file type.")
file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content;
file.compiledContent = result.compiledContent;
file.compiledWatContent = result.compiledWatContent;
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.error(err)
let message: string;
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: 'error',
message
})
})
message = "Compilation errors occurred, see logs for more info."
} else if (err instanceof Error) {
message = err.message
} else {
message = 'Something went wrong, try again later!'
}
toast.error(message, { position: 'bottom-center' })
file.containsErrors = true
}
state.compiling = state.compiling.filter(id => id !== activeId);
}
/* compileC sends the code of the active file to compile endpoint
* If all goes well you will get base64 encoded wasm file back with * If all goes well you will get base64 encoded wasm file back with
* some extra logging information if we can provide it. This function * some extra logging information if we can provide it. This function
* also decodes the returned wasm and creates human readable WAT file * also decodes the returned wasm and creates human readable WAT file
* out of it and store both in global state. * out of it and store both in global state.
*/ */
export const compileCode = async (activeId: number) => { export const compileC = async (file: IFile): Promise<CompilationResult> => {
// Save the file to global state
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 C compile endpoint!')
} }
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return // TODO Inform user about it.
return
}
// Set loading state to true
state.compiling = true
state.logs = []
try {
file.containsErrors = false
let res: Response let res: Response
try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, { res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -54,11 +105,8 @@ export const compileCode = async (activeId: number) => {
] ]
}) })
}) })
} catch (error) {
throw Error('Something went wrong, check your network connection and try again!')
}
const json = await res.json() const json = await res.json()
state.compiling = false
if (!json.success) { if (!json.success) {
const errors = [json.message] const errors = [json.message]
if (json.tasks && json.tasks.length > 0) { if (json.tasks && json.tasks.length > 0) {
@@ -84,88 +132,87 @@ export const compileCode = async (activeId: number) => {
const wast = myModule.toText({ foldExprs: false, inlineExport: false }) const wast = myModule.toText({ foldExprs: false, inlineExport: false })
file.compiledContent = ref(bufferData) return {
file.lastCompiled = new Date() compiledContent: ref(bufferData),
file.compiledValueSnapshot = file.content compiledWatContent: wast
file.compiledWatContent = wast }
} catch (error) { } catch (error) {
throw Error('Invalid compilation result produced, check your code for errors and try again!') throw Error('Invalid compilation result produced, check your code for errors and try again!')
} }
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)
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: 'error',
message
})
})
} else if (err instanceof Error) {
state.logs.push({
type: 'error',
message: err.message
})
} else {
state.logs.push({
type: 'error',
message: 'Something went wrong, come back later!'
})
} }
state.compiling = false export const compileWat = async (file: IFile): Promise<CompilationResult> => {
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
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 wabt = await (await import('wabt')).default()
const module = wabt.parseWat(file.name, file.content); const mod = wabt.parseWat(file.name, file.content);
module.resolveNames(); mod.resolveNames();
module.validate(); mod.validate();
const { buffer } = module.toBinary({ const { buffer } = mod.toBinary({
log: false, log: false,
write_debug_names: true, write_debug_names: true,
}); });
file.compiledContent = ref(buffer) return {
file.lastCompiled = new Date() compiledContent: ref(buffer),
file.compiledValueSnapshot = file.content compiledWatContent: file.content,
file.compiledWatContent = file.content }
}
toast.success('Compiled successfully!', { position: 'bottom-center' }) export const compileTs = async (file: IFile): Promise<CompilationResult> => {
return new Promise(async (resolve, reject) => {
let result: Partial<CompilationResult> = {}
const { error, stdout, stderr } = await asc.main([
// Command line options
file.name,
"--outFile", `${file.name}.wasm`,
"--textFile", `${file.name}.wat`,
"--runtime", "stub",
"--initialMemory", "1",
"--maximumMemory", "1",
"--noExportMemory",
"--optimize",
"--topLevelToHook"
], {
readFile: (name, baseDir) => {
const file = state.files.find(file => file.name === name)
if (file) {
return file.content
}
return null
},
writeFile: async (name, data: ArrayBuffer | string, baseDir) => {
const ext = getFileExtention(name);
if (ext === 'wasm') {
result.compiledContent = data as ArrayBuffer;
}
else if (ext === 'wat') {
result.compiledWatContent = data as string;
}
if (result.compiledContent && result.compiledWatContent) {
resolve({ ...result });
}
},
listFiles: (dirname, baseDir) => {
return state.files.map(file => file.name)
},
// reportDiagnostic?: ...,
});
let logMsg = stdout.toString()
let errMsg = stderr.toString()
if (logMsg) {
state.logs.push({ state.logs.push({
type: 'success', type: "log",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`, message: logMsg
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
} }
if (errMsg) {
state.logs.push({ state.logs.push({
type: 'error', type: "error",
message message: errMsg
}) })
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
} }
state.compiling = false
if (error) return reject(error)
})
} }

View File

@@ -1,8 +1,8 @@
import { getFileExtention } from '../../utils/helpers' import { getFileExtention } from '../../utils/helpers'
import state, { IFile } from '../index' import state, { IFile, ILang } from '../index'
const languageMapping: Record<string, string | undefined> = { const languageMapping: Record<string, ILang | undefined> = {
ts: 'typescript', ts: 'ts',
js: 'javascript', js: 'javascript',
md: 'markdown', md: 'markdown',
c: 'c', c: 'c',

View File

@@ -14,11 +14,4 @@ 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,8 +6,9 @@ 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 } from '../../utils/setHook' import { SetHookData, toHex } 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)
@@ -17,13 +18,6 @@ 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) {
@@ -64,9 +58,6 @@ 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
@@ -87,12 +78,13 @@ export const prepareDeployHookTx = async (
// } // }
// } // }
// }); // });
if (typeof window !== 'undefined') { if (typeof window === 'undefined') return
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: {
@@ -111,36 +103,31 @@ export const prepareDeployHookTx = async (
} }
return tx return tx
} }
}
/* deployHook function turns the wasm binary into /*
* hex string, signs the transaction and deploys it to * Turns the wasm binary into hex string, signs the transaction and deploys it to Hooks testnet.
* Hooks testnet.
*/ */
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => { export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
if (typeof window !== 'undefined') {
const activeFile = state.files[state.active]?.compiledContent const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active] ? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0] : state.files.filter(file => file.compiledContent)[0]
state.deployValues[activeFile.name] = data state.deployValues[activeFile.name] = data
const tx = await prepareDeployHookTx(account, data) const tx = await prepareDeployHookTx(account, data)
if (!tx) { if (!tx) {
return return
} }
if (!state.client) {
return
}
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, keypair) const { signedTransaction } = sign(tx, keypair)
const currentAccount = state.accounts.find(acc => acc.address === account.address) const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = true currentAccount.isLoading = true
} }
let submitRes
let submitRes
try { try {
submitRes = await state.client?.send({ submitRes = await xrplSend({
command: 'submit', command: 'submit',
tx_blob: signedTransaction tx_blob: signedTransaction
}) })
@@ -185,7 +172,7 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
}) })
} }
} catch (err) { } catch (err) {
console.log(err) console.error(err)
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: 'error',
message: 'Error occurred while deploying' message: 'Error occurred while deploying'
@@ -196,22 +183,18 @@ export const deployHook = async (account: IAccount & { name?: string }, data: Se
} }
return submitRes 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
} }
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: '100000', Fee: '100000',
NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
Hooks: [ Hooks: [
{ {
Hook: { Hook: {
@@ -221,25 +204,22 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
} }
] ]
} }
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret)
try { try {
// Update tx Fee value with network estimation // Update tx Fee value with network estimation
const res = await estimateFee(tx, account) const res = await estimateFee(tx, account)
tx['Fee'] = res?.base_fee ? res?.base_fee : '1000' tx['Fee'] = res?.base_fee || '1000'
} catch (err) { } catch (err) {
// use default value what you defined earlier console.error(err)
console.log(err)
} }
const { signedTransaction } = sign(tx, keypair) const { signedTransaction } = sign(tx, keypair)
if (currentAccount) { if (currentAccount) {
currentAccount.isLoading = true currentAccount.isLoading = true
} }
let submitRes let submitRes
const toastId = toast.loading('Deleting hook...') const toastId = toast.loading('Deleting hook...')
try { try {
submitRes = await state.client.send({ submitRes = await xrplSend({
command: 'submit', command: 'submit',
tx_blob: signedTransaction tx_blob: signedTransaction
}) })
@@ -267,7 +247,7 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}) })
} }
} catch (err) { } catch (err) {
console.log(err) console.error(err)
toast.error('Error occurred while deleting hook', { id: toastId }) toast.error('Error occurred while deleting hook', { id: toastId })
state.deployLogs.push({ state.deployLogs.push({
type: 'error', type: 'error',
@@ -279,4 +259,3 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
} }
return submitRes return submitRes
} }
}

View File

@@ -1,5 +1,5 @@
import { Octokit } from '@octokit/core' import { Octokit } from '@octokit/core'
import state, { IFile } from '../index' import state, { IFile, ILang } from '../index'
import { templateFileIds } from '../constants' import { templateFileIds } from '../constants'
const octokit = new Octokit() const octokit = new Octokit()
@@ -48,7 +48,7 @@ export const fetchFiles = async (gistId: string) => {
const files: IFile[] = Object.keys(res.data.files).map(filename => ({ const files: IFile[] = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'untitled.c', name: res.data.files?.[filename]?.filename || 'untitled.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '', language: res.data.files?.[filename]?.language?.toLowerCase() as ILang | undefined,
content: res.data.files?.[filename]?.content || '' content: res.data.files?.[filename]?.content || ''
})) }))

View File

@@ -1,66 +0,0 @@
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,20 +21,23 @@ 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, // TODO auto-fillable default Fee,
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 state.client.send({ const response = await xrplSend({
command: 'submit', command: 'submit',
tx_blob: signedTransaction tx_blob: signedTransaction
}) })

View File

@@ -55,7 +55,7 @@ export const syncToGist = async (session?: Session | null, createNewGist?: boole
return toast.success('Updated to gist successfully!', { id: toastId }) return toast.success('Updated to gist successfully!', { id: toastId })
}) })
.catch(err => { .catch(err => {
console.log(err) console.error(err)
state.gistLoading = false state.gistLoading = false
return toast.error(`Could not update Gist, try again later!`, { return toast.error(`Could not update Gist, try again later!`, {
id: toastId id: toastId
@@ -85,7 +85,7 @@ export const syncToGist = async (session?: Session | null, createNewGist?: boole
return toast.success('Created new gist successfully!', { id: toastId }) return toast.success('Created new gist successfully!', { id: toastId })
}) })
.catch(err => { .catch(err => {
console.log(err) console.error(err)
state.gistLoading = false state.gistLoading = false
return toast.error(`Could not create Gist, try again later!`, { return toast.error(`Could not create Gist, try again later!`, {
id: toastId id: toastId

View File

@@ -0,0 +1,7 @@
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

@@ -9,9 +9,10 @@ declare module 'valtio' {
function snapshot<T extends object>(p: T): T function snapshot<T extends object>(p: T): T
} }
export type ILang = "ts" | "javascript" | "markdown" | "c" | "text"
export interface IFile { export interface IFile {
name: string name: string
language: string language: ILang | undefined
content: string content: string
compiledValueSnapshot?: string compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null compiledContent?: ArrayBuffer | null
@@ -66,7 +67,7 @@ export interface IState {
loading: boolean loading: boolean
gistLoading: boolean gistLoading: boolean
zipLoading: boolean zipLoading: boolean
compiling: boolean compiling: /* file id */ number[]
logs: ILog[] logs: ILog[]
deployLogs: ILog[] deployLogs: ILog[]
transactionLogs: ILog[] transactionLogs: ILog[]
@@ -78,7 +79,7 @@ export interface IState {
splits: { splits: {
[id: string]: SplitSize [id: string]: SplitSize
} }
client: XrplClient | null client: XrplClient
clientStatus: 'offline' | 'online' clientStatus: 'offline' | 'online'
mainModalOpen: boolean mainModalOpen: boolean
mainModalShowed: boolean mainModalShowed: boolean
@@ -98,7 +99,7 @@ let initialState: IState = {
// Active file index on the Deploy page editor // Active file index on the Deploy page editor
activeWat: 0, activeWat: 0,
loading: false, loading: false,
compiling: false, compiling: [],
logs: [], logs: [],
deployLogs: [], deployLogs: [],
transactionLogs: [], transactionLogs: [],
@@ -113,7 +114,7 @@ let initialState: IState = {
tabSize: 2 tabSize: 2
}, },
splits: {}, splits: {},
client: null, client: undefined!, // set below only.
clientStatus: 'offline' as 'offline', clientStatus: 'offline' as 'offline',
mainModalOpen: false, mainModalOpen: false,
mainModalShowed: false, mainModalShowed: false,
@@ -135,7 +136,7 @@ if (typeof window !== 'undefined') {
try { try {
localStorageAccounts = localStorage.getItem('hooksIdeAccounts') localStorageAccounts = localStorage.getItem('hooksIdeAccounts')
} catch (err) { } catch (err) {
console.log(`localStorage state broken`) console.error(`localStorage state broken`)
localStorage.removeItem('hooksIdeAccounts') localStorage.removeItem('hooksIdeAccounts')
} }
if (localStorageAccounts) { if (localStorageAccounts) {
@@ -153,9 +154,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

@@ -5,35 +5,55 @@ 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 { 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
selectedDestAccount: SelectOption | null
selectedFlags: SelectOption[] | null selectedFlags: 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]>,
'Account' | 'Sequence' | 'TransactionType' typeof commonFields[number]
> >
export const defaultTransaction: TransactionState = { export const defaultTransaction: TransactionState = {
selectedTransaction: null, selectedTransaction: null,
selectedAccount: null, selectedAccount: null,
selectedDestAccount: null,
selectedFlags: null, selectedFlags: null,
hookParameters: {},
memos: {},
editorIsSaved: true,
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
@@ -109,46 +129,51 @@ 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]
// convert xrp if (!typeIs(_value, 'object')) return
if (_value && typeof _value === 'object' && _value.$type === 'xrp') { // amount.xrp
if (+_value.$value) { if (_value.$type === 'amount.xrp') {
options[field] = (+_value.$value * 1000000 + '') as any if (_value.$value) {
options[field] = (+(_value as any).$value * 1000000 + '')
} else { } else {
options[field] = undefined // 👇 💀 options[field] = ""
} }
} }
// handle type: `json` // amount.token
if (_value && typeof _value === 'object' && _value.$type === 'json') { if (_value.$type === 'amount.token') {
if (typeof _value.$value === 'object') { if (typeIs(_value.$value, 'string')) {
options[field] = parseJSON(_value.$value)
} else if (typeIs(_value.$value, 'object')) {
options[field] = _value.$value options[field] = _value.$value
} else { } else {
try { options[field] = undefined
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') {
options[field] = (_value.$value as any)?.toString() || ""
}
// 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;
} }
// delete unnecessary fields
if (!options[field]) {
delete options[field]
} }
}) })
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) {
@@ -158,7 +183,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return return
} }
const { Account, TransactionType, Destination, ...rest } = options const { Account, TransactionType, HookParameters, Memos, ...rest } = options
let tx: Partial<TransactionState> = {} let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType) const schema = getTxFields(transactionType)
@@ -188,25 +213,23 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null tx.selectedTransaction = null
} }
if (schema.Destination !== undefined) { if (HookParameters && HookParameters instanceof Array) {
const dest = state.accounts.find(acc => acc.address === Destination) tx.hookParameters = HookParameters.reduce<TransactionState["hookParameters"]>((acc, cur, idx) => {
if (dest) { const param = { label: fromHex(cur.HookParameter?.HookParameterName || ""), value: cur.HookParameter?.HookParameterValue || "" }
tx.selectedDestAccount = { acc[idx] = param;
label: dest.name, return acc;
value: dest.address }, {})
} }
} else if (Destination) {
tx.selectedDestAccount = { if (Memos && Memos instanceof Array) {
label: Destination, tx.memos = Memos.reduce<TransactionState["memos"]>((acc, cur, idx) => {
value: Destination const memo = { data: cur.Memo?.MemoData || "", type: fromHex(cur.Memo?.MemoType || ""), format: fromHex(cur.Memo?.MemoFormat || "") }
} acc[idx] = memo;
} else { return acc;
tx.selectedDestAccount = null }, {})
}
} else if (Destination) {
rest.Destination = Destination
} }
if (getFlags(TransactionType) && rest.Flags) { if (getFlags(TransactionType) && rest.Flags) {
const flags = extractFlags(TransactionType, rest.Flags) const flags = extractFlags(TransactionType, rest.Flags)
@@ -217,17 +240,31 @@ export const prepareState = (value: string, transactionType?: string) => {
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 =
typeof value !== 'object' && const isAmount = schemaVal &&
schemaVal && typeIs(schemaVal, "object") &&
typeof schemaVal === 'object' && schemaVal.$type.startsWith('amount.');
schemaVal.$type === 'xrp' const isAccount = schemaVal &&
if (isXrp) { typeIs(schemaVal, "object") &&
schemaVal.$type.startsWith("account");
if (isAmount && ["number", "string"].includes(typeof value)) {
rest[field] = { rest[field] = {
$type: 'xrp', $type: 'amount.xrp', // TODO narrow typed $type.
$value: +value / 1000000 // ! maybe use bigint? $value: +value / 1000000 // ! maybe use bigint?
} }
} else if (typeof value === 'object') { } else if (isAmount && 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
@@ -236,6 +273,7 @@ export const prepareState = (value: string, transactionType?: string) => {
}) })
tx.txFields = rest tx.txFields = rest
tx.editorIsSaved = true;
return tx return tx
} }
@@ -246,12 +284,12 @@ export const getTxFields = (tt?: string) => {
if (!txFields) return {} if (!txFields) return {}
let _txFields = Object.keys(txFields) let _txFields = Object.keys(txFields)
.filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key)) .filter(key => !commonFields.includes(key as any))
.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 } export { transactionsData, commonFields }
export const transactionsOptions = transactionsData.map(tx => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,

31
utils/accountInfo.ts Normal file
View File

@@ -0,0 +1,31 @@
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,6 +1,7 @@
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 state, { IAccount } from '../state' import { IAccount } from '../state'
import { xrplSend } from '../state/actions/xrpl-client'
const estimateFee = async ( const estimateFee = async (
tx: Record<string, unknown>, tx: Record<string, unknown>,
@@ -22,7 +23,10 @@ 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 state.client?.send({ command: 'fee', tx_blob: signedTransaction }) const res = await xrplSend({ 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
} }
@@ -30,7 +34,8 @@ const estimateFee = async (
} catch (err) { } catch (err) {
if (!opts.silent) { if (!opts.silent) {
console.error(err) console.error(err)
toast.error('Cannot estimate fee.') // ? Some better msg const msg = err instanceof Error ? err.message : 'Error estimating fee!';
toast.error(msg);
} }
return null return null
} }

View File

@@ -19,3 +19,23 @@ export const getFileExtention = (filename?: string): string | undefined => {
const ext = (filename.includes('.') && filename.split('.').pop()) || undefined const ext = (filename.includes('.') && filename.split('.').pop()) || undefined
return ext return ext
} }
export const getFileNamePart = (filename?: string): string | undefined => {
if (!filename) return
const name = (filename.includes('.') && filename.split('.').slice(0, -1).join(".")) || filename
return name
}
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,23 +23,22 @@ 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 start = '0x000000003e3ff5bf' let s = '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff'
arr.forEach(n => { arr.forEach(n => {
let v = BigInt(start) let v = BigInt(s)
v ^= BigInt(1) << BigInt(tts[n as keyof TTS]) v ^= BigInt(1) << BigInt(tts[n])
let s = v.toString(16) s = "0x" + v.toString(16)
let l = s.length
if (l < 16) s = '0'.repeat(16 - l) + s
s = '0x' + s
start = s
}) })
return start.substring(2) s = s.replace('0x', '')
s = s.padStart(64, '0')
return s
} }
export default calculateHookOn export default calculateHookOn

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,8 @@
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 typeOf = <T>(arg: T) => const value = typeIs(val, "object") && '$type' in val && '$value' in val ? val?.$value : val
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 = {
@@ -12,19 +11,19 @@ export const extractSchemaProps = <O extends object>(obj: O) =>
default: value default: value
} }
if (typeOf(value) === 'array') { if (typeIs(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 (typeOf(item) !== 'object') { if (typeIs(item, "object")) {
schema.items = { schema.items = {
type: 'object', type: 'object',
properties: extractSchemaProps(item), properties: extractSchemaProps(item),
default: item default: item
} }
} }
// TODO support primitive-value arrays // TODO primitive-value arrays
} }
if (typeOf(value) === 'object') { if (typeIs(value, "object")) {
schema.properties = extractSchemaProps(value) schema.properties = extractSchemaProps(value)
} }
return { return {

View File

@@ -20,6 +20,13 @@ export type SetHookData = {
} }
$metaData?: any $metaData?: any
}[] }[]
Memos?: {
Memo: {
MemoType: string,
MemoData: string
MemoFormat: string
}
}[]
// HookGrants: { // HookGrants: {
// HookGrant: { // HookGrant: {
// Authorize: string; // Authorize: string;
@@ -74,3 +81,20 @@ export const getInvokeOptions = (content?: string) => {
return invokeOptions return invokeOptions
} }
export function toHex(str: string) {
var result = ''
for (var i = 0; i < str.length; i++) {
const hex = str.charCodeAt(i).toString(16)
result += hex.padStart(2, '0')
}
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,6 +22,7 @@ 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'
@@ -70,6 +71,7 @@ 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

@@ -0,0 +1,6 @@
# 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,14 +1,35 @@
# hooks-guard-in-for # hooks-guard-in-for
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: 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)
``` ```
<BR/> To satisfy the guard rule when using a for-loop in C guard should be
This is the only way to satisfy the guard rule when using a for-loop in C. placed either in the condition part of the loop, or as a first call in loop body, e.g.
```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)

2255
yarn.lock

File diff suppressed because it is too large Load Diff