Compare commits

...

10 Commits

Author SHA1 Message Date
muzam1l
3340857575 Add owner field to NFTokenCreateOffer UI. 2023-03-14 21:38:35 +05:30
muzamil
e1f34c4beb Merge pull request #291 from XRPLF/fix/sequence-ui
Fix Account sequence UI reset.
2023-03-14 21:30:35 +05:30
muzam1l
54a89c969e Fix account sequence reset. 2023-03-14 15:38:09 +05:30
muzamil
ded867d997 Merge pull request #289 from XRPLF/fix/tx
Account sequence.
2023-03-10 17:31:55 +05:30
muzam1l
0fce9af77c Fetch sequence on account creation. 2023-03-10 16:18:35 +05:30
muzam1l
55c68c580a Account Sequence UI. 2023-03-10 16:02:32 +05:30
muzamil
832a7997d1 Merge pull request #288 from XRPLF/fix/tx
Fix tx json saving and discarding.
2023-03-08 21:32:36 +05:30
muzam1l
4528e5a16e Actually fix Json 'save'. 2023-03-08 20:33:23 +05:30
muzam1l
38f064c6d8 Fix json saving and discarding. 2023-03-08 17:13:36 +05:30
muzamil
fbf4565dbc Merge pull request #287 from XRPLF/feat/memos-ui
Memos UI
2023-03-07 17:32:32 +05:30
9 changed files with 204 additions and 78 deletions

71
components/Sequence.tsx Normal file
View File

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

View File

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

View File

@@ -93,15 +93,29 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} }
}, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading]) }, [selectedAccount?.value, selectedTransaction?.value, setState, txIsLoading])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const saveEditorState = useCallback(
(value: string = '', transactionType?: string) => {
const pTx = prepareState(value, transactionType)
if (pTx) {
pTx.editorValue = getJsonString(pTx)
return setState(pTx)
}
},
[getJsonString, setState]
)
const submitTest = useCallback(async () => { const submitTest = useCallback(async () => {
let st: TransactionState | undefined let st: TransactionState | undefined
const tt = txState.selectedTransaction?.value const tt = txState.selectedTransaction?.value
if (viewType === 'json') { if (viewType === 'json') {
// save the editor state first st = saveEditorState(editorValue, tt)
const pst = prepareState(editorValue || '', tt) if (!st) return
if (!pst) return
st = setState(pst)
} }
const account = accounts.find(acc => acc.address === selectedAccount?.value) const account = accounts.find(acc => acc.address === selectedAccount?.value)
@@ -132,23 +146,18 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} }
setState({ txIsLoading: false }) setState({ txIsLoading: false })
}, [ }, [
txState.selectedTransaction?.value,
viewType, viewType,
accounts, accounts,
txIsDisabled, txIsDisabled,
setState, setState,
header, header,
saveEditorState,
editorValue, editorValue,
txState,
selectedAccount?.value, selectedAccount?.value,
prepareOptions prepareOptions
]) ])
const getJsonString = useCallback(
(state?: Partial<TransactionState>) =>
JSON.stringify(prepareOptions?.(state) || {}, null, editorSettings.tabSize),
[editorSettings.tabSize, prepareOptions]
)
const resetState = useCallback( const resetState = useCallback(
(transactionType: SelectOption | undefined = defaultTransactionType) => { (transactionType: SelectOption | undefined = defaultTransactionType) => {
const fields = getTxFields(transactionType?.value) const fields = getTxFields(transactionType?.value)
@@ -201,11 +210,21 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
[accounts, prepareOptions, setState, txState] [accounts, prepareOptions, setState, txState]
) )
const switchToJson = useCallback(() => {
const editorValue = getJsonString()
setState({ viewType: 'json', editorValue })
}, [getJsonString, setState])
const switchToUI = useCallback(() => {
setState({ viewType: 'ui' })
}, [setState])
return ( return (
<Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}> <Box css={{ position: 'relative', height: 'calc(100% - 28px)' }} {...props}>
{viewType === 'json' ? ( {viewType === 'json' ? (
<TxJson <TxJson
getJsonString={getJsonString} getJsonString={getJsonString}
saveEditorState={saveEditorState}
header={header} header={header}
state={txState} state={txState}
setState={setState} setState={setState}
@@ -213,6 +232,7 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
/> />
) : ( ) : (
<TxUI <TxUI
switchToJson={switchToJson}
state={txState} state={txState}
resetState={resetState} resetState={resetState}
setState={setState} setState={setState}
@@ -233,8 +253,8 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
<Button <Button
onClick={() => { onClick={() => {
if (viewType === 'ui') { if (viewType === 'ui') {
setState({ viewType: 'json' }) switchToJson()
} else setState({ viewType: 'ui' }) } else switchToUI()
}} }}
outline outline
> >

View File

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

View File

@@ -19,15 +19,23 @@ import { Button } from '..'
import Textarea from '../Textarea' import Textarea from '../Textarea'
import { getFlags } from '../../state/constants/flags' import { getFlags } from '../../state/constants/flags'
import { Plus, Trash } from 'phosphor-react' import { Plus, Trash } from 'phosphor-react'
import AccountSequence from '../Sequence'
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
resetState: (tt?: SelectOption) => TransactionState | undefined resetState: (tt?: SelectOption) => TransactionState | undefined
state: TransactionState state: TransactionState
estimateFee?: (...arg: any) => Promise<string | undefined> estimateFee?: (...arg: any) => Promise<string | undefined>
switchToJson: () => void
} }
export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estimateFee }) => { export const TxUI: FC<UIProps> = ({
state: txState,
setState,
resetState,
estimateFee,
switchToJson
}) => {
const { accounts } = useSnapshot(state) const { accounts } = useSnapshot(state)
const { const {
selectedAccount, selectedAccount,
@@ -102,8 +110,6 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
[handleEstimateFee, resetState, setState] [handleEstimateFee, resetState, setState]
) )
const switchToJson = () => setState({ viewType: 'json' })
// default tx // default tx
useEffect(() => { useEffect(() => {
if (selectedTransaction?.value) return if (selectedTransaction?.value) return
@@ -156,6 +162,9 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
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
/> />
</TxField> </TxField>
<TxField label="Sequence">
<AccountSequence address={selectedAccount?.value} />
</TxField>
{richFields.includes('Destination') && ( {richFields.includes('Destination') && (
<TxField label="Destination account"> <TxField label="Destination account">
<Select <Select
@@ -342,7 +351,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState, resetState, estima
row row
css={{ css={{
flexWrap: 'wrap', flexWrap: 'wrap',
width: '100%', width: '100%'
}} }}
> >
<Input <Input

View File

@@ -124,7 +124,8 @@
}, },
"Flags": "1", "Flags": "1",
"Destination": "", "Destination": "",
"Fee": "10" "Fee": "10",
"Owner": ""
}, },
{ {
"TransactionType": "OfferCancel", "TransactionType": "OfferCancel",

View File

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

View File

@@ -36,6 +36,7 @@ export interface TransactionState {
txFields: TxFields txFields: TxFields
viewType: 'json' | 'ui' viewType: 'json' | 'ui'
editorValue?: string editorValue?: string
editorIsSaved: boolean
estimatedFee?: string estimatedFee?: string
} }
@@ -53,6 +54,7 @@ export const defaultTransaction: TransactionState = {
selectedFlags: null, selectedFlags: null,
hookParameters: {}, hookParameters: {},
memos: {}, memos: {},
editorIsSaved: true,
txIsLoading: false, txIsLoading: false,
txIsDisabled: false, txIsDisabled: false,
txFields: {}, txFields: {},
@@ -271,6 +273,7 @@ export const prepareState = (value: string, transactionType?: string) => {
}) })
tx.txFields = rest tx.txFields = rest
tx.editorIsSaved = true;
return tx return tx
} }

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