Compare commits

..

13 Commits

Author SHA1 Message Date
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
24 changed files with 4203 additions and 2070 deletions

View File

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

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ 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
@@ -40,16 +41,30 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
const prepareOptions = useCallback(
(state: Partial<TransactionState> = txState) => {
const { selectedTransaction, selectedDestAccount, selectedAccount, txFields, selectedFlags } =
state
const {
selectedTransaction,
selectedDestAccount,
selectedAccount,
txFields,
selectedFlags,
hookParameters
} = 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) }
})
}, [])
return prepareTransaction({
...txFields,
HookParameters,
Flags,
TransactionType,
Destination,

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 Flex from '../Flex'
import Input from '../Input'
@@ -18,6 +18,7 @@ import { streamState } from '../DebugStream'
import { Button } from '..'
import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
@@ -28,8 +29,14 @@ interface UIProps {
export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estimateFee }) => {
const { accounts } = useSnapshot(state)
const { selectedAccount, selectedDestAccount, selectedTransaction, txFields, selectedFlags } =
txState
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
selectedFlags,
hookParameters
} = txState
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
@@ -110,7 +117,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[selectedTransaction?.value]
)
const richFields = ['TransactionType', 'Account']
const richFields = ['TransactionType', 'Account', 'HookParameters']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
@@ -120,7 +127,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
}
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
return (
<Container
css={{
@@ -130,94 +136,41 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
}}
>
<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>
</TxField>
{richFields.includes('Destination') && (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', textAlign: 'end' }}>
Destination account:{' '}
</Text>
<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') && (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: 'center',
mb: '$3',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3' }}>
Flags:{' '}
</Text>
<TxField label="Flags">
<Select
isClearable
css={{ width: '70%' }}
instanceId="flags"
placeholder="Select flags to apply"
menuPosition="fixed"
@@ -229,7 +182,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
selectedFlags ? selectedFlags.length >= flagsOptions.length - 1 : false
}
/>
</Flex>
</TxField>
)}
{otherFields.map(field => {
let _value = txFields[field]
@@ -251,93 +204,162 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
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>
</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>
)
}

View File

@@ -245,5 +245,14 @@
}
},
"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.module.rules.push({
test: /\.md$/,
test: [/\.md$/, /hook-bundle\.js$/],
use: 'raw-loader'
})
return config

View File

@@ -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",
@@ -65,8 +66,8 @@
"vscode-languageserver": "^7.0.0",
"vscode-uri": "^3.0.2",
"wabt": "^1.0.30",
"xrpl-accountlib": "^1.5.2",
"xrpl-client": "^1.9.4"
"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",

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
export default content
}
declare module '*.hook-bundle.js' {
const content: string
export default content
}

View File

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

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 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
})

View File

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

View File

@@ -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'
})

View File

@@ -5,17 +5,23 @@ 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 interface TransactionState {
selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null
selectedDestAccount: SelectOption | null
selectedFlags: SelectOption[] | null
hookParameters: HookParameters
txIsLoading: boolean
txIsDisabled: boolean
txFields: TxFields
@@ -24,9 +30,11 @@ export interface TransactionState {
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 = {
@@ -34,6 +42,7 @@ export const defaultTransaction: TransactionState = {
selectedAccount: null,
selectedDestAccount: null,
selectedFlags: null,
hookParameters: {},
txIsLoading: false,
txIsDisabled: false,
txFields: {},
@@ -158,7 +167,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return
}
const { Account, TransactionType, Destination, ...rest } = options
const { Account, TransactionType, Destination, HookParameters, ...rest } = options
let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType)
@@ -188,6 +197,14 @@ 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 (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination)
if (dest) {
@@ -246,12 +263,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,

View File

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

View File

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

View File

@@ -74,3 +74,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
}

899
yarn.lock

File diff suppressed because it is too large Load Diff