Compare commits

...

36 Commits

Author SHA1 Message Date
muzam1l
9001c64fed minor label changes. 2023-03-07 16:58:32 +05:30
muzam1l
03b768db4e UI for memos fields. 2023-03-07 16:03:47 +05:30
muzam1l
825af0db89 Add memos field in transactions. 2023-03-06 21:14:14 +05:30
muzamil
31043f33ab Merge pull request #286 from XRPLF/feat/tx-params-ui
HookParameters UI for transactions.
2023-03-06 16:06:19 +05:30
muzam1l
39699a1cb9 HookParameters UI for transactions. 2023-03-03 18:32:03 +05:30
muzam1l
b50b300307 Refactor tx. 2023-03-03 16:00:43 +05:30
muzamil
82c06cbb12 Merge pull request #285 from XRPLF/fix/params
Add invoke hookon option.
2023-03-02 19:17:38 +05:30
muzam1l
423ee18e6a Add Invoke HookOn option. 2023-03-02 15:11:44 +05:30
muzamil
3bb26d0c9b Merge pull request #281 from XRPLF/feat/testnet-v3
Testnet v3.
2023-02-10 16:12:22 +05:30
muzam1l
43c83d0de6 Fix delete hook and some refactor. 2023-02-10 14:43:51 +05:30
muzam1l
6bb407cb0f Better package loading experience. 2023-02-09 22:15:22 +05:30
muzamil
d7b29ba809 Merge pull request #282 from XRPLF/feat/bundle-xrpl
Bundle patched `xrpl-accountlib` and expose through require syntax.
2023-02-09 20:22:43 +05:30
muzam1l
ca81d8ad41 Bundle xrpl-accountlib and expose throw require syntax. 2023-02-09 15:41:06 +05:30
muzam1l
a7e59d7b73 Add UriToken transaction. 2023-02-07 14:32:06 +05:30
muzam1l
8f6b28cef5 Add Invoke transaction type. 2023-02-07 14:15:07 +05:30
muzam1l
c1815f272b refactor. 2023-02-07 14:00:57 +05:30
muzam1l
76871a8041 Testnet v3. 2023-02-06 21:09:11 +05:30
mariopil
d2033c8035 Hooks docs update (#279)
* Added hooks-guard-call-non-const checker doc, updated hooks-guard-in-for doc.
2023-01-09 13:43:32 +01:00
muzamil
48daf1c5c8 Merge pull request #278 from XRPLF/feat/compile-wat
Increase wat file priority in sorting.
2022-12-07 15:03:40 +05:30
muzam1l
a3365e4beb Increase wat file priority in sorting. 2022-12-07 15:02:09 +05:30
muzamil
45d813cdad Merge pull request #277 from XRPLF/feat/compile-wat
Compile raw wat files.
2022-12-07 14:46:05 +05:30
muzam1l
911416aa2f Compile wat files. 2022-12-07 13:31:59 +05:30
muzamil
2d836af9ed Merge pull request #276 from XRPLF/feat/flags-ui
Transaction flags UI.
2022-11-01 17:14:44 +05:30
muzam1l
4f0fc838be Minor css fix. 2022-11-01 11:20:09 +05:30
muzam1l
fa93912c38 Remove reductant comment. 2022-10-28 14:58:11 +05:30
muzam1l
f5cb76c302 Merge branch 'main' into feat/flags-ui 2022-10-28 14:48:41 +05:30
muzamil
df1d65dcab Merge pull request #275 from XRPLF/dependabot/npm_and_yarn/jose-4.10.0
Bump jose from 4.6.0 to 4.10.0
2022-10-28 14:48:02 +05:30
muzam1l
1513f78991 Add tx specific flags. 2022-10-28 14:43:37 +05:30
muzam1l
3a064f307b Add global flags. 2022-10-28 14:21:22 +05:30
muzam1l
6fca05f310 Add flags UI to Payment transaction. 2022-10-28 12:29:57 +05:30
muzamil
31e67d382f Merge pull request #273 from XRPLF/fix/renaming-ext
Update file language on renaming.
2022-10-26 18:10:43 +05:30
dependabot[bot]
27475301e4 Bump jose from 4.6.0 to 4.10.0
Bumps [jose](https://github.com/panva/jose) from 4.6.0 to 4.10.0.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v4.6.0...v4.10.0)

---
updated-dependencies:
- dependency-name: jose
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-14 08:29:14 +00:00
muzam1l
934283976a Add plain text language to monaco. 2022-09-09 14:07:13 +05:30
muzamil
2d9ca2674e Merge pull request #266 from XRPLF/feat/engine-code-links
Linkify engine error codes and some refactor.
2022-08-19 20:00:22 +05:30
muzam1l
0d9e9e7b45 Merge branch 'main' into fix/renaming-ext 2022-08-19 15:14:52 +05:30
muzam1l
2c2bf59bcd Update file language on renaming. 2022-08-17 12:46:17 +05:30
35 changed files with 4604 additions and 2135 deletions

View File

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

4
.gitignore vendored
View File

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

View File

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

View File

@@ -15,7 +15,7 @@ const contentShow = keyframes({
'100%': { opacity: 1 } '100%': { opacity: 1 }
}) })
const StyledOverlay = styled(DialogPrimitive.Overlay, { const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 10000, zIndex: 3000,
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
position: 'fixed', position: 'fixed',
inset: 0, inset: 0,

View File

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

View File

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

View File

@@ -38,7 +38,8 @@ const Select = forwardRef<any, Props>((props, ref) => {
container: provided => { container: provided => {
return { return {
...provided, ...provided,
position: 'relative' position: 'relative',
width: '100%'
} }
}, },
singleValue: provided => ({ singleValue: provided => ({

View File

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

View File

@@ -19,6 +19,8 @@ import { TxJson } from './json'
import { TxUI } from './ui' import { TxUI } from './ui'
import { default as _estimateFee } from '../../utils/estimateFee' import { default as _estimateFee } from '../../utils/estimateFee'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { combineFlags, extractFlags, transactionFlags } from '../../state/constants/flags'
import { SetHookData, toHex } from '../../utils/setHook'
export interface TransactionProps { export interface TransactionProps {
header: string header: string
@@ -39,17 +41,43 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
const prepareOptions = useCallback( const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => { (state: Partial<TransactionState> = txState) => {
const { selectedTransaction, selectedDestAccount, selectedAccount, txFields } = state const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
selectedFlags,
hookParameters,
memos
} = state
const TransactionType = selectedTransaction?.value || null const TransactionType = selectedTransaction?.value || null
const Destination = selectedDestAccount?.value || txFields?.Destination const Destination = selectedDestAccount?.value || txFields?.Destination
const Account = selectedAccount?.value || null const Account = selectedAccount?.value || null
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
const HookParameters = Object.entries(hookParameters || {}).reduce<
SetHookData['HookParameters']
>((acc, [_, { label, value }]) => {
return acc.concat({
HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({ return prepareTransaction({
...txFields, ...txFields,
HookParameters,
Flags,
TransactionType, TransactionType,
Destination, Destination,
Account Account,
Memos
}) })
}, },
[txState] [txState]
@@ -136,8 +164,13 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} else { } else {
fields.Destination = undefined 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 state = modifyTxState(header, nwState, { replaceState: true })
const editorValue = getJsonString(state) const editorValue = getJsonString(state)
return setState({ editorValue }) return setState({ editorValue })
@@ -179,7 +212,12 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
estimateFee={estimateFee} estimateFee={estimateFee}
/> />
) : ( ) : (
<TxUI state={txState} setState={setState} estimateFee={estimateFee} /> <TxUI
state={txState}
resetState={resetState}
setState={setState}
estimateFee={estimateFee}
/>
)} )}
<Flex <Flex
row row

View File

@@ -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 Container from '../Container'
import Flex from '../Flex' import Flex from '../Flex'
import Input from '../Input' import Input from '../Input'
@@ -17,16 +17,27 @@ import state from '../../state'
import { streamState } from '../DebugStream' import { streamState } from '../DebugStream'
import { Button } from '..' import { Button } from '..'
import Textarea from '../Textarea' import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined> estimateFee?: (...arg: any) => Promise<string | undefined>
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) => { export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estimateFee }) => {
const { accounts } = useSnapshot(state) 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 => ({ const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name, label: acc.name,
@@ -40,23 +51,15 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
})) }))
.filter(acc => acc.value !== selectedAccount?.value) .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 [feeLoading, setFeeLoading] = useState(false)
const resetFields = useCallback(
(tt: string) => {
const fields = getTxFields(tt)
if (fields.Destination !== undefined) {
setState({ selectedDestAccount: null })
fields.Destination = ''
} else {
fields.Destination = undefined
}
return setState({ txFields: fields })
},
[setState]
)
const handleSetAccount = (acc: SelectOption) => { const handleSetAccount = (acc: SelectOption) => {
setState({ selectedAccount: acc }) setState({ selectedAccount: acc })
streamState.selectedAccount = acc streamState.selectedAccount = acc
@@ -92,11 +95,11 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
(tt: SelectOption) => { (tt: SelectOption) => {
setState({ selectedTransaction: tt }) setState({ selectedTransaction: tt })
const newState = resetFields(tt.value) const newState = resetState(tt)
handleEstimateFee(newState, true) handleEstimateFee(newState, true)
}, },
[handleEstimateFee, resetFields, setState] [handleEstimateFee, resetState, setState]
) )
const switchToJson = () => setState({ viewType: 'json' }) const switchToJson = () => setState({ viewType: 'json' })
@@ -115,15 +118,16 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
[selectedTransaction?.value] [selectedTransaction?.value]
) )
const specialFields = ['TransactionType', 'Account'] const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
if (fields.Destination !== undefined) { if (fields.Destination !== undefined) {
specialFields.push('Destination') richFields.push('Destination')
} }
const otherFields = Object.keys(txFields).filter(k => !specialFields.includes(k)) as [ if (flagsOptions.length) {
keyof TxFields richFields.push('Flags')
] }
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
return ( return (
<Container <Container
css={{ css={{
@@ -133,76 +137,53 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
}} }}
> >
<Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}> <Flex column fluid css={{ height: '100%', overflowY: 'auto', pr: '$1' }}>
<Flex <TxField label="Transaction type">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
mt: '1px',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Transaction type:{' '}
</Text>
<Select <Select
instanceId="transactionsType" instanceId="transactionsType"
placeholder="Select transaction type" placeholder="Select transaction type"
options={transactionsOptions} options={transactionsOptions}
hideSelectedOptions hideSelectedOptions
css={{ width: '70%' }}
value={selectedTransaction} value={selectedTransaction}
onChange={(tt: any) => handleChangeTxType(tt)} onChange={(tt: any) => handleChangeTxType(tt)}
/> />
</Flex> </TxField>
<Flex <TxField label="Account">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Account:{' '}
</Text>
<Select <Select
instanceId="from-account" instanceId="from-account"
placeholder="Select your account" placeholder="Select your account"
css={{ width: '70%' }}
options={accountOptions} options={accountOptions}
value={selectedAccount} value={selectedAccount}
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
/> />
</Flex> </TxField>
{fields.Destination !== undefined && ( {richFields.includes('Destination') && (
<Flex <TxField label="Destination account">
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Destination account:{' '}
</Text>
<Select <Select
instanceId="to-account" instanceId="to-account"
placeholder="Select the destination account" placeholder="Select the destination account"
css={{ width: '70%' }}
options={destAccountOptions} options={destAccountOptions}
value={selectedDestAccount} value={selectedDestAccount}
isClearable isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })} 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 => { {otherFields.map(field => {
let _value = txFields[field] let _value = txFields[field]
@@ -224,93 +205,239 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, estimateFee }) =>
let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined let rows = isJson ? (value?.match(/\n/gm)?.length || 0) + 1 : undefined
if (rows && rows > 5) rows = 5 if (rows && rows > 5) rows = 5
return ( return (
<Flex column key={field} css={{ mb: '$2', pr: '1px' }}> <TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}>
<Flex {isJson ? (
row <Textarea
fluid rows={rows}
css={{ value={value}
justifyContent: 'flex-end', spellCheck={false}
alignItems: 'center', onChange={switchToJson}
position: 'relative' css={{
}} flex: 'inherit',
> resize: 'vertical'
<Text muted css={{ mr: '$3' }}> }}
{field + (isXrp ? ' (XRP)' : '')}:{' '} />
</Text> ) : (
{isJson ? ( <Input
<Textarea type={isFee ? 'number' : 'text'}
rows={rows} value={value}
value={value} onChange={e => {
spellCheck={false} if (isFee) {
onChange={switchToJson} const val = e.target.value.replaceAll('.', '').replaceAll(',', '')
css={{ handleSetField(field, val)
width: '70%', } else {
flex: 'inherit', handleSetField(field, e.target.value)
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
} }
css={{ }}
width: '70%', onKeyPress={
flex: 'inherit', isFee
'-moz-appearance': 'textfield', ? e => {
'&::-webkit-outer-spin-button': { if (e.key === '.' || e.key === ',') {
'-webkit-appearance': 'none', e.preventDefault()
margin: 0 }
}, }
'&::-webkit-inner-spin-button ': { : undefined
'-webkit-appearance': 'none', }
margin: 0 css={{
} flex: 'inherit',
}} '-moz-appearance': 'textfield',
/> '&::-webkit-outer-spin-button': {
)} '-webkit-appearance': 'none',
{isFee && ( margin: 0
<Button },
size="xs" '&::-webkit-inner-spin-button ': {
variant="primary" '-webkit-appearance': 'none',
outline margin: 0
disabled={txState.txIsDisabled} }
isDisabled={txState.txIsDisabled} }}
isLoading={feeLoading} />
css={{ )}
position: 'absolute', {isFee && (
right: '$2', <Button
fontSize: '$xs', size="xs"
cursor: 'pointer', variant="primary"
alignContent: 'center', outline
display: 'flex' disabled={txState.txIsDisabled}
}} isDisabled={txState.txIsDisabled}
onClick={() => handleEstimateFee()} isLoading={feeLoading}
> css={{
Suggest position: 'absolute',
</Button> right: '$2',
)} fontSize: '$xs',
</Flex> cursor: 'pointer',
</Flex> 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> </Flex>
</Container> </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>
)
}

View File

@@ -6,7 +6,7 @@
"DestinationTag": 13, "DestinationTag": 13,
"Fee": "2000000", "Fee": "2000000",
"Sequence": 2470665, "Sequence": 2470665,
"Flags": 2147483648 "Flags": "2147483648"
}, },
{ {
"TransactionType": "AccountSet", "TransactionType": "AccountSet",
@@ -48,7 +48,7 @@
"Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8", "Account": "rsUiUMpnrgxQp24dJYZDhmV4bE3aBtQyt8",
"Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de", "Authorize": "rEhxGqkqPPSxQ3P25J66ft5TwpzV14k2de",
"Fee": "10", "Fee": "10",
"Flags": 2147483648, "Flags": "2147483648",
"Sequence": 2 "Sequence": 2
}, },
{ {
@@ -109,7 +109,9 @@
"Fee": "10", "Fee": "10",
"NFTokenOffers": { "NFTokenOffers": {
"$type": "json", "$type": "json",
"$value": ["4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"] "$value": [
"4AAAEEA76E3C8148473CB3840CE637676E561FB02BD4CA22CA59729EA815B862"
]
} }
}, },
{ {
@@ -120,7 +122,7 @@
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "xrp"
}, },
"Flags": 1, "Flags": "1",
"Destination": "", "Destination": "",
"Fee": "10" "Fee": "10"
}, },
@@ -128,7 +130,7 @@
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 0, "Flags": "0",
"LastLedgerSequence": 7108629, "LastLedgerSequence": 7108629,
"OfferSequence": 6, "OfferSequence": 6,
"Sequence": 7 "Sequence": 7
@@ -137,7 +139,7 @@
"TransactionType": "OfferCreate", "TransactionType": "OfferCreate",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 0, "Flags": "0",
"LastLedgerSequence": 7108682, "LastLedgerSequence": 7108682,
"Sequence": 8, "Sequence": 8,
"TakerGets": "6000000", "TakerGets": "6000000",
@@ -155,7 +157,7 @@
"$type": "xrp" "$type": "xrp"
}, },
"Fee": "12", "Fee": "12",
"Flags": 2147483648, "Flags": "2147483648",
"Sequence": 2 "Sequence": 2
}, },
{ {
@@ -185,14 +187,14 @@
"Fee": "10" "Fee": "10"
}, },
{ {
"Flags": 0, "Flags": "0",
"TransactionType": "SetRegularKey", "TransactionType": "SetRegularKey",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
"RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD" "RegularKey": "rAR8rR8sUkBoCZFawhkWzY4Y5YoyuznwD"
}, },
{ {
"Flags": 0, "Flags": "0",
"TransactionType": "SignerListSet", "TransactionType": "SignerListSet",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Fee": "12", "Fee": "12",
@@ -232,7 +234,7 @@
"TransactionType": "TrustSet", "TransactionType": "TrustSet",
"Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Account": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
"Fee": "12", "Fee": "12",
"Flags": 262144, "Flags": "262144",
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"LimitAmount": { "LimitAmount": {
"$type": "json", "$type": "json",
@@ -243,5 +245,14 @@
} }
}, },
"Sequence": 12 "Sequence": 12
},
{
"TransactionType": "Invoke",
"Fee": "12"
},
{
"TransactionType": "UriToken",
"Fee": "12",
"URI": "697066733A2F2F516D614374444B5A4656767666756676626479346573745A626851483744586831364354707631686F776D424779"
} }
] ]

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

5
raw-loader.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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
}

View File

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

View File

@@ -4,16 +4,33 @@ import transactionsData from '../content/transactions.json'
import state from '.' import state from '.'
import { showAlert } from '../state/actions/showAlert' import { showAlert } from '../state/actions/showAlert'
import { parseJSON } from '../utils/json' import { parseJSON } from '../utils/json'
import { extractFlags, getFlags } from './constants/flags'
import { fromHex } from '../utils/setHook'
export type SelectOption = { export type SelectOption = {
value: string value: string
label: string label: string
} }
export type HookParameters = {
[key: string]: SelectOption
}
export type Memos = {
[key: string]: {
type: string
format: string
data: string
}
}
export interface TransactionState { export interface TransactionState {
selectedTransaction: SelectOption | null selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null selectedAccount: SelectOption | null
selectedDestAccount: SelectOption | null selectedDestAccount: SelectOption | null
selectedFlags: SelectOption[] | null
hookParameters: HookParameters
memos: Memos
txIsLoading: boolean txIsLoading: boolean
txIsDisabled: boolean txIsDisabled: boolean
txFields: TxFields txFields: TxFields
@@ -22,15 +39,20 @@ export interface TransactionState {
estimatedFee?: string estimatedFee?: string
} }
const commonFields = ['TransactionType', 'Account', 'Sequence', "HookParameters"] as const;
export type TxFields = Omit< export type TxFields = Omit<
Partial<typeof transactionsData[0]>, Partial<typeof transactionsData[0]>,
'Account' | 'Sequence' | 'TransactionType' typeof commonFields[number]
> >
export const defaultTransaction: TransactionState = { export const defaultTransaction: TransactionState = {
selectedTransaction: null, selectedTransaction: null,
selectedAccount: null, selectedAccount: null,
selectedDestAccount: null, selectedDestAccount: null,
selectedFlags: null,
hookParameters: {},
memos: {},
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
@@ -128,9 +150,8 @@ export const prepareTransaction = (data: any) => {
try { try {
options[field] = JSON.parse(_value.$value) options[field] = JSON.parse(_value.$value)
} catch (error) { } catch (error) {
const message = `Input error for json field '${field}': ${ const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ''
error instanceof Error ? error.message : '' }`
}`
console.error(message) console.error(message)
options[field] = _value.$value options[field] = _value.$value
} }
@@ -156,7 +177,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return return
} }
const { Account, TransactionType, Destination, ...rest } = options const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options
let tx: Partial<TransactionState> = {} let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType) const schema = getTxFields(transactionType)
@@ -186,6 +207,22 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null 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) { if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination) const dest = state.accounts.find(acc => acc.address === Destination)
if (dest) { if (dest) {
@@ -205,6 +242,13 @@ export const prepareState = (value: string, transactionType?: string) => {
rest.Destination = Destination rest.Destination = Destination
} }
if (getFlags(TransactionType) && rest.Flags) {
const flags = extractFlags(TransactionType, rest.Flags)
rest.Flags = undefined
tx.selectedFlags = flags
}
Object.keys(rest).forEach(field => { Object.keys(rest).forEach(field => {
const value = rest[field] const value = rest[field]
const schemaVal = schema[field as keyof TxFields] const schemaVal = schema[field as keyof TxFields]
@@ -237,12 +281,12 @@ export const getTxFields = (tt?: string) => {
if (!txFields) return {} if (!txFields) return {}
let _txFields = Object.keys(txFields) let _txFields = Object.keys(txFields)
.filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key)) .filter(key => !commonFields.includes(key as any))
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {}) .reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {})
return _txFields return _txFields
} }
export { transactionsData } export { transactionsData, commonFields }
export const transactionsOptions = transactionsData.map(tx => ({ export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType, value: tx.TransactionType,

View File

@@ -1,6 +1,7 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { derive, sign } from 'xrpl-accountlib' import { derive, sign } from 'xrpl-accountlib'
import state, { IAccount } from '../state' import { IAccount } from '../state'
import { xrplSend } from '../state/actions/xrpl-client'
const estimateFee = async ( const estimateFee = async (
tx: Record<string, unknown>, tx: Record<string, unknown>,
@@ -22,7 +23,7 @@ const estimateFee = async (
const keypair = derive.familySeed(account.secret) const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(copyTx, keypair) const { signedTransaction } = sign(copyTx, keypair)
const res = await state.client?.send({ command: 'fee', tx_blob: signedTransaction }) const res = await xrplSend({ command: 'fee', tx_blob: signedTransaction })
if (res && res.drops) { if (res && res.drops) {
return res.drops return res.drops
} }

View File

@@ -13,3 +13,9 @@ export const capitalize = (value?: string) => {
return value[0].toLocaleUpperCase() + value.slice(1) return value[0].toLocaleUpperCase() + value.slice(1)
} }
export const getFileExtention = (filename?: string): string | undefined => {
if (!filename) return
const ext = (filename.includes('.') && filename.split('.').pop()) || undefined
return ext
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

913
yarn.lock

File diff suppressed because it is too large Load Diff