Compare commits
	
		
			42 Commits
		
	
	
		
			feat/engin
			...
			fix/tx
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0fce9af77c | ||
| 
						 | 
					55c68c580a | ||
| 
						 | 
					832a7997d1 | ||
| 
						 | 
					4528e5a16e | ||
| 
						 | 
					38f064c6d8 | ||
| 
						 | 
					fbf4565dbc | ||
| 
						 | 
					9001c64fed | ||
| 
						 | 
					03b768db4e | ||
| 
						 | 
					825af0db89 | ||
| 
						 | 
					31043f33ab | ||
| 
						 | 
					39699a1cb9 | ||
| 
						 | 
					b50b300307 | ||
| 
						 | 
					82c06cbb12 | ||
| 
						 | 
					423ee18e6a | ||
| 
						 | 
					3bb26d0c9b | ||
| 
						 | 
					43c83d0de6 | ||
| 
						 | 
					6bb407cb0f | ||
| 
						 | 
					d7b29ba809 | ||
| 
						 | 
					ca81d8ad41 | ||
| 
						 | 
					a7e59d7b73 | ||
| 
						 | 
					8f6b28cef5 | ||
| 
						 | 
					c1815f272b | ||
| 
						 | 
					76871a8041 | ||
| 
						 | 
					d2033c8035 | ||
| 
						 | 
					48daf1c5c8 | ||
| 
						 | 
					a3365e4beb | ||
| 
						 | 
					45d813cdad | ||
| 
						 | 
					911416aa2f | ||
| 
						 | 
					2d836af9ed | ||
| 
						 | 
					4f0fc838be | ||
| 
						 | 
					fa93912c38 | ||
| 
						 | 
					f5cb76c302 | ||
| 
						 | 
					df1d65dcab | ||
| 
						 | 
					1513f78991 | ||
| 
						 | 
					3a064f307b | ||
| 
						 | 
					6fca05f310 | ||
| 
						 | 
					31e67d382f | ||
| 
						 | 
					27475301e4 | ||
| 
						 | 
					934283976a | ||
| 
						 | 
					2d9ca2674e | ||
| 
						 | 
					0d9e9e7b45 | ||
| 
						 | 
					2c2bf59bcd | 
@@ -5,7 +5,8 @@ GITHUB_ID=""
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"
 | 
			
		||||
NEXT_PUBLIC_COMPILE_API_BASE_URL="http://localhost:9000"
 | 
			
		||||
NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT="ws://localhost:9000/language-server/c"
 | 
			
		||||
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v2.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v2-debugstream.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v2-explorer.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
 | 
			
		||||
NEXT_PUBLIC_TESTNET_URL="hooks-testnet-v3.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_DEBUG_STREAM_URL="hooks-testnet-v3-debugstream.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_EXPLORER_URL="hooks-testnet-v3-explorer.xrpl-labs.com"
 | 
			
		||||
NEXT_PUBLIC_NETWORK_ID="21338"
 | 
			
		||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -33,3 +33,7 @@ yarn-error.log*
 | 
			
		||||
# vercel
 | 
			
		||||
.vercel
 | 
			
		||||
.vscode
 | 
			
		||||
 | 
			
		||||
# yarn
 | 
			
		||||
.yarnrc.yml
 | 
			
		||||
.yarn/
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ import { addFunds } from '../state/actions/addFaucetAccount'
 | 
			
		||||
import { deleteHook } from '../state/actions/deployHook'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
import { deleteAccount } from '../state/actions/deleteAccount'
 | 
			
		||||
import { xrplSend } from '../state/actions/xrpl-client'
 | 
			
		||||
 | 
			
		||||
export const AccountDialog = ({
 | 
			
		||||
  activeAccountAddress,
 | 
			
		||||
@@ -301,7 +302,7 @@ const Accounts: FC<AccountProps> = props => {
 | 
			
		||||
    const fetchAccInfo = async () => {
 | 
			
		||||
      if (snap.clientStatus === 'online') {
 | 
			
		||||
        const requests = snap.accounts.map(acc =>
 | 
			
		||||
          snap.client?.send({
 | 
			
		||||
          xrplSend({
 | 
			
		||||
            id: `hooks-builder-req-info-${acc.address}`,
 | 
			
		||||
            command: 'account_info',
 | 
			
		||||
            account: acc.address
 | 
			
		||||
@@ -329,7 +330,7 @@ const Accounts: FC<AccountProps> = props => {
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const objectRequests = snap.accounts.map(acc => {
 | 
			
		||||
          return snap.client?.send({
 | 
			
		||||
          return xrplSend({
 | 
			
		||||
            id: `hooks-builder-req-objects-${acc.address}`,
 | 
			
		||||
            command: 'account_objects',
 | 
			
		||||
            account: acc.address
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ const contentShow = keyframes({
 | 
			
		||||
  '100%': { opacity: 1 }
 | 
			
		||||
})
 | 
			
		||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
 | 
			
		||||
  zIndex: 10000,
 | 
			
		||||
  zIndex: 3000,
 | 
			
		||||
  backgroundColor: blackA.blackA9,
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  inset: 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -200,15 +200,15 @@ const HooksEditor = () => {
 | 
			
		||||
            defaultValue={file?.content}
 | 
			
		||||
            // onChange={val => (state.files[snap.active].content = val)} // Auto save?
 | 
			
		||||
            beforeMount={monaco => {
 | 
			
		||||
              if (!snap.editorCtx) {
 | 
			
		||||
                snap.files.forEach(file =>
 | 
			
		||||
                  monaco.editor.createModel(
 | 
			
		||||
                    file.content,
 | 
			
		||||
                    file.language,
 | 
			
		||||
                    monaco.Uri.parse(`file:///work/c/${file.name}`)
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              // if (!snap.editorCtx) {
 | 
			
		||||
              //   snap.files.forEach(file =>
 | 
			
		||||
              //     monaco.editor.createModel(
 | 
			
		||||
              //       file.content,
 | 
			
		||||
              //       file.language,
 | 
			
		||||
              //       monaco.Uri.parse(`file:///work/c/${file.name}`)
 | 
			
		||||
              //     )
 | 
			
		||||
              //   )
 | 
			
		||||
              // }
 | 
			
		||||
 | 
			
		||||
              // create the web socket
 | 
			
		||||
              if (!subscriptionRef.current) {
 | 
			
		||||
@@ -218,6 +218,11 @@ const HooksEditor = () => {
 | 
			
		||||
                  aliases: ['C', 'c', 'H', 'h'],
 | 
			
		||||
                  mimetypes: ['text/plain']
 | 
			
		||||
                })
 | 
			
		||||
                monaco.languages.register({
 | 
			
		||||
                  id: 'text',
 | 
			
		||||
                  extensions: ['.txt'],
 | 
			
		||||
                  mimetypes: ['text/plain'],
 | 
			
		||||
                })
 | 
			
		||||
                MonacoServices.install(monaco)
 | 
			
		||||
                const webSocket = createWebSocket(
 | 
			
		||||
                  process.env.NEXT_PUBLIC_LANGUAGE_SERVER_API_ENDPOINT || ''
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import { saveFile } from '../../state/actions/saveFile'
 | 
			
		||||
import { getErrors, getTags } from '../../utils/comment-parser'
 | 
			
		||||
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
 | 
			
		||||
  const process = { env: { NODE_ENV: 'production' } } as any
 | 
			
		||||
  if (data) {
 | 
			
		||||
@@ -29,8 +29,10 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
 | 
			
		||||
      process.env[key] = data[key]
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  processString = JSON.stringify(process)
 | 
			
		||||
 | 
			
		||||
  const libs = (await import("xrpl-accountlib/dist/browser.hook-bundle.js")).default;
 | 
			
		||||
  return `
 | 
			
		||||
  <html>
 | 
			
		||||
  <head>
 | 
			
		||||
@@ -72,7 +74,9 @@ const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
 | 
			
		||||
 | 
			
		||||
      window.addEventListener('error', windowErrorHandler);
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
      ${libs}
 | 
			
		||||
    </script>
 | 
			
		||||
    <script type="module">
 | 
			
		||||
      ${code}
 | 
			
		||||
    </script>
 | 
			
		||||
@@ -100,6 +104,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
 | 
			
		||||
  const [fields, setFields] = useState<Fields>({})
 | 
			
		||||
  const [iFrameCode, setIframeCode] = useState('')
 | 
			
		||||
  const [isDialogOpen, setIsDialogOpen] = useState(false)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const getFields = useCallback(() => {
 | 
			
		||||
    const inputTags = ['input', 'param', 'arg', 'argument']
 | 
			
		||||
@@ -127,16 +132,31 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
 | 
			
		||||
    return fields
 | 
			
		||||
  }, [content])
 | 
			
		||||
 | 
			
		||||
  const runScript = useCallback(() => {
 | 
			
		||||
  const runScript = useCallback(async () => {
 | 
			
		||||
    setIsLoading(true);
 | 
			
		||||
    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 = {}
 | 
			
		||||
      Object.keys(fields).forEach(key => {
 | 
			
		||||
        data[key] = fields[key].value
 | 
			
		||||
      })
 | 
			
		||||
      const template = generateHtmlTemplate(content, data)
 | 
			
		||||
 | 
			
		||||
      const template = await generateHtmlTemplate(content, data)
 | 
			
		||||
      setIframeCode(template)
 | 
			
		||||
 | 
			
		||||
      loaded = true
 | 
			
		||||
      if (toastId) {
 | 
			
		||||
        toast.dismiss(toastId)
 | 
			
		||||
      }
 | 
			
		||||
      state.scriptLogs = [{ type: 'success', message: 'Started running...' }]
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      state.scriptLogs = [
 | 
			
		||||
@@ -145,6 +165,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
 | 
			
		||||
        { type: 'error', message: err?.message || 'Could not parse template' }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
    setIsLoading(false);
 | 
			
		||||
  }, [content, fields, snap.scriptLogs])
 | 
			
		||||
 | 
			
		||||
  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 handleRun = useCallback(() => {
 | 
			
		||||
  const handleRun = useCallback(async () => {
 | 
			
		||||
    if (isDisabled) return toast.error('Please fill in all the required fields.')
 | 
			
		||||
 | 
			
		||||
    state.scriptLogs = []
 | 
			
		||||
    runScript()
 | 
			
		||||
    await runScript();
 | 
			
		||||
    setIsDialogOpen(false)
 | 
			
		||||
  }, [isDisabled, runScript])
 | 
			
		||||
 | 
			
		||||
@@ -279,7 +300,7 @@ const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
 | 
			
		||||
              <DialogClose asChild>
 | 
			
		||||
                <Button outline>Cancel</Button>
 | 
			
		||||
              </DialogClose>
 | 
			
		||||
              <Button variant="primary" isDisabled={isDisabled} onClick={handleRun}>
 | 
			
		||||
              <Button variant="primary" isDisabled={isDisabled || isLoading} isLoading={isLoading} onClick={handleRun}>
 | 
			
		||||
                Run script
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Flex>
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,8 @@ const Select = forwardRef<any, Props>((props, ref) => {
 | 
			
		||||
        container: provided => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...provided,
 | 
			
		||||
            position: 'relative'
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            width: '100%'
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        singleValue: provided => ({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								components/Sequence.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								components/Sequence.tsx
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -21,6 +21,7 @@ import { prepareDeployHookTx, sha256 } from '../state/actions/deployHook'
 | 
			
		||||
import estimateFee from '../utils/estimateFee'
 | 
			
		||||
import { getParameters, getInvokeOptions, transactionOptions, SetHookData } from '../utils/setHook'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
import AccountSequence from './Sequence'
 | 
			
		||||
 | 
			
		||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
 | 
			
		||||
  ({ accountAddress }) => {
 | 
			
		||||
@@ -190,6 +191,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
 | 
			
		||||
                    onChange={(acc: any) => setSelectedAccount(acc)}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%', position: 'relative' }}>
 | 
			
		||||
                  <Label>Sequence</Label>
 | 
			
		||||
                  <AccountSequence address={selectedAccount?.value} />
 | 
			
		||||
                </Box>
 | 
			
		||||
                <Box css={{ width: '100%' }}>
 | 
			
		||||
                  <Label>Invoke on transactions</Label>
 | 
			
		||||
                  <Controller
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import {
 | 
			
		||||
} from './Dialog'
 | 
			
		||||
import { Plus, X } from 'phosphor-react'
 | 
			
		||||
import { styled } from '../stitches.config'
 | 
			
		||||
import { capitalize } from '../utils/helpers'
 | 
			
		||||
import { capitalize, getFileExtention } from '../utils/helpers'
 | 
			
		||||
import ContextMenu, { ContentMenuOption } from './ContextMenu'
 | 
			
		||||
 | 
			
		||||
const ErrorText = styled(Text, {
 | 
			
		||||
@@ -97,7 +97,7 @@ export const Tabs = ({
 | 
			
		||||
      if (!tabname) {
 | 
			
		||||
        return { error: `Please enter ${label.toLocaleLowerCase()} name.` }
 | 
			
		||||
      }
 | 
			
		||||
      let ext = (tabname.includes('.') && tabname.split('.').pop()) || ''
 | 
			
		||||
      let ext = getFileExtention(tabname)
 | 
			
		||||
 | 
			
		||||
      if (!ext && defaultExtension) {
 | 
			
		||||
        ext = defaultExtension
 | 
			
		||||
@@ -109,7 +109,7 @@ export const Tabs = ({
 | 
			
		||||
      if (extensionRequired && !ext) {
 | 
			
		||||
        return { error: 'File extension is required!' }
 | 
			
		||||
      }
 | 
			
		||||
      if (allowedExtensions && !allowedExtensions.includes(ext)) {
 | 
			
		||||
      if (allowedExtensions && ext && !allowedExtensions.includes(ext)) {
 | 
			
		||||
        return { error: 'This file extension is not allowed!' }
 | 
			
		||||
      }
 | 
			
		||||
      if (headerExtraValidation && !tabname.match(headerExtraValidation.regex)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,8 @@ import { TxJson } from './json'
 | 
			
		||||
import { TxUI } from './ui'
 | 
			
		||||
import { default as _estimateFee } from '../../utils/estimateFee'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
 | 
			
		||||
import { SetHookData, toHex } from '../../utils/setHook'
 | 
			
		||||
 | 
			
		||||
export interface TransactionProps {
 | 
			
		||||
  header: string
 | 
			
		||||
@@ -39,17 +41,43 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
 | 
			
		||||
  const prepareOptions = useCallback(
 | 
			
		||||
    (state: Partial<TransactionState> = txState) => {
 | 
			
		||||
      const { selectedTransaction, selectedDestAccount, selectedAccount, txFields } = state
 | 
			
		||||
      const {
 | 
			
		||||
        selectedTransaction,
 | 
			
		||||
        selectedDestAccount,
 | 
			
		||||
        selectedAccount,
 | 
			
		||||
        txFields,
 | 
			
		||||
        selectedFlags,
 | 
			
		||||
        hookParameters,
 | 
			
		||||
        memos
 | 
			
		||||
      } = state
 | 
			
		||||
 | 
			
		||||
      const TransactionType = selectedTransaction?.value || null
 | 
			
		||||
      const Destination = selectedDestAccount?.value || txFields?.Destination
 | 
			
		||||
      const Account = selectedAccount?.value || null
 | 
			
		||||
      const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
 | 
			
		||||
      const HookParameters = Object.entries(hookParameters || {}).reduce<
 | 
			
		||||
        SetHookData['HookParameters']
 | 
			
		||||
      >((acc, [_, { label, value }]) => {
 | 
			
		||||
        return acc.concat({
 | 
			
		||||
          HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
 | 
			
		||||
        })
 | 
			
		||||
      }, [])
 | 
			
		||||
      const Memos = memos
 | 
			
		||||
        ? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
 | 
			
		||||
            return acc?.concat({
 | 
			
		||||
              Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
 | 
			
		||||
            })
 | 
			
		||||
          }, [])
 | 
			
		||||
        : undefined
 | 
			
		||||
 | 
			
		||||
      return prepareTransaction({
 | 
			
		||||
        ...txFields,
 | 
			
		||||
        HookParameters,
 | 
			
		||||
        Flags,
 | 
			
		||||
        TransactionType,
 | 
			
		||||
        Destination,
 | 
			
		||||
        Account
 | 
			
		||||
        Account,
 | 
			
		||||
        Memos
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    [txState]
 | 
			
		||||
@@ -65,15 +93,29 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
    }
 | 
			
		||||
  }, [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 () => {
 | 
			
		||||
    let st: TransactionState | undefined
 | 
			
		||||
    const tt = txState.selectedTransaction?.value
 | 
			
		||||
    if (viewType === 'json') {
 | 
			
		||||
      // save the editor state first
 | 
			
		||||
      const pst = prepareState(editorValue || '', tt)
 | 
			
		||||
      if (!pst) return
 | 
			
		||||
 | 
			
		||||
      st = setState(pst)
 | 
			
		||||
      st = saveEditorState(editorValue, tt)
 | 
			
		||||
      if (!st) return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const account = accounts.find(acc => acc.address === selectedAccount?.value)
 | 
			
		||||
@@ -104,23 +146,18 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
    }
 | 
			
		||||
    setState({ txIsLoading: false })
 | 
			
		||||
  }, [
 | 
			
		||||
    txState.selectedTransaction?.value,
 | 
			
		||||
    viewType,
 | 
			
		||||
    accounts,
 | 
			
		||||
    txIsDisabled,
 | 
			
		||||
    setState,
 | 
			
		||||
    header,
 | 
			
		||||
    saveEditorState,
 | 
			
		||||
    editorValue,
 | 
			
		||||
    txState,
 | 
			
		||||
    selectedAccount?.value,
 | 
			
		||||
    prepareOptions
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const getJsonString = useCallback(
 | 
			
		||||
    (state?: Partial<TransactionState>) =>
 | 
			
		||||
      JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
 | 
			
		||||
    [editorSettings.tabSize, prepareOptions]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const resetState = useCallback(
 | 
			
		||||
    (transactionType: SelectOption | undefined = defaultTransactionType) => {
 | 
			
		||||
      const fields = getTxFields(transactionType?.value)
 | 
			
		||||
@@ -136,8 +173,13 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
      } else {
 | 
			
		||||
        fields.Destination = undefined
 | 
			
		||||
      }
 | 
			
		||||
      nwState.txFields = fields
 | 
			
		||||
 | 
			
		||||
      if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
 | 
			
		||||
        nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
 | 
			
		||||
        fields.Flags = undefined
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      nwState.txFields = fields
 | 
			
		||||
      const state = modifyTxState(header, nwState, { replaceState: true })
 | 
			
		||||
      const editorValue = getJsonString(state)
 | 
			
		||||
      return setState({ editorValue })
 | 
			
		||||
@@ -168,18 +210,34 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
    [accounts, prepareOptions, setState, txState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const switchToJson = useCallback(() => {
 | 
			
		||||
    const editorValue = getJsonString()
 | 
			
		||||
    setState({ viewType: 'json', editorValue })
 | 
			
		||||
  }, [getJsonString, setState])
 | 
			
		||||
 | 
			
		||||
  const switchToUI = useCallback(() => {
 | 
			
		||||
    setState({ viewType: 'ui' })
 | 
			
		||||
  }, [setState])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
 | 
			
		||||
      {viewType === 'json' ? (
 | 
			
		||||
        <TxJson
 | 
			
		||||
          getJsonString={getJsonString}
 | 
			
		||||
          saveEditorState={saveEditorState}
 | 
			
		||||
          header={header}
 | 
			
		||||
          state={txState}
 | 
			
		||||
          setState={setState}
 | 
			
		||||
          estimateFee={estimateFee}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <TxUI state={txState} setState={setState} estimateFee={estimateFee} />
 | 
			
		||||
        <TxUI
 | 
			
		||||
          switchToJson={switchToJson}
 | 
			
		||||
          state={txState}
 | 
			
		||||
          resetState={resetState}
 | 
			
		||||
          setState={setState}
 | 
			
		||||
          estimateFee={estimateFee}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <Flex
 | 
			
		||||
        row
 | 
			
		||||
@@ -195,8 +253,8 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (viewType === 'ui') {
 | 
			
		||||
              setState({ viewType: 'json' })
 | 
			
		||||
            } else setState({ viewType: 'ui' })
 | 
			
		||||
              switchToJson()
 | 
			
		||||
            } else switchToUI()
 | 
			
		||||
          }}
 | 
			
		||||
          outline
 | 
			
		||||
        >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import { FC, useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useSnapshot } from 'valtio'
 | 
			
		||||
import state, { prepareState, transactionsData, TransactionState } from '../../state'
 | 
			
		||||
import state, { transactionsData, TransactionState } from '../../state'
 | 
			
		||||
import Text from '../Text'
 | 
			
		||||
import { Flex, Link } from '..'
 | 
			
		||||
import { showAlert } from '../../state/actions/showAlert'
 | 
			
		||||
@@ -11,27 +11,27 @@ import Monaco from '../Monaco'
 | 
			
		||||
import type monaco from 'monaco-editor'
 | 
			
		||||
 | 
			
		||||
interface JsonProps {
 | 
			
		||||
  getJsonString?: (state?: Partial<TransactionState>) => string
 | 
			
		||||
  getJsonString: (st?: Partial<TransactionState>) => string
 | 
			
		||||
  saveEditorState: (val?: string, tt?: string) => TransactionState | undefined
 | 
			
		||||
  header?: string
 | 
			
		||||
  setState: (pTx?: Partial<TransactionState> | undefined) => void
 | 
			
		||||
  state: TransactionState
 | 
			
		||||
  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 { editorValue, estimatedFee } = txState
 | 
			
		||||
  const { editorValue, estimatedFee, editorIsSaved } = txState
 | 
			
		||||
  const [currTxType, setCurrTxType] = useState<string | undefined>(
 | 
			
		||||
    txState.selectedTransaction?.value
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setState({
 | 
			
		||||
      editorValue: getJsonString?.()
 | 
			
		||||
    })
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const parsed = parseJSON(editorValue)
 | 
			
		||||
    if (!parsed) return
 | 
			
		||||
@@ -44,29 +44,19 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
 | 
			
		||||
    }
 | 
			
		||||
  }, [editorValue])
 | 
			
		||||
 | 
			
		||||
  const saveState = (value: string, transactionType?: string) => {
 | 
			
		||||
    const tx = prepareState(value, transactionType)
 | 
			
		||||
    if (tx) {
 | 
			
		||||
      setState(tx)
 | 
			
		||||
      setState({
 | 
			
		||||
        editorValue: getJsonString?.(tx)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const discardChanges = () => {
 | 
			
		||||
    showAlert('Confirm', {
 | 
			
		||||
      body: 'Are you sure to discard these changes?',
 | 
			
		||||
      confirmText: 'Yes',
 | 
			
		||||
      onCancel: () => {},
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString?.() })
 | 
			
		||||
      onConfirm: () => setState({ editorValue: getJsonString() })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onExit = (value: string) => {
 | 
			
		||||
    const options = parseJSON(value)
 | 
			
		||||
    if (options) {
 | 
			
		||||
      saveState(value, currTxType)
 | 
			
		||||
      saveEditorState(value, currTxType)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    showAlert('Error!', {
 | 
			
		||||
@@ -163,8 +153,6 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
 | 
			
		||||
    })
 | 
			
		||||
  }, [getSchemas, monacoInst])
 | 
			
		||||
 | 
			
		||||
  const hasUnsaved = useMemo(() => editorValue !== getJsonString?.(), [editorValue, getJsonString])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Monaco
 | 
			
		||||
      rootProps={{
 | 
			
		||||
@@ -174,7 +162,7 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
 | 
			
		||||
      id={header}
 | 
			
		||||
      height="100%"
 | 
			
		||||
      value={editorValue}
 | 
			
		||||
      onChange={val => setState({ editorValue: val })}
 | 
			
		||||
      onChange={val => setState({ editorValue: val, editorIsSaved: false })}
 | 
			
		||||
      onMount={(editor, monaco) => {
 | 
			
		||||
        editor.updateOptions({
 | 
			
		||||
          minimap: { enabled: false },
 | 
			
		||||
@@ -190,12 +178,12 @@ export const TxJson: FC<JsonProps> = ({ getJsonString, state: txState, header, s
 | 
			
		||||
        model?.onWillDispose(() => onExit(model.getValue()))
 | 
			
		||||
      }}
 | 
			
		||||
      overlay={
 | 
			
		||||
        hasUnsaved ? (
 | 
			
		||||
        !editorIsSaved ? (
 | 
			
		||||
          <Flex row align="center" css={{ fontSize: '$xs', color: '$textMuted', ml: 'auto' }}>
 | 
			
		||||
            <Text muted small>
 | 
			
		||||
              This file has unsaved changes.
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Link css={{ ml: '$1' }} onClick={() => saveState(editorValue || '', currTxType)}>
 | 
			
		||||
            <Link css={{ ml: '$1' }} onClick={() => saveEditorState(editorValue, currTxType)}>
 | 
			
		||||
              save
 | 
			
		||||
            </Link>
 | 
			
		||||
            <Link css={{ ml: '$1' }} onClick={discardChanges}>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import Container from '../Container'
 | 
			
		||||
import Flex from '../Flex'
 | 
			
		||||
import Input from '../Input'
 | 
			
		||||
@@ -17,16 +17,35 @@ import state from '../../state'
 | 
			
		||||
import { streamState } from '../DebugStream'
 | 
			
		||||
import { Button } from '..'
 | 
			
		||||
import Textarea from '../Textarea'
 | 
			
		||||
import { getFlags } from '../../state/constants/flags'
 | 
			
		||||
import { Plus, Trash } from 'phosphor-react'
 | 
			
		||||
import AccountSequence from '../Sequence'
 | 
			
		||||
 | 
			
		||||
interface UIProps {
 | 
			
		||||
  setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
 | 
			
		||||
  resetState: (tt?: SelectOption) => TransactionState | undefined
 | 
			
		||||
  state: TransactionState
 | 
			
		||||
  estimateFee?: (...arg: any) => Promise<string | undefined>
 | 
			
		||||
  switchToJson: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) => {
 | 
			
		||||
export const TxUI: FC<UIProps> = ({
 | 
			
		||||
  state: txState,
 | 
			
		||||
  setState,
 | 
			
		||||
  resetState,
 | 
			
		||||
  estimateFee,
 | 
			
		||||
  switchToJson
 | 
			
		||||
}) => {
 | 
			
		||||
  const { accounts } = useSnapshot(state)
 | 
			
		||||
  const { selectedAccount, selectedDestAccount, selectedTransaction, txFields } = txState
 | 
			
		||||
  const {
 | 
			
		||||
    selectedAccount,
 | 
			
		||||
    selectedDestAccount,
 | 
			
		||||
    selectedTransaction,
 | 
			
		||||
    txFields,
 | 
			
		||||
    selectedFlags,
 | 
			
		||||
    hookParameters,
 | 
			
		||||
    memos
 | 
			
		||||
  } = txState
 | 
			
		||||
 | 
			
		||||
  const accountOptions: SelectOption[] = accounts.map(acc => ({
 | 
			
		||||
    label: acc.name,
 | 
			
		||||
@@ -40,23 +59,15 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
 | 
			
		||||
    }))
 | 
			
		||||
    .filter(acc => acc.value !== selectedAccount?.value)
 | 
			
		||||
 | 
			
		||||
  const flagsOptions: SelectOption[] = Object.entries(
 | 
			
		||||
    getFlags(selectedTransaction?.value) || {}
 | 
			
		||||
  ).map(([label, value]) => ({
 | 
			
		||||
    label,
 | 
			
		||||
    value
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  const [feeLoading, setFeeLoading] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const resetFields = useCallback(
 | 
			
		||||
    (tt: string) => {
 | 
			
		||||
      const fields = getTxFields(tt)
 | 
			
		||||
 | 
			
		||||
      if (fields.Destination !== undefined) {
 | 
			
		||||
        setState({ selectedDestAccount: null })
 | 
			
		||||
        fields.Destination = ''
 | 
			
		||||
      } else {
 | 
			
		||||
        fields.Destination = undefined
 | 
			
		||||
      }
 | 
			
		||||
      return setState({ txFields: fields })
 | 
			
		||||
    },
 | 
			
		||||
    [setState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const handleSetAccount = (acc: SelectOption) => {
 | 
			
		||||
    setState({ selectedAccount: acc })
 | 
			
		||||
    streamState.selectedAccount = acc
 | 
			
		||||
@@ -92,15 +103,13 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
 | 
			
		||||
    (tt: SelectOption) => {
 | 
			
		||||
      setState({ selectedTransaction: tt })
 | 
			
		||||
 | 
			
		||||
      const newState = resetFields(tt.value)
 | 
			
		||||
      const newState = resetState(tt)
 | 
			
		||||
 | 
			
		||||
      handleEstimateFee(newState, true)
 | 
			
		||||
    },
 | 
			
		||||
    [handleEstimateFee, resetFields, setState]
 | 
			
		||||
    [handleEstimateFee, resetState, setState]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const switchToJson = () => setState({ viewType: 'json' })
 | 
			
		||||
 | 
			
		||||
  // default tx
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedTransaction?.value) return
 | 
			
		||||
@@ -115,15 +124,16 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
 | 
			
		||||
    [selectedTransaction?.value]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const specialFields = ['TransactionType', 'Account']
 | 
			
		||||
  const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
 | 
			
		||||
  if (fields.Destination !== undefined) {
 | 
			
		||||
    specialFields.push('Destination')
 | 
			
		||||
    richFields.push('Destination')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(k => !specialFields.includes(k)) as [
 | 
			
		||||
    keyof TxFields
 | 
			
		||||
  ]
 | 
			
		||||
  if (flagsOptions.length) {
 | 
			
		||||
    richFields.push('Flags')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
 | 
			
		||||
  return (
 | 
			
		||||
    <Container
 | 
			
		||||
      css={{
 | 
			
		||||
@@ -133,76 +143,56 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            justifyContent: 'flex-end',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            mb: '$3',
 | 
			
		||||
            mt: '1px',
 | 
			
		||||
            pr: '1px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Text muted css={{ mr: '$3' }}>
 | 
			
		||||
            Transaction type:{' '}
 | 
			
		||||
          </Text>
 | 
			
		||||
        <TxField label="Transaction type">
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="transactionsType"
 | 
			
		||||
            placeholder="Select transaction type"
 | 
			
		||||
            options={transactionsOptions}
 | 
			
		||||
            hideSelectedOptions
 | 
			
		||||
            css={{ width: '70%' }}
 | 
			
		||||
            value={selectedTransaction}
 | 
			
		||||
            onChange={(tt: any) => handleChangeTxType(tt)}
 | 
			
		||||
          />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        <Flex
 | 
			
		||||
          row
 | 
			
		||||
          fluid
 | 
			
		||||
          css={{
 | 
			
		||||
            justifyContent: 'flex-end',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            mb: '$3',
 | 
			
		||||
            pr: '1px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Text muted css={{ mr: '$3' }}>
 | 
			
		||||
            Account:{' '}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </TxField>
 | 
			
		||||
        <TxField label="Account">
 | 
			
		||||
          <Select
 | 
			
		||||
            instanceId="from-account"
 | 
			
		||||
            placeholder="Select your account"
 | 
			
		||||
            css={{ width: '70%' }}
 | 
			
		||||
            options={accountOptions}
 | 
			
		||||
            value={selectedAccount}
 | 
			
		||||
            onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
 | 
			
		||||
          />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {fields.Destination !== undefined && (
 | 
			
		||||
          <Flex
 | 
			
		||||
            row
 | 
			
		||||
            fluid
 | 
			
		||||
            css={{
 | 
			
		||||
              justifyContent: 'flex-end',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              mb: '$3',
 | 
			
		||||
              pr: '1px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Text muted css={{ mr: '$3' }}>
 | 
			
		||||
              Destination account:{' '}
 | 
			
		||||
            </Text>
 | 
			
		||||
        </TxField>
 | 
			
		||||
        <TxField label="Sequence">
 | 
			
		||||
          <AccountSequence address={selectedAccount?.value} />
 | 
			
		||||
        </TxField>
 | 
			
		||||
        {richFields.includes('Destination') && (
 | 
			
		||||
          <TxField label="Destination account">
 | 
			
		||||
            <Select
 | 
			
		||||
              instanceId="to-account"
 | 
			
		||||
              placeholder="Select the destination account"
 | 
			
		||||
              css={{ width: '70%' }}
 | 
			
		||||
              options={destAccountOptions}
 | 
			
		||||
              value={selectedDestAccount}
 | 
			
		||||
              isClearable
 | 
			
		||||
              onChange={(acc: any) => setState({ selectedDestAccount: acc })}
 | 
			
		||||
            />
 | 
			
		||||
          </Flex>
 | 
			
		||||
          </TxField>
 | 
			
		||||
        )}
 | 
			
		||||
        {richFields.includes('Flags') && (
 | 
			
		||||
          <TxField label="Flags">
 | 
			
		||||
            <Select
 | 
			
		||||
              isClearable
 | 
			
		||||
              instanceId="flags"
 | 
			
		||||
              placeholder="Select flags to apply"
 | 
			
		||||
              menuPosition="fixed"
 | 
			
		||||
              value={selectedFlags}
 | 
			
		||||
              isMulti
 | 
			
		||||
              options={flagsOptions}
 | 
			
		||||
              onChange={flags => setState({ selectedFlags: flags as any })}
 | 
			
		||||
              closeMenuOnSelect={
 | 
			
		||||
                selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </TxField>
 | 
			
		||||
        )}
 | 
			
		||||
        {otherFields.map(field => {
 | 
			
		||||
          let _value = txFields[field]
 | 
			
		||||
@@ -224,93 +214,239 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
 | 
			
		||||
          let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
 | 
			
		||||
          if (rows && rows > 5) rows = 5
 | 
			
		||||
          return (
 | 
			
		||||
            <Flex column key={field} css={{ mb: '$2', pr: '1px' }}>
 | 
			
		||||
              <Flex
 | 
			
		||||
                row
 | 
			
		||||
                fluid
 | 
			
		||||
                css={{
 | 
			
		||||
                  justifyContent: 'flex-end',
 | 
			
		||||
                  alignItems: 'center',
 | 
			
		||||
                  position: 'relative'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text muted css={{ mr: '$3' }}>
 | 
			
		||||
                  {field + (isXrp ? ' (XRP)' : '')}:{' '}
 | 
			
		||||
                </Text>
 | 
			
		||||
                {isJson ? (
 | 
			
		||||
                  <Textarea
 | 
			
		||||
                    rows={rows}
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    spellCheck={false}
 | 
			
		||||
                    onChange={switchToJson}
 | 
			
		||||
                    css={{
 | 
			
		||||
                      width: '70%',
 | 
			
		||||
                      flex: 'inherit',
 | 
			
		||||
                      resize: 'vertical'
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Input
 | 
			
		||||
                    type={isFee ? 'number' : 'text'}
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      if (isFee) {
 | 
			
		||||
                        const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
 | 
			
		||||
                        handleSetField(field, val)
 | 
			
		||||
                      } else {
 | 
			
		||||
                        handleSetField(field, e.target.value)
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                    onKeyPress={
 | 
			
		||||
                      isFee
 | 
			
		||||
                        ? e => {
 | 
			
		||||
                            if (e.key === '.' || e.key === ',') {
 | 
			
		||||
                              e.preventDefault()
 | 
			
		||||
                            }
 | 
			
		||||
                          }
 | 
			
		||||
                        : undefined
 | 
			
		||||
            <TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}>
 | 
			
		||||
              {isJson ? (
 | 
			
		||||
                <Textarea
 | 
			
		||||
                  rows={rows}
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  spellCheck={false}
 | 
			
		||||
                  onChange={switchToJson}
 | 
			
		||||
                  css={{
 | 
			
		||||
                    flex: 'inherit',
 | 
			
		||||
                    resize: 'vertical'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Input
 | 
			
		||||
                  type={isFee ? 'number' : 'text'}
 | 
			
		||||
                  value={value}
 | 
			
		||||
                  onChange={e => {
 | 
			
		||||
                    if (isFee) {
 | 
			
		||||
                      const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
 | 
			
		||||
                      handleSetField(field, val)
 | 
			
		||||
                    } else {
 | 
			
		||||
                      handleSetField(field, e.target.value)
 | 
			
		||||
                    }
 | 
			
		||||
                    css={{
 | 
			
		||||
                      width: '70%',
 | 
			
		||||
                      flex: 'inherit',
 | 
			
		||||
                      '-moz-appearance': 'textfield',
 | 
			
		||||
                      '&::-webkit-outer-spin-button': {
 | 
			
		||||
                        '-webkit-appearance': 'none',
 | 
			
		||||
                        margin: 0
 | 
			
		||||
                      },
 | 
			
		||||
                      '&::-webkit-inner-spin-button ': {
 | 
			
		||||
                        '-webkit-appearance': 'none',
 | 
			
		||||
                        margin: 0
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                {isFee && (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    variant="primary"
 | 
			
		||||
                    outline
 | 
			
		||||
                    disabled={txState.txIsDisabled}
 | 
			
		||||
                    isDisabled={txState.txIsDisabled}
 | 
			
		||||
                    isLoading={feeLoading}
 | 
			
		||||
                    css={{
 | 
			
		||||
                      position: 'absolute',
 | 
			
		||||
                      right: '$2',
 | 
			
		||||
                      fontSize: '$xs',
 | 
			
		||||
                      cursor: 'pointer',
 | 
			
		||||
                      alignContent: 'center',
 | 
			
		||||
                      display: 'flex'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={() => handleEstimateFee()}
 | 
			
		||||
                  >
 | 
			
		||||
                    Suggest
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
              </Flex>
 | 
			
		||||
            </Flex>
 | 
			
		||||
                  }}
 | 
			
		||||
                  onKeyPress={
 | 
			
		||||
                    isFee
 | 
			
		||||
                      ? e => {
 | 
			
		||||
                          if (e.key === '.' || e.key === ',') {
 | 
			
		||||
                            e.preventDefault()
 | 
			
		||||
                          }
 | 
			
		||||
                        }
 | 
			
		||||
                      : undefined
 | 
			
		||||
                  }
 | 
			
		||||
                  css={{
 | 
			
		||||
                    flex: 'inherit',
 | 
			
		||||
                    '-moz-appearance': 'textfield',
 | 
			
		||||
                    '&::-webkit-outer-spin-button': {
 | 
			
		||||
                      '-webkit-appearance': 'none',
 | 
			
		||||
                      margin: 0
 | 
			
		||||
                    },
 | 
			
		||||
                    '&::-webkit-inner-spin-button ': {
 | 
			
		||||
                      '-webkit-appearance': 'none',
 | 
			
		||||
                      margin: 0
 | 
			
		||||
                    }
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {isFee && (
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="xs"
 | 
			
		||||
                  variant="primary"
 | 
			
		||||
                  outline
 | 
			
		||||
                  disabled={txState.txIsDisabled}
 | 
			
		||||
                  isDisabled={txState.txIsDisabled}
 | 
			
		||||
                  isLoading={feeLoading}
 | 
			
		||||
                  css={{
 | 
			
		||||
                    position: 'absolute',
 | 
			
		||||
                    right: '$2',
 | 
			
		||||
                    fontSize: '$xs',
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    alignContent: 'center',
 | 
			
		||||
                    display: 'flex'
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={() => handleEstimateFee()}
 | 
			
		||||
                >
 | 
			
		||||
                  Suggest
 | 
			
		||||
                </Button>
 | 
			
		||||
              )}
 | 
			
		||||
            </TxField>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
        <TxField multiLine label="Hook parameters">
 | 
			
		||||
          <Flex column fluid>
 | 
			
		||||
            {Object.entries(hookParameters).map(([id, { label, value }]) => (
 | 
			
		||||
              <Flex column key={id} css={{ mb: '$2' }}>
 | 
			
		||||
                <Flex row>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    placeholder="Parameter name"
 | 
			
		||||
                    value={label}
 | 
			
		||||
                    onChange={e => {
 | 
			
		||||
                      setState({
 | 
			
		||||
                        hookParameters: {
 | 
			
		||||
                          ...hookParameters,
 | 
			
		||||
                          [id]: { label: e.target.value, value }
 | 
			
		||||
                        }
 | 
			
		||||
                      })
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Input
 | 
			
		||||
                    css={{ mx: '$2' }}
 | 
			
		||||
                    placeholder="Value"
 | 
			
		||||
                    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"
 | 
			
		||||
                    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>
 | 
			
		||||
    </Container>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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: '$3',
 | 
			
		||||
        mt: '1px',
 | 
			
		||||
        pr: '1px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
 | 
			
		||||
        {label}:{' '}
 | 
			
		||||
      </Text>
 | 
			
		||||
      <Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
    "DestinationTag": 13,
 | 
			
		||||
    "Fee": "2000000",
 | 
			
		||||
    "Sequence": 2470665,
 | 
			
		||||
    "Flags": 2147483648
 | 
			
		||||
    "Flags": "2147483648"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "AccountSet",
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
    "Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
 | 
			
		||||
    "Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "Flags": 2147483648,
 | 
			
		||||
    "Flags": "2147483648",
 | 
			
		||||
    "Sequence": 2
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -109,7 +109,9 @@
 | 
			
		||||
    "Fee": "10",
 | 
			
		||||
    "NFTokenOffers": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
      "$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"]
 | 
			
		||||
      "$value": [
 | 
			
		||||
        "4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -120,7 +122,7 @@
 | 
			
		||||
      "$value": "100",
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Flags": 1,
 | 
			
		||||
    "Flags": "1",
 | 
			
		||||
    "Destination": "",
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
@@ -128,7 +130,7 @@
 | 
			
		||||
    "TransactionType": "OfferCancel",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 0,
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "LastLedgerSequence": 7108629,
 | 
			
		||||
    "OfferSequence": 6,
 | 
			
		||||
    "Sequence": 7
 | 
			
		||||
@@ -137,7 +139,7 @@
 | 
			
		||||
    "TransactionType": "OfferCreate",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 0,
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "LastLedgerSequence": 7108682,
 | 
			
		||||
    "Sequence": 8,
 | 
			
		||||
    "TakerGets": "6000000",
 | 
			
		||||
@@ -155,7 +157,7 @@
 | 
			
		||||
      "$type": "xrp"
 | 
			
		||||
    },
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 2147483648,
 | 
			
		||||
    "Flags": "2147483648",
 | 
			
		||||
    "Sequence": 2
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
@@ -185,14 +187,14 @@
 | 
			
		||||
    "Fee": "10"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Flags": 0,
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "TransactionType": "SetRegularKey",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "Flags": 0,
 | 
			
		||||
    "Flags": "0",
 | 
			
		||||
    "TransactionType": "SignerListSet",
 | 
			
		||||
    "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
@@ -232,7 +234,7 @@
 | 
			
		||||
    "TransactionType": "TrustSet",
 | 
			
		||||
    "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "Flags": 262144,
 | 
			
		||||
    "Flags": "262144",
 | 
			
		||||
    "LastLedgerSequence": 8007750,
 | 
			
		||||
    "LimitAmount": {
 | 
			
		||||
      "$type": "json",
 | 
			
		||||
@@ -243,5 +245,14 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Sequence": 12
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "Invoke",
 | 
			
		||||
    "Fee": "12"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "TransactionType": "UriToken",
 | 
			
		||||
    "Fee": "12",
 | 
			
		||||
    "URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
]
 | 
			
		||||
@@ -12,7 +12,7 @@ module.exports = {
 | 
			
		||||
      config.resolve.fallback.fs = false
 | 
			
		||||
    }
 | 
			
		||||
    config.module.rules.push({
 | 
			
		||||
      test: /\.md$/,
 | 
			
		||||
      test: [/\.md$/, /hook-bundle\.js$/],
 | 
			
		||||
      use: 'raw-loader'
 | 
			
		||||
    })
 | 
			
		||||
    return config
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -8,7 +8,8 @@
 | 
			
		||||
    "start": "next start",
 | 
			
		||||
    "lint": "next lint",
 | 
			
		||||
    "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": {
 | 
			
		||||
    "@codingame/monaco-jsonrpc": "^0.3.1",
 | 
			
		||||
@@ -64,9 +65,9 @@
 | 
			
		||||
    "valtio": "^1.2.5",
 | 
			
		||||
    "vscode-languageserver": "^7.0.0",
 | 
			
		||||
    "vscode-uri": "^3.0.2",
 | 
			
		||||
    "wabt": "1.0.16",
 | 
			
		||||
    "xrpl-accountlib": "^1.5.2",
 | 
			
		||||
    "xrpl-client": "^1.9.4"
 | 
			
		||||
    "wabt": "^1.0.30",
 | 
			
		||||
    "xrpl-accountlib": "^1.6.1",
 | 
			
		||||
    "xrpl-client": "^2.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/dinero.js": "^1.9.0",
 | 
			
		||||
@@ -75,6 +76,7 @@
 | 
			
		||||
    "@types/lodash.xor": "^4.5.6",
 | 
			
		||||
    "@types/pako": "^1.0.2",
 | 
			
		||||
    "@types/react": "17.0.31",
 | 
			
		||||
    "browserify": "^17.0.0",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "raw-loader": "^4.0.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,11 @@ import Box from '../../components/Box'
 | 
			
		||||
import Button from '../../components/Button'
 | 
			
		||||
import Popover from '../../components/Popover'
 | 
			
		||||
import RunScript from '../../components/RunScript'
 | 
			
		||||
import state from '../../state'
 | 
			
		||||
import state, { IFile } from '../../state'
 | 
			
		||||
import { compileCode } from '../../state/actions'
 | 
			
		||||
import { getSplit, saveSplit } from '../../state/actions/persistSplits'
 | 
			
		||||
import { styled } from '../../stitches.config'
 | 
			
		||||
import { getFileExtention } from '../../utils/helpers'
 | 
			
		||||
 | 
			
		||||
const HooksEditor = dynamic(() => import('../../components/HooksEditor'), {
 | 
			
		||||
  ssr: false
 | 
			
		||||
@@ -147,6 +148,9 @@ const CompilerSettings = () => {
 | 
			
		||||
const Home: NextPage = () => {
 | 
			
		||||
  const snap = useSnapshot(state)
 | 
			
		||||
 | 
			
		||||
  const activeFile = snap.files[snap.active] as IFile | undefined
 | 
			
		||||
  const activeFileExt = getFileExtention(activeFile?.name)
 | 
			
		||||
  const canCompile = activeFileExt === 'c' || activeFileExt === 'wat'
 | 
			
		||||
  return (
 | 
			
		||||
    <Split
 | 
			
		||||
      direction="vertical"
 | 
			
		||||
@@ -159,7 +163,7 @@ const Home: NextPage = () => {
 | 
			
		||||
    >
 | 
			
		||||
      <main style={{ display: 'flex', flex: 1, position: 'relative' }}>
 | 
			
		||||
        <HooksEditor />
 | 
			
		||||
        {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'c' && (
 | 
			
		||||
        {canCompile && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
            onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
 | 
			
		||||
@@ -193,7 +197,7 @@ const Home: NextPage = () => {
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
        {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'js' && (
 | 
			
		||||
        {activeFileExt === 'js' && (
 | 
			
		||||
          <Hotkeys
 | 
			
		||||
            keyName="command+b,ctrl+b"
 | 
			
		||||
            onKeyDown={() => !snap.compiling && snap.files.length && compileCode(snap.active)}
 | 
			
		||||
@@ -209,7 +213,7 @@ const Home: NextPage = () => {
 | 
			
		||||
                gap: '$2'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <RunScript file={snap.files[snap.active]} />
 | 
			
		||||
              <RunScript file={activeFile as IFile} />
 | 
			
		||||
            </Flex>
 | 
			
		||||
          </Hotkeys>
 | 
			
		||||
        )}
 | 
			
		||||
@@ -225,7 +229,7 @@ const Home: NextPage = () => {
 | 
			
		||||
        >
 | 
			
		||||
          <LogBox title="Development Log" clearLog={() => (state.logs = [])} logs={snap.logs} />
 | 
			
		||||
        </Flex>
 | 
			
		||||
        {snap.files[snap.active]?.name?.split('.')?.[1]?.toLowerCase() === 'js' && (
 | 
			
		||||
        {activeFileExt === 'js' && (
 | 
			
		||||
          <Flex
 | 
			
		||||
            css={{
 | 
			
		||||
              flex: 1
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										5
									
								
								raw-loader.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								raw-loader.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -2,3 +2,8 @@ declare module '*.md' {
 | 
			
		||||
  const content: string
 | 
			
		||||
  export default content
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module '*.hook-bundle.js' {
 | 
			
		||||
  const content: string
 | 
			
		||||
  export default content
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import state, { FaucetAccountRes } from '../index'
 | 
			
		||||
import fetchAccountInfo from '../../utils/accountInfo';
 | 
			
		||||
 | 
			
		||||
export const names = [
 | 
			
		||||
  'Alice',
 | 
			
		||||
@@ -35,40 +36,37 @@ export const addFaucetAccount = async (name?: string, showToast: boolean = false
 | 
			
		||||
  })
 | 
			
		||||
  const json: FaucetAccountRes | { error: string } = await res.json()
 | 
			
		||||
  if ('error' in json) {
 | 
			
		||||
    if (showToast) {
 | 
			
		||||
      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)
 | 
			
		||||
    state.accounts.push({
 | 
			
		||||
      name: name || names.filter(name => !currNames.includes(name))[0],
 | 
			
		||||
      xrp: (json.xrp || 0 * 1000000).toString(),
 | 
			
		||||
      address: json.address,
 | 
			
		||||
      secret: json.secret,
 | 
			
		||||
      sequence: 1,
 | 
			
		||||
      hooks: [],
 | 
			
		||||
      isLoading: false,
 | 
			
		||||
      version: '2'
 | 
			
		||||
    })
 | 
			
		||||
    if (!showToast) return;
 | 
			
		||||
    return toast.error(json.error, { id: toastId })
 | 
			
		||||
  }
 | 
			
		||||
  const currNames = state.accounts.map(acc => acc.name)
 | 
			
		||||
  const info = await fetchAccountInfo(json.address, { silent: true })
 | 
			
		||||
  state.accounts.push({
 | 
			
		||||
    name: name || names.filter(name => !currNames.includes(name))[0],
 | 
			
		||||
    xrp: (json.xrp || 0 * 1000000).toString(),
 | 
			
		||||
    address: json.address,
 | 
			
		||||
    secret: json.secret,
 | 
			
		||||
    sequence: info?.Sequence || 1,
 | 
			
		||||
    hooks: [],
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
    version: '2'
 | 
			
		||||
  })
 | 
			
		||||
  if (showToast) {
 | 
			
		||||
    toast.success('New account created', { id: toastId })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fetch initial faucets
 | 
			
		||||
;(async function fetchFaucets() {
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    if (state.accounts.length === 0) {
 | 
			
		||||
      await addFaucetAccount()
 | 
			
		||||
      // setTimeout(() => {
 | 
			
		||||
      //   addFaucetAccount();
 | 
			
		||||
      // }, 10000);
 | 
			
		||||
  // fetch initial faucets
 | 
			
		||||
  ; (async function fetchFaucets() {
 | 
			
		||||
    if (typeof window !== 'undefined') {
 | 
			
		||||
      if (state.accounts.length === 0) {
 | 
			
		||||
        await addFaucetAccount()
 | 
			
		||||
        // setTimeout(() => {
 | 
			
		||||
        //   addFaucetAccount();
 | 
			
		||||
        // }, 10000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})()
 | 
			
		||||
  })()
 | 
			
		||||
 | 
			
		||||
export const addFunds = async (address: string) => {
 | 
			
		||||
  const toastId = toast.loading('Requesting funds')
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,11 @@ import { ref } from 'valtio'
 | 
			
		||||
export const compileCode = async (activeId: number) => {
 | 
			
		||||
  // 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) {
 | 
			
		||||
    throw Error('Missing env!')
 | 
			
		||||
  }
 | 
			
		||||
@@ -26,7 +31,6 @@ export const compileCode = async (activeId: number) => {
 | 
			
		||||
  // Set loading state to true
 | 
			
		||||
  state.compiling = true
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  const file = state.files[activeId]
 | 
			
		||||
  try {
 | 
			
		||||
    file.containsErrors = false
 | 
			
		||||
    let res: Response
 | 
			
		||||
@@ -72,7 +76,7 @@ export const compileCode = async (activeId: number) => {
 | 
			
		||||
 | 
			
		||||
      // Import wabt from and create human readable version of wasm file and
 | 
			
		||||
      // put it into state
 | 
			
		||||
      const ww = (await import('wabt')).default()
 | 
			
		||||
      const ww = await (await import('wabt')).default()
 | 
			
		||||
      const myModule = ww.readWasm(new Uint8Array(bufferData), {
 | 
			
		||||
        readDebugNames: true
 | 
			
		||||
      })
 | 
			
		||||
@@ -122,3 +126,46 @@ export const compileCode = async (activeId: number) => {
 | 
			
		||||
    file.containsErrors = true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const compileWat = async (activeId: number) => {
 | 
			
		||||
  if (state.compiling) return;
 | 
			
		||||
  const file = state.files[activeId]
 | 
			
		||||
  state.compiling = true
 | 
			
		||||
  state.logs = []
 | 
			
		||||
  try {
 | 
			
		||||
    const wabt = await (await import('wabt')).default()
 | 
			
		||||
    const module = wabt.parseWat(file.name, file.content);
 | 
			
		||||
    module.resolveNames();
 | 
			
		||||
    module.validate();
 | 
			
		||||
    const { buffer } = module.toBinary({
 | 
			
		||||
      log: false,
 | 
			
		||||
      write_debug_names: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    file.compiledContent = ref(buffer)
 | 
			
		||||
    file.lastCompiled = new Date()
 | 
			
		||||
    file.compiledValueSnapshot = file.content
 | 
			
		||||
    file.compiledWatContent = file.content
 | 
			
		||||
 | 
			
		||||
    toast.success('Compiled successfully!', { position: 'bottom-center' })
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: 'success',
 | 
			
		||||
      message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
 | 
			
		||||
      link: Router.asPath.replace('develop', 'deploy'),
 | 
			
		||||
      linkText: 'Go to deploy'
 | 
			
		||||
    })
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
    let message = "Error compiling WAT file!"
 | 
			
		||||
    if (err instanceof Error) {
 | 
			
		||||
      message = err.message
 | 
			
		||||
    }
 | 
			
		||||
    state.logs.push({
 | 
			
		||||
      type: 'error',
 | 
			
		||||
      message
 | 
			
		||||
    })
 | 
			
		||||
    toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
 | 
			
		||||
    file.containsErrors = true
 | 
			
		||||
  }
 | 
			
		||||
  state.compiling = false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,19 @@
 | 
			
		||||
import { getFileExtention } from '../../utils/helpers'
 | 
			
		||||
import state, { IFile } from '../index'
 | 
			
		||||
 | 
			
		||||
const languageMapping = {
 | 
			
		||||
const languageMapping: Record<string, string | undefined> = {
 | 
			
		||||
  ts: 'typescript',
 | 
			
		||||
  js: 'javascript',
 | 
			
		||||
  md: 'markdown',
 | 
			
		||||
  c: 'c',
 | 
			
		||||
  h: 'c',
 | 
			
		||||
  other: ''
 | 
			
		||||
} /* Initializes empty file to global state */
 | 
			
		||||
  txt: 'text'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createNewFile = (name: string) => {
 | 
			
		||||
  const tempName = name.split('.')
 | 
			
		||||
  const fileExt = tempName[tempName.length - 1] || 'other'
 | 
			
		||||
  const emptyFile: IFile = {
 | 
			
		||||
    name,
 | 
			
		||||
    language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'],
 | 
			
		||||
    content: ''
 | 
			
		||||
  }
 | 
			
		||||
  const ext = getFileExtention(name) || ''
 | 
			
		||||
 | 
			
		||||
  const emptyFile: IFile = { name, language: languageMapping[ext] || 'text', content: '' }
 | 
			
		||||
  state.files.push(emptyFile)
 | 
			
		||||
  state.active = state.files.length - 1
 | 
			
		||||
}
 | 
			
		||||
@@ -24,5 +22,8 @@ export const renameFile = (oldName: string, nwName: string) => {
 | 
			
		||||
  const file = state.files.find(file => file.name === oldName)
 | 
			
		||||
  if (!file) throw Error(`No file exists with name ${oldName}`)
 | 
			
		||||
 | 
			
		||||
  const ext = getFileExtention(nwName) || ''
 | 
			
		||||
  const language = languageMapping[ext] || 'text'
 | 
			
		||||
  file.name = nwName
 | 
			
		||||
  file.language = language
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,9 @@ import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
 | 
			
		||||
import { Link } from '../../components'
 | 
			
		||||
import { ref } from 'valtio'
 | 
			
		||||
import estimateFee from '../../utils/estimateFee'
 | 
			
		||||
import { SetHookData } from '../../utils/setHook'
 | 
			
		||||
import { SetHookData, toHex } from '../../utils/setHook'
 | 
			
		||||
import ResultLink from '../../components/ResultLink'
 | 
			
		||||
import { xrplSend } from './xrpl-client'
 | 
			
		||||
 | 
			
		||||
export const sha256 = async (string: string) => {
 | 
			
		||||
  const utf8 = new TextEncoder().encode(string)
 | 
			
		||||
@@ -17,13 +18,6 @@ export const sha256 = async (string: string) => {
 | 
			
		||||
  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) {
 | 
			
		||||
  if (!arrayBuffer) {
 | 
			
		||||
@@ -64,9 +58,6 @@ export const prepareDeployHookTx = async (
 | 
			
		||||
  if (!activeFile?.compiledContent) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
 | 
			
		||||
  const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
 | 
			
		||||
  const { HookParameters } = data
 | 
			
		||||
@@ -87,196 +78,184 @@ export const prepareDeployHookTx = async (
 | 
			
		||||
  //     }
 | 
			
		||||
  //   }
 | 
			
		||||
  // });
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: 'SetHook',
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: data.Fee,
 | 
			
		||||
      Hooks: [
 | 
			
		||||
        {
 | 
			
		||||
          Hook: {
 | 
			
		||||
            CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
 | 
			
		||||
            HookOn: calculateHookOn(hookOnValues),
 | 
			
		||||
            HookNamespace,
 | 
			
		||||
            HookApiVersion: 0,
 | 
			
		||||
            Flags: 1,
 | 
			
		||||
            // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
 | 
			
		||||
            ...(filteredHookParameters.length > 0 && {
 | 
			
		||||
              HookParameters: filteredHookParameters
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
  if (typeof window === 'undefined') return
 | 
			
		||||
  const tx = {
 | 
			
		||||
    Account: account.address,
 | 
			
		||||
    TransactionType: 'SetHook',
 | 
			
		||||
    Sequence: account.sequence,
 | 
			
		||||
    Fee: data.Fee,
 | 
			
		||||
    NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
 | 
			
		||||
    Hooks: [
 | 
			
		||||
      {
 | 
			
		||||
        Hook: {
 | 
			
		||||
          CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
 | 
			
		||||
          HookOn: calculateHookOn(hookOnValues),
 | 
			
		||||
          HookNamespace,
 | 
			
		||||
          HookApiVersion: 0,
 | 
			
		||||
          Flags: 1,
 | 
			
		||||
          // ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
 | 
			
		||||
          ...(filteredHookParameters.length > 0 && {
 | 
			
		||||
            HookParameters: filteredHookParameters
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
    return tx
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
  return tx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* deployHook function turns the wasm binary into
 | 
			
		||||
 * hex string, signs the transaction and deploys it to
 | 
			
		||||
 * Hooks testnet.
 | 
			
		||||
/*
 | 
			
		||||
 * Turns the wasm binary into hex string, signs the transaction and deploys it to Hooks testnet.
 | 
			
		||||
 */
 | 
			
		||||
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    const activeFile = state.files[state.active]?.compiledContent
 | 
			
		||||
      ? state.files[state.active]
 | 
			
		||||
      : state.files.filter(file => file.compiledContent)[0]
 | 
			
		||||
    state.deployValues[activeFile.name] = data
 | 
			
		||||
    const tx = await prepareDeployHookTx(account, data)
 | 
			
		||||
    if (!tx) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!state.client) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const keypair = derive.familySeed(account.secret)
 | 
			
		||||
  const activeFile = state.files[state.active]?.compiledContent
 | 
			
		||||
    ? state.files[state.active]
 | 
			
		||||
    : state.files.filter(file => file.compiledContent)[0]
 | 
			
		||||
  state.deployValues[activeFile.name] = data
 | 
			
		||||
 | 
			
		||||
    const { signedTransaction } = sign(tx, keypair)
 | 
			
		||||
    const currentAccount = state.accounts.find(acc => acc.address === account.address)
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = true
 | 
			
		||||
    }
 | 
			
		||||
    let submitRes
 | 
			
		||||
  const tx = await prepareDeployHookTx(account, data)
 | 
			
		||||
  if (!tx) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const keypair = derive.familySeed(account.secret)
 | 
			
		||||
  const { signedTransaction } = sign(tx, keypair)
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      submitRes = await state.client?.send({
 | 
			
		||||
        command: 'submit',
 | 
			
		||||
        tx_blob: signedTransaction
 | 
			
		||||
  const currentAccount = state.accounts.find(acc => acc.address === account.address)
 | 
			
		||||
  if (currentAccount) {
 | 
			
		||||
    currentAccount.isLoading = true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let submitRes
 | 
			
		||||
  try {
 | 
			
		||||
    submitRes = await xrplSend({
 | 
			
		||||
      command: 'submit',
 | 
			
		||||
      tx_blob: signedTransaction
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const txHash = submitRes.tx_json?.hash
 | 
			
		||||
    const resultMsg = ref(
 | 
			
		||||
      <>
 | 
			
		||||
        [<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
 | 
			
		||||
        {txHash && (
 | 
			
		||||
          <>
 | 
			
		||||
            Transaction hash:{' '}
 | 
			
		||||
            <Link
 | 
			
		||||
              as="a"
 | 
			
		||||
              href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
 | 
			
		||||
              target="_blank"
 | 
			
		||||
              rel="noopener noreferrer"
 | 
			
		||||
            >
 | 
			
		||||
              {txHash}
 | 
			
		||||
            </Link>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
    if (submitRes.engine_result === 'tesSUCCESS') {
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        message: 'Hook deployed successfully ✅'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const txHash = submitRes.tx_json?.hash
 | 
			
		||||
      const resultMsg = ref(
 | 
			
		||||
        <>
 | 
			
		||||
          [<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
 | 
			
		||||
          {txHash && (
 | 
			
		||||
            <>
 | 
			
		||||
              Transaction hash:{' '}
 | 
			
		||||
              <Link
 | 
			
		||||
                as="a"
 | 
			
		||||
                href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noopener noreferrer"
 | 
			
		||||
              >
 | 
			
		||||
                {txHash}
 | 
			
		||||
              </Link>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )
 | 
			
		||||
      if (submitRes.engine_result === 'tesSUCCESS') {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          message: 'Hook deployed successfully ✅'
 | 
			
		||||
        })
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          message: resultMsg
 | 
			
		||||
        })
 | 
			
		||||
      } else if (submitRes.engine_result) {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          message: resultMsg
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          message: `[${submitRes.error}] ${submitRes.error_exception}`
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err)
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        message: resultMsg
 | 
			
		||||
      })
 | 
			
		||||
    } else if (submitRes.engine_result) {
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        message: 'Error occurred while deploying'
 | 
			
		||||
        message: resultMsg
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        message: `[${submitRes.error}] ${submitRes.error_exception}`
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false
 | 
			
		||||
    }
 | 
			
		||||
    return submitRes
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error(err)
 | 
			
		||||
    state.deployLogs.push({
 | 
			
		||||
      type: 'error',
 | 
			
		||||
      message: 'Error occurred while deploying'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  if (currentAccount) {
 | 
			
		||||
    currentAccount.isLoading = false
 | 
			
		||||
  }
 | 
			
		||||
  return submitRes
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const deleteHook = async (account: IAccount & { name?: string }) => {
 | 
			
		||||
  if (!state.client) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const currentAccount = state.accounts.find(acc => acc.address === account.address)
 | 
			
		||||
  if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    const tx = {
 | 
			
		||||
      Account: account.address,
 | 
			
		||||
      TransactionType: 'SetHook',
 | 
			
		||||
      Sequence: account.sequence,
 | 
			
		||||
      Fee: '100000',
 | 
			
		||||
      Hooks: [
 | 
			
		||||
        {
 | 
			
		||||
          Hook: {
 | 
			
		||||
            CreateCode: '',
 | 
			
		||||
            Flags: 1
 | 
			
		||||
          }
 | 
			
		||||
  const tx = {
 | 
			
		||||
    Account: account.address,
 | 
			
		||||
    TransactionType: 'SetHook',
 | 
			
		||||
    Sequence: account.sequence,
 | 
			
		||||
    Fee: '100000',
 | 
			
		||||
    NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
 | 
			
		||||
    Hooks: [
 | 
			
		||||
      {
 | 
			
		||||
        Hook: {
 | 
			
		||||
          CreateCode: '',
 | 
			
		||||
          Flags: 1
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const keypair = derive.familySeed(account.secret)
 | 
			
		||||
    try {
 | 
			
		||||
      // Update tx Fee value with network estimation
 | 
			
		||||
      const res = await estimateFee(tx, account)
 | 
			
		||||
      tx['Fee'] = res?.base_fee ? res?.base_fee : '1000'
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // use default value what you defined earlier
 | 
			
		||||
      console.log(err)
 | 
			
		||||
    }
 | 
			
		||||
    const { signedTransaction } = sign(tx, keypair)
 | 
			
		||||
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = true
 | 
			
		||||
    }
 | 
			
		||||
    let submitRes
 | 
			
		||||
    const toastId = toast.loading('Deleting hook...')
 | 
			
		||||
    try {
 | 
			
		||||
      submitRes = await state.client.send({
 | 
			
		||||
        command: 'submit',
 | 
			
		||||
        tx_blob: signedTransaction
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (submitRes.engine_result === 'tesSUCCESS') {
 | 
			
		||||
        toast.success('Hook deleted successfully ✅', { id: toastId })
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          message: 'Hook deleted successfully ✅'
 | 
			
		||||
        })
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
 | 
			
		||||
        })
 | 
			
		||||
        currentAccount.hooks = []
 | 
			
		||||
      } else {
 | 
			
		||||
        toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
 | 
			
		||||
          id: toastId
 | 
			
		||||
        })
 | 
			
		||||
        state.deployLogs.push({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          message: `[${submitRes.engine_result || submitRes.error}] ${
 | 
			
		||||
            submitRes.engine_result_message || submitRes.error_exception
 | 
			
		||||
          }`
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err)
 | 
			
		||||
      toast.error('Error occurred while deleting hook', { id: toastId })
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
  const keypair = derive.familySeed(account.secret)
 | 
			
		||||
  try {
 | 
			
		||||
    // Update tx Fee value with network estimation
 | 
			
		||||
    const res = await estimateFee(tx, account)
 | 
			
		||||
    tx['Fee'] = res?.base_fee || '1000'
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.error(err)
 | 
			
		||||
  }
 | 
			
		||||
  const { signedTransaction } = sign(tx, keypair)
 | 
			
		||||
  if (currentAccount) {
 | 
			
		||||
    currentAccount.isLoading = true
 | 
			
		||||
  }
 | 
			
		||||
  let submitRes
 | 
			
		||||
  const toastId = toast.loading('Deleting hook...')
 | 
			
		||||
  try {
 | 
			
		||||
    submitRes = await xrplSend({
 | 
			
		||||
      command: 'submit',
 | 
			
		||||
      tx_blob: signedTransaction
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (submitRes.engine_result === 'tesSUCCESS') {
 | 
			
		||||
      toast.success('Hook deleted successfully ✅', { id: toastId })
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        message: 'Hook deleted successfully ✅'
 | 
			
		||||
      })
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
 | 
			
		||||
      })
 | 
			
		||||
      currentAccount.hooks = []
 | 
			
		||||
    } else {
 | 
			
		||||
      toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
 | 
			
		||||
        id: toastId
 | 
			
		||||
      })
 | 
			
		||||
      state.deployLogs.push({
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        message: 'Error occurred while deleting hook'
 | 
			
		||||
        message: `[${submitRes.engine_result || submitRes.error}] ${
 | 
			
		||||
          submitRes.engine_result_message || submitRes.error_exception
 | 
			
		||||
        }`
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    if (currentAccount) {
 | 
			
		||||
      currentAccount.isLoading = false
 | 
			
		||||
    }
 | 
			
		||||
    return submitRes
 | 
			
		||||
  } catch (err) {
 | 
			
		||||
    console.log(err)
 | 
			
		||||
    toast.error('Error occurred while deleting hook', { id: toastId })
 | 
			
		||||
    state.deployLogs.push({
 | 
			
		||||
      type: 'error',
 | 
			
		||||
      message: 'Error occurred while deleting hook'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  if (currentAccount) {
 | 
			
		||||
    currentAccount.isLoading = false
 | 
			
		||||
  }
 | 
			
		||||
  return submitRes
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,7 @@ export const fetchFiles = async (gistId: string) => {
 | 
			
		||||
      // default priority is undefined == 0
 | 
			
		||||
      const extPriority: Record<string, number> = {
 | 
			
		||||
        c: 3,
 | 
			
		||||
        wat: 3,
 | 
			
		||||
        md: 2,
 | 
			
		||||
        h: -1
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -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`
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,7 @@ import state from '..'
 | 
			
		||||
import type { IAccount } from '..'
 | 
			
		||||
import ResultLink from '../../components/ResultLink'
 | 
			
		||||
import { ref } from 'valtio'
 | 
			
		||||
import { xrplSend } from './xrpl-client'
 | 
			
		||||
 | 
			
		||||
interface TransactionOptions {
 | 
			
		||||
  TransactionType: string
 | 
			
		||||
@@ -21,20 +22,23 @@ export const sendTransaction = async (
 | 
			
		||||
  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
 | 
			
		||||
    Fee,
 | 
			
		||||
    NetworkID: process.env.NEXT_PUBLIC_NETWORK_ID,
 | 
			
		||||
    ...opts
 | 
			
		||||
  }
 | 
			
		||||
  const { logPrefix = '' } = options || {}
 | 
			
		||||
  state.transactionLogs.push({
 | 
			
		||||
    type: 'log',
 | 
			
		||||
    message: `${logPrefix}${JSON.stringify(tx, null, 2)}`
 | 
			
		||||
  })
 | 
			
		||||
  try {
 | 
			
		||||
    const signedAccount = derive.familySeed(account.secret)
 | 
			
		||||
    const { signedTransaction } = sign(tx, signedAccount)
 | 
			
		||||
    const response = await state.client.send({
 | 
			
		||||
    const response = await xrplSend({
 | 
			
		||||
      command: 'submit',
 | 
			
		||||
      tx_blob: signedTransaction
 | 
			
		||||
    })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								state/actions/xrpl-client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								state/actions/xrpl-client.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								state/constants/flags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								state/constants/flags.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
import { SelectOption } from '../transactions';
 | 
			
		||||
 | 
			
		||||
interface Flags {
 | 
			
		||||
    [key: string]: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const transactionFlags: { [key: /* TransactionType */ string]: Flags } = {
 | 
			
		||||
    "*": {
 | 
			
		||||
        tfFullyCanonicalSig: '0x80000000'
 | 
			
		||||
    },
 | 
			
		||||
    Payment: {
 | 
			
		||||
        tfNoDirectRipple: '0x00010000',
 | 
			
		||||
        tfPartialPayment: '0x00020000',
 | 
			
		||||
        tfLimitQuality: '0x00040000',
 | 
			
		||||
    },
 | 
			
		||||
    AccountSet: {
 | 
			
		||||
        tfRequireDestTag: '0x00010000',
 | 
			
		||||
        tfOptionalDestTag: '0x00020000',
 | 
			
		||||
        tfRequireAuth: '0x00040000',
 | 
			
		||||
        tfOptionalAuth: '0x00080000',
 | 
			
		||||
        tfDisallowXRP: '0x00100000',
 | 
			
		||||
        tfAllowXRP: '0x00200000',
 | 
			
		||||
    },
 | 
			
		||||
    NFTokenCreateOffer: {
 | 
			
		||||
        tfSellNFToken: '0x00000001',
 | 
			
		||||
    },
 | 
			
		||||
    NFTokenMint: {
 | 
			
		||||
        tfBurnable: '0x00000001',
 | 
			
		||||
        tfOnlyXRP: '0x00000002',
 | 
			
		||||
        tfTrustLine: '0x00000004',
 | 
			
		||||
        tfTransferable: '0x00000008',
 | 
			
		||||
    },
 | 
			
		||||
    OfferCreate: {
 | 
			
		||||
        tfPassive: '0x00010000',
 | 
			
		||||
        tfImmediateOrCancel: '0x00020000',
 | 
			
		||||
        tfFillOrKill: '0x00040000',
 | 
			
		||||
        tfSell: '0x00080000',
 | 
			
		||||
    },
 | 
			
		||||
    PaymentChannelClaim: {
 | 
			
		||||
        tfRenew: '0x00010000',
 | 
			
		||||
        tfClose: '0x00020000',
 | 
			
		||||
    },
 | 
			
		||||
    TrustSet: {
 | 
			
		||||
        tfSetfAuth: '0x00010000',
 | 
			
		||||
        tfSetNoRipple: '0x00020000',
 | 
			
		||||
        tfClearNoRipple: '0x00040000',
 | 
			
		||||
        tfSetFreeze: '0x00100000',
 | 
			
		||||
        tfClearFreeze: '0x00200000',
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getFlags = (tt?: string) => {
 | 
			
		||||
    if (!tt) return
 | 
			
		||||
    const flags = {
 | 
			
		||||
        ...transactionFlags['*'],
 | 
			
		||||
        ...transactionFlags[tt]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return flags
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export function combineFlags(flags?: string[]): string | undefined {
 | 
			
		||||
    if (!flags) return
 | 
			
		||||
 | 
			
		||||
    const num = flags.reduce((cumm, curr) => cumm | BigInt(curr), BigInt(0))
 | 
			
		||||
    return num.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function extractFlags(transactionType: string, flags?: string | number,): SelectOption[] {
 | 
			
		||||
    const flagsObj = getFlags(transactionType)
 | 
			
		||||
    if (!flags || !flagsObj) return []
 | 
			
		||||
 | 
			
		||||
    const extracted = Object.entries(flagsObj).reduce((cumm, [label, value]) => {
 | 
			
		||||
        return (BigInt(flags) & BigInt(value)) ? cumm.concat({ label, value }) : cumm
 | 
			
		||||
    }, [] as SelectOption[])
 | 
			
		||||
 | 
			
		||||
    return extracted
 | 
			
		||||
}
 | 
			
		||||
@@ -78,7 +78,7 @@ export interface IState {
 | 
			
		||||
  splits: {
 | 
			
		||||
    [id: string]: SplitSize
 | 
			
		||||
  }
 | 
			
		||||
  client: XrplClient | null
 | 
			
		||||
  client: XrplClient
 | 
			
		||||
  clientStatus: 'offline' | 'online'
 | 
			
		||||
  mainModalOpen: boolean
 | 
			
		||||
  mainModalShowed: boolean
 | 
			
		||||
@@ -113,7 +113,7 @@ let initialState: IState = {
 | 
			
		||||
    tabSize: 2
 | 
			
		||||
  },
 | 
			
		||||
  splits: {},
 | 
			
		||||
  client: null,
 | 
			
		||||
  client: undefined!, // set below only.
 | 
			
		||||
  clientStatus: 'offline' as 'offline',
 | 
			
		||||
  mainModalOpen: false,
 | 
			
		||||
  mainModalShowed: false,
 | 
			
		||||
@@ -153,9 +153,9 @@ const state = proxy<IState>({
 | 
			
		||||
})
 | 
			
		||||
// Initialize socket connection
 | 
			
		||||
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`)
 | 
			
		||||
state.client = ref(client);
 | 
			
		||||
 | 
			
		||||
client.on('online', () => {
 | 
			
		||||
  state.client = ref(client)
 | 
			
		||||
  state.clientStatus = 'online'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,33 +4,57 @@ import transactionsData from '../content/transactions.json'
 | 
			
		||||
import state from '.'
 | 
			
		||||
import { showAlert } from '../state/actions/showAlert'
 | 
			
		||||
import { parseJSON } from '../utils/json'
 | 
			
		||||
import { extractFlags, getFlags } from './constants/flags'
 | 
			
		||||
import { fromHex } from '../utils/setHook'
 | 
			
		||||
 | 
			
		||||
export type SelectOption = {
 | 
			
		||||
  value: string
 | 
			
		||||
  label: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HookParameters = {
 | 
			
		||||
  [key: string]: SelectOption
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Memos = {
 | 
			
		||||
  [key: string]: {
 | 
			
		||||
    type: string
 | 
			
		||||
    format: string
 | 
			
		||||
    data: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionState {
 | 
			
		||||
  selectedTransaction: SelectOption | null
 | 
			
		||||
  selectedAccount: SelectOption | null
 | 
			
		||||
  selectedDestAccount: SelectOption | null
 | 
			
		||||
  selectedFlags: SelectOption[] | null
 | 
			
		||||
  hookParameters: HookParameters
 | 
			
		||||
  memos: Memos
 | 
			
		||||
  txIsLoading: boolean
 | 
			
		||||
  txIsDisabled: boolean
 | 
			
		||||
  txFields: TxFields
 | 
			
		||||
  viewType: 'json' | 'ui'
 | 
			
		||||
  editorValue?: string
 | 
			
		||||
  editorIsSaved: boolean
 | 
			
		||||
  estimatedFee?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const commonFields = ['TransactionType', 'Account', 'Sequence', "HookParameters"] as const;
 | 
			
		||||
 | 
			
		||||
export type TxFields = Omit<
 | 
			
		||||
  Partial<typeof transactionsData[0]>,
 | 
			
		||||
  'Account' | 'Sequence' | 'TransactionType'
 | 
			
		||||
  typeof commonFields[number]
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
export const defaultTransaction: TransactionState = {
 | 
			
		||||
  selectedTransaction: null,
 | 
			
		||||
  selectedAccount: null,
 | 
			
		||||
  selectedDestAccount: null,
 | 
			
		||||
  selectedFlags: null,
 | 
			
		||||
  hookParameters: {},
 | 
			
		||||
  memos: {},
 | 
			
		||||
  editorIsSaved: true,
 | 
			
		||||
  txIsLoading: false,
 | 
			
		||||
  txIsDisabled: false,
 | 
			
		||||
  txFields: {},
 | 
			
		||||
@@ -128,9 +152,8 @@ export const prepareTransaction = (data: any) => {
 | 
			
		||||
        try {
 | 
			
		||||
          options[field] = JSON.parse(_value.$value)
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          const message = `Input error for json field '${field}': ${
 | 
			
		||||
            error instanceof Error ? error.message : ''
 | 
			
		||||
          }`
 | 
			
		||||
          const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ''
 | 
			
		||||
            }`
 | 
			
		||||
          console.error(message)
 | 
			
		||||
          options[field] = _value.$value
 | 
			
		||||
        }
 | 
			
		||||
@@ -156,7 +179,7 @@ export const prepareState = (value: string, transactionType?: string) => {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { Account, TransactionType, Destination, ...rest } = options
 | 
			
		||||
  const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options
 | 
			
		||||
  let tx: Partial<TransactionState> = {}
 | 
			
		||||
  const schema = getTxFields(transactionType)
 | 
			
		||||
 | 
			
		||||
@@ -186,6 +209,22 @@ export const prepareState = (value: string, transactionType?: string) => {
 | 
			
		||||
    tx.selectedTransaction = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (HookParameters && HookParameters instanceof Array) {
 | 
			
		||||
    tx.hookParameters = HookParameters.reduce<TransactionState["hookParameters"]>((acc, cur, idx) => {
 | 
			
		||||
      const param = { label: fromHex(cur.HookParameter?.HookParameterName || ""), value: fromHex(cur.HookParameter?.HookParameterValue || "") }
 | 
			
		||||
      acc[idx] = param;
 | 
			
		||||
      return acc;
 | 
			
		||||
    }, {})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (Memos && Memos instanceof Array) {
 | 
			
		||||
    tx.memos = Memos.reduce<TransactionState["memos"]>((acc, cur, idx) => {
 | 
			
		||||
      const memo = { data: fromHex(cur.Memo?.MemoData || ""), type: fromHex(cur.Memo?.MemoType || ""), format: fromHex(cur.Memo?.MemoFormat || "") }
 | 
			
		||||
      acc[idx] = memo;
 | 
			
		||||
      return acc;
 | 
			
		||||
    }, {})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (schema.Destination !== undefined) {
 | 
			
		||||
    const dest = state.accounts.find(acc => acc.address === Destination)
 | 
			
		||||
    if (dest) {
 | 
			
		||||
@@ -205,6 +244,13 @@ export const prepareState = (value: string, transactionType?: string) => {
 | 
			
		||||
    rest.Destination = Destination
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (getFlags(TransactionType) && rest.Flags) {
 | 
			
		||||
    const flags = extractFlags(TransactionType, rest.Flags)
 | 
			
		||||
 | 
			
		||||
    rest.Flags = undefined
 | 
			
		||||
    tx.selectedFlags = flags
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Object.keys(rest).forEach(field => {
 | 
			
		||||
    const value = rest[field]
 | 
			
		||||
    const schemaVal = schema[field as keyof TxFields]
 | 
			
		||||
@@ -227,6 +273,7 @@ export const prepareState = (value: string, transactionType?: string) => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  tx.txFields = rest
 | 
			
		||||
  tx.editorIsSaved = true;
 | 
			
		||||
 | 
			
		||||
  return tx
 | 
			
		||||
}
 | 
			
		||||
@@ -237,12 +284,12 @@ export const getTxFields = (tt?: string) => {
 | 
			
		||||
  if (!txFields) return {}
 | 
			
		||||
 | 
			
		||||
  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), {})
 | 
			
		||||
  return _txFields
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { transactionsData }
 | 
			
		||||
export { transactionsData, commonFields }
 | 
			
		||||
 | 
			
		||||
export const transactionsOptions = transactionsData.map(tx => ({
 | 
			
		||||
  value: tx.TransactionType,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								utils/accountInfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								utils/accountInfo.ts
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
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 (
 | 
			
		||||
  tx: Record<string, unknown>,
 | 
			
		||||
@@ -22,7 +23,7 @@ const estimateFee = async (
 | 
			
		||||
    const keypair = derive.familySeed(account.secret)
 | 
			
		||||
    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 && res.drops) {
 | 
			
		||||
      return res.drops
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,3 +13,9 @@ export const capitalize = (value?: string) => {
 | 
			
		||||
 | 
			
		||||
  return value[0].toLocaleUpperCase() + value.slice(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getFileExtention = (filename?: string): string | undefined => {
 | 
			
		||||
  if (!filename) return
 | 
			
		||||
  const ext = (filename.includes('.') && filename.split('.').pop()) || undefined
 | 
			
		||||
  return ext
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -23,23 +23,22 @@ export const tts = {
 | 
			
		||||
  ttNFTOKEN_BURN: 26,
 | 
			
		||||
  ttNFTOKEN_CREATE_OFFER: 27,
 | 
			
		||||
  ttNFTOKEN_CANCEL_OFFER: 28,
 | 
			
		||||
  ttNFTOKEN_ACCEPT_OFFER: 29
 | 
			
		||||
  ttNFTOKEN_ACCEPT_OFFER: 29,
 | 
			
		||||
  ttINVOKE: 99,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TTS = typeof tts
 | 
			
		||||
 | 
			
		||||
const calculateHookOn = (arr: (keyof TTS)[]) => {
 | 
			
		||||
  let start = '0x000000003e3ff5bf'
 | 
			
		||||
  let s = '0x3e3ff5bf'
 | 
			
		||||
  arr.forEach(n => {
 | 
			
		||||
    let v = BigInt(start)
 | 
			
		||||
    v ^= BigInt(1) << BigInt(tts[n as keyof TTS])
 | 
			
		||||
    let s = v.toString(16)
 | 
			
		||||
    let l = s.length
 | 
			
		||||
    if (l < 16) s = '0'.repeat(16 - l) + s
 | 
			
		||||
    s = '0x' + s
 | 
			
		||||
    start = s
 | 
			
		||||
    let v = BigInt(s)
 | 
			
		||||
    v ^= BigInt(1) << BigInt(tts[n])
 | 
			
		||||
    s = "0x" + v.toString(16)
 | 
			
		||||
  })
 | 
			
		||||
  return start.substring(2)
 | 
			
		||||
  s = s.replace('0x', '')
 | 
			
		||||
  s = s.padStart(64, '0')
 | 
			
		||||
  return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default calculateHookOn
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -20,6 +20,13 @@ export type SetHookData = {
 | 
			
		||||
    }
 | 
			
		||||
    $metaData?: any
 | 
			
		||||
  }[]
 | 
			
		||||
  Memos?: {
 | 
			
		||||
    Memo: {
 | 
			
		||||
      MemoType: string,
 | 
			
		||||
      MemoData: string
 | 
			
		||||
      MemoFormat: string
 | 
			
		||||
    }
 | 
			
		||||
  }[]
 | 
			
		||||
  // HookGrants: {
 | 
			
		||||
  //   HookGrant: {
 | 
			
		||||
  //     Authorize: string;
 | 
			
		||||
@@ -74,3 +81,19 @@ export const getInvokeOptions = (content?: string) => {
 | 
			
		||||
 | 
			
		||||
  return invokeOptions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toHex(str: string) {
 | 
			
		||||
  var result = ''
 | 
			
		||||
  for (var i = 0; i < str.length; i++) {
 | 
			
		||||
    result += str.charCodeAt(i).toString(16)
 | 
			
		||||
  }
 | 
			
		||||
  return result.toUpperCase()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fromHex(hex: string) {
 | 
			
		||||
  var str = ''
 | 
			
		||||
  for (var i = 0; i < hex.length; i += 2) {
 | 
			
		||||
    str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16))
 | 
			
		||||
  }
 | 
			
		||||
  return str
 | 
			
		||||
}
 | 
			
		||||
@@ -22,6 +22,7 @@ import hooksFloatManipPure from './md/hooks-float-manip-pure.md'
 | 
			
		||||
import hooksFloatOnePure from './md/hooks-float-one-pure.md'
 | 
			
		||||
import hooksFloatPure from './md/hooks-float-pure.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 hooksGuardInWhile from './md/hooks-guard-in-while.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-pure': hooksFloatPure,
 | 
			
		||||
  'hooks-guard-called': hooksGuardCalled,
 | 
			
		||||
  'hooks-guard-call-non-const': hooksGuardCallNonConst,
 | 
			
		||||
  'hooks-guard-in-for': hooksGuardInFor,
 | 
			
		||||
  'hooks-guard-in-while': hooksGuardInWhile,
 | 
			
		||||
  'hooks-hash-buf-len': hooksHashBufLen,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								xrpl-hooks-docs/md/hooks-guard-call-non-const.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								xrpl-hooks-docs/md/hooks-guard-call-non-const.md
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@@ -1,14 +1,35 @@
 | 
			
		||||
# 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
 | 
			
		||||
  #define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; GUARD(3), i < 3; ++i)
 | 
			
		||||
#define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
 | 
			
		||||
for (int i = 0; GUARD(3), i < 3; ++i)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<BR/>
 | 
			
		||||
This is the only way to satisfy the guard rule when using a for-loop in C.
 | 
			
		||||
To satisfy the guard rule when using a for-loop in C guard should be 
 | 
			
		||||
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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user