Compare commits

..

15 Commits

Author SHA1 Message Date
muzam1l
0fce9af77c Fetch sequence on account creation. 2023-03-10 16:18:35 +05:30
muzam1l
55c68c580a Account Sequence UI. 2023-03-10 16:02:32 +05:30
muzamil
832a7997d1 Merge pull request #288 from XRPLF/fix/tx
Fix tx json saving and discarding.
2023-03-08 21:32:36 +05:30
muzam1l
4528e5a16e Actually fix Json 'save'. 2023-03-08 20:33:23 +05:30
muzam1l
38f064c6d8 Fix json saving and discarding. 2023-03-08 17:13:36 +05:30
muzamil
fbf4565dbc Merge pull request #287 from XRPLF/feat/memos-ui
Memos UI
2023-03-07 17:32:32 +05:30
muzam1l
9001c64fed minor label changes. 2023-03-07 16:58:32 +05:30
muzam1l
03b768db4e UI for memos fields. 2023-03-07 16:03:47 +05:30
muzam1l
825af0db89 Add memos field in transactions. 2023-03-06 21:14:14 +05:30
muzamil
31043f33ab Merge pull request #286 from XRPLF/feat/tx-params-ui
HookParameters UI for transactions.
2023-03-06 16:06:19 +05:30
muzam1l
39699a1cb9 HookParameters UI for transactions. 2023-03-03 18:32:03 +05:30
muzam1l
b50b300307 Refactor tx. 2023-03-03 16:00:43 +05:30
muzamil
82c06cbb12 Merge pull request #285 from XRPLF/fix/params
Add invoke hookon option.
2023-03-02 19:17:38 +05:30
muzam1l
423ee18e6a Add Invoke HookOn option. 2023-03-02 15:11:44 +05:30
muzamil
3bb26d0c9b Merge pull request #281 from XRPLF/feat/testnet-v3
Testnet v3.
2023-02-10 16:12:22 +05:30
13 changed files with 548 additions and 242 deletions

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 => ({

71
components/Sequence.tsx Normal file
View File

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

View File

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

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,20 +41,43 @@ 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,
memos
} = state
const TransactionType = selectedTransaction?.value || null
const Destination = selectedDestAccount?.value || txFields?.Destination
const Account = selectedAccount?.value || null
const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
const HookParameters = Object.entries(hookParameters || {}).reduce<
SetHookData['HookParameters']
>((acc, [_, { label, value }]) => {
return acc.concat({
HookParameter: { HookParameterName: toHex(label), HookParameterValue: toHex(value) }
})
}, [])
const Memos = memos
? Object.entries(memos).reduce<SetHookData['Memos']>((acc, [_, { format, data, type }]) => {
return acc?.concat({
Memo: { MemoData: toHex(data), MemoFormat: toHex(format), MemoType: toHex(type) }
})
}, [])
: undefined
return prepareTransaction({
...txFields,
HookParameters,
Flags,
TransactionType,
Destination,
Account
Account,
Memos
})
},
[txState]
@@ -69,15 +93,29 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
}
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const saveEditorState = useCallback(
(value: string = '', transactionType?: string) => {
const pTx = prepareState(value, transactionType)
if (pTx) {
pTx.editorValue = getJsonString(pTx)
return setState(pTx)
}
},
[getJsonString, setState]
)
const submitTest = useCallback(async () => {
let st: TransactionState | undefined
const tt = txState.selectedTransaction?.value
if (viewType === 'json') {
// save the editor state first
const pst = prepareState(editorValue || '', tt)
if (!pst) return
st = setState(pst)
st = saveEditorState(editorValue, tt)
if (!st) return
}
const account = accounts.find(acc => acc.address === selectedAccount?.value)
@@ -108,23 +146,18 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
}
setState({ txIsLoading: false })
}, [
txState.selectedTransaction?.value,
viewType,
accounts,
txIsDisabled,
setState,
header,
saveEditorState,
editorValue,
txState,
selectedAccount?.value,
prepareOptions
])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value)
@@ -177,11 +210,21 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
[accounts, prepareOptions, setState, txState]
)
const switchToJson = useCallback(() => {
const editorValue = getJsonString()
setState({ viewType: 'json', editorValue })
}, [getJsonString, setState])
const switchToUI = useCallback(() => {
setState({ viewType: 'ui' })
}, [setState])
return (
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === 'json' ? (
<TxJson
getJsonString={getJsonString}
saveEditorState={saveEditorState}
header={header}
state={txState}
setState={setState}
@@ -189,6 +232,7 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
/>
) : (
<TxUI
switchToJson={switchToJson}
state={txState}
resetState={resetState}
setState={setState}
@@ -209,8 +253,8 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
<Button
onClick={() => {
if (viewType === 'ui') {
setState({ viewType: 'json' })
} else setState({ viewType: 'ui' })
switchToJson()
} else switchToUI()
}}
outline
>

View File

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

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,18 +18,34 @@ import { streamState } from '../DebugStream'
import { Button } from '..'
import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react'
import AccountSequence from '../Sequence'
interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined>
switchToJson: () => void
}
export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estimateFee }) => {
export const TxUI: FC<UIProps> = ({
state: txState,
setState,
resetState,
estimateFee,
switchToJson
}) => {
const { accounts } = useSnapshot(state)
const { selectedAccount, selectedDestAccount, selectedTransaction, txFields, selectedFlags } =
txState
const {
selectedAccount,
selectedDestAccount,
selectedTransaction,
txFields,
selectedFlags,
hookParameters,
memos
} = txState
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
@@ -94,8 +110,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[handleEstimateFee, resetState, setState]
)
const switchToJson = () => setState({ viewType: 'json' })
// default tx
useEffect(() => {
if (selectedTransaction?.value) return
@@ -110,7 +124,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[selectedTransaction?.value]
)
const richFields = ['TransactionType', 'Account']
const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
@@ -120,7 +134,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 +143,44 @@ 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>
<TxField label="Sequence">
<AccountSequence address={selectedAccount?.value} />
</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 +192,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 +214,239 @@ 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>
<TxField multiLine label="Memos">
<Flex column fluid>
{Object.entries(memos).map(([id, memo]) => (
<Flex column key={id} css={{ mb: '$2' }}>
<Flex
row
css={{
flexWrap: 'wrap',
width: '100%'
}}
>
<Input
placeholder="Memo type"
value={memo.type}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, type: e.target.value }
}
})
}}
/>
<Input
placeholder="Data"
css={{ mx: '$2' }}
value={memo.data}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, data: e.target.value }
}
})
}}
/>
<Input
placeholder="Format"
value={memo.format}
onChange={e => {
setState({
memos: {
...memos,
[id]: { ...memo, format: e.target.value }
}
})
}}
/>
<Button
css={{ ml: '$2' }}
onClick={() => {
const { [id]: _, ...rest } = memos
setState({ memos: rest })
}}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
</Flex>
))}
<Button
outline
fullWidth
type="button"
onClick={() => {
const id = Object.keys(memos).length
setState({
memos: { ...memos, [id]: { data: '', format: '', type: '' } }
})
}}
>
<Plus size="16px" />
Add Memo
</Button>
</Flex>
</TxField>
</Flex>
</Container>
)
}
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
label,
children,
multiLine = false
}) => {
return (
<Flex
row
fluid
css={{
justifyContent: 'flex-end',
alignItems: multiLine ? 'flex-start' : 'center',
position: 'relative',
mb: '$3',
mt: '1px',
pr: '1px'
}}
>
<Text muted css={{ mr: '$3', mt: multiLine ? '$2' : 0 }}>
{label}:{' '}
</Text>
<Flex css={{ width: '70%', alignItems: 'center' }}>{children}</Flex>
</Flex>
)
}

View File

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

View File

@@ -6,7 +6,7 @@ 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'
@@ -18,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) {

View File

@@ -31,6 +31,10 @@ export const sendTransaction = async (
...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)

View File

@@ -5,28 +5,46 @@ import state from '.'
import { showAlert } from '../state/actions/showAlert'
import { parseJSON } from '../utils/json'
import { extractFlags, getFlags } from './constants/flags'
import { fromHex } from '../utils/setHook'
export type SelectOption = {
value: string
label: string
}
export type HookParameters = {
[key: string]: SelectOption
}
export type Memos = {
[key: string]: {
type: string
format: string
data: string
}
}
export interface TransactionState {
selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null
selectedDestAccount: SelectOption | null
selectedFlags: SelectOption[] | null
hookParameters: HookParameters
memos: Memos
txIsLoading: boolean
txIsDisabled: boolean
txFields: TxFields
viewType: 'json' | 'ui'
editorValue?: string
editorIsSaved: boolean
estimatedFee?: string
}
const commonFields = ['TransactionType', 'Account', 'Sequence', "HookParameters"] as const;
export type TxFields = Omit<
Partial<typeof transactionsData[0]>,
'Account' | 'Sequence' | 'TransactionType'
typeof commonFields[number]
>
export const defaultTransaction: TransactionState = {
@@ -34,6 +52,9 @@ export const defaultTransaction: TransactionState = {
selectedAccount: null,
selectedDestAccount: null,
selectedFlags: null,
hookParameters: {},
memos: {},
editorIsSaved: true,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
@@ -158,7 +179,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return
}
const { Account, TransactionType, Destination, ...rest } = options
const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options
let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType)
@@ -188,6 +209,22 @@ export const prepareState = (value: string, transactionType?: string) => {
tx.selectedTransaction = null
}
if (HookParameters && HookParameters instanceof Array) {
tx.hookParameters = HookParameters.reduce<TransactionState["hookParameters"]>((acc, cur, idx) => {
const param = { label: fromHex(cur.HookParameter?.HookParameterName || ""), value: fromHex(cur.HookParameter?.HookParameterValue || "") }
acc[idx] = param;
return acc;
}, {})
}
if (Memos && Memos instanceof Array) {
tx.memos = Memos.reduce<TransactionState["memos"]>((acc, cur, idx) => {
const memo = { data: fromHex(cur.Memo?.MemoData || ""), type: fromHex(cur.Memo?.MemoType || ""), format: fromHex(cur.Memo?.MemoFormat || "") }
acc[idx] = memo;
return acc;
}, {})
}
if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination)
if (dest) {
@@ -236,6 +273,7 @@ export const prepareState = (value: string, transactionType?: string) => {
})
tx.txFields = rest
tx.editorIsSaved = true;
return tx
}
@@ -246,12 +284,12 @@ export const getTxFields = (tt?: string) => {
if (!txFields) return {}
let _txFields = Object.keys(txFields)
.filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key))
.filter(key => !commonFields.includes(key as any))
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {})
return _txFields
}
export { transactionsData }
export { transactionsData, commonFields }
export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,

31
utils/accountInfo.ts Normal file
View File

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

View File

@@ -23,7 +23,8 @@ 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

View File

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