Compare commits

...

22 Commits

Author SHA1 Message Date
muzam1l
2cf18ef61c Add Destination field to Invoke transaction. 2023-03-27 15:32:25 +05:30
muzamil
4d2dc16ce5 Merge pull request #296 from XRPLF/feat/account-ui
Extend transactions Account UI.
2023-03-23 20:45:27 +05:30
muzam1l
9ecf5478e6 Remove tx Destination default values. 2023-03-23 19:39:56 +05:30
muzam1l
6d1ef110b7 Token Issuer account UI. 2023-03-23 17:52:17 +05:30
muzam1l
b9edfcd63b Fix: nextjs build. 2023-03-23 16:55:32 +05:30
muzam1l
b653d9a9cb Fix: Remove last traces of custom Destination handling! 2023-03-23 16:47:25 +05:30
muzam1l
da28e0a7d1 Use "creatable select" for accounts. 2023-03-23 16:45:24 +05:30
muzam1l
8a5b83d57f Fix: Don't remove fields in JSON mode if empty. 2023-03-23 16:26:29 +05:30
muzam1l
025eff6cf2 Add Select UI for account. 2023-03-23 16:12:35 +05:30
muzam1l
62d521b2cc Add owner field to NFTokenCreateOffer. 2023-03-23 15:51:36 +05:30
muzam1l
7aafca21df Remove Destination as special field. 2023-03-23 15:49:17 +05:30
muzamil
80f58e903c Merge pull request #294 from XRPLF/feat/amount-ui
Transaction amount UI.
2023-03-20 14:04:30 +05:30
muzam1l
c4af3df017 Add DeliverMin field to tx CheckCash. 2023-03-17 20:10:57 +05:30
muzam1l
5d8d142bc4 Catch all estimate fee errors. 2023-03-17 19:01:32 +05:30
muzam1l
e27a71d713 Estimate fee correct error message and amount input type. 2023-03-17 18:57:03 +05:30
muzam1l
e08b07cbeb Update tx OfferCreate. 2023-03-17 18:03:37 +05:30
muzam1l
e4936c03ef Css changes. 2023-03-17 17:17:29 +05:30
muzam1l
21a69ac8ea minor type fix. 2023-03-17 16:59:12 +05:30
muzam1l
52e4f219f7 Transaction amount UI. 2023-03-17 16:49:00 +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
11 changed files with 449 additions and 235 deletions

View File

@@ -3,13 +3,11 @@ import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import { styled } from '../stitches.config' import { styled } from '../stitches.config'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import type { Props } from 'react-select' import type { Props, StylesConfig } from 'react-select'
const SelectInput = dynamic(() => import('react-select'), { ssr: false }) const SelectInput = dynamic(() => import('react-select'), { ssr: false })
const CreatableSelectInput = dynamic(() => import('react-select/creatable'), { ssr: false })
// eslint-disable-next-line react/display-name const getColors = (isDark: boolean) => {
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const colors: any = { const colors: any = {
// primary: pink.pink9, // primary: pink.pink9,
active: isDark ? purpleDark.purple9 : purple.purple9, active: isDark ? purpleDark.purple9 : purple.purple9,
@@ -30,11 +28,12 @@ const Select = forwardRef<any, Props>((props, ref) => {
} }
colors.outline = colors.background colors.outline = colors.background
colors.selected = colors.secondary colors.selected = colors.secondary
return ( return colors
<SelectInput }
ref={ref}
menuPosition={props.menuPosition || 'fixed'} const getStyles = (isDark: boolean) => {
styles={{ const colors = getColors(isDark)
const styles: StylesConfig = {
container: provided => { container: provided => {
return { return {
...provided, ...provided,
@@ -108,16 +107,57 @@ const Select = forwardRef<any, Props>((props, ref) => {
dropdownIndicator: (provided, state) => { dropdownIndicator: (provided, state) => {
return { return {
...provided, ...provided,
padding: 6,
color: state.isFocused ? colors.border : colors.secondary, color: state.isFocused ? colors.border : colors.secondary,
':hover': { ':hover': {
color: colors.border color: colors.border
} }
} }
},
clearIndicator: provided => {
return {
...provided,
padding: 6,
color: colors.secondary,
':hover': {
color: colors.border
} }
}} }
}
}
return styles
}
// eslint-disable-next-line react/display-name
const Select = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return (
<SelectInput
ref={ref}
menuPosition={props.menuPosition || 'fixed'}
styles={styles}
{...props}
/>
)
})
// eslint-disable-next-line react/display-name
const Creatable = forwardRef<any, Props>((props, ref) => {
const { theme } = useTheme()
const isDark = theme === 'dark'
const styles = getStyles(isDark)
return (
<CreatableSelectInput
ref={ref}
formatCreateLabel={label => `Enter "${label}"`}
menuPosition={props.menuPosition || 'fixed'}
styles={styles}
{...props} {...props}
/> />
) )
}) })
export default styled(Select, {}) export default styled(Select, {})
export const CreatableSelect = styled(Creatable, {})

View File

@@ -38,7 +38,7 @@ const AccountSequence: FC<AccountSequenceProps> = ({ address }) => {
<Flex row align="center" fluid> <Flex row align="center" fluid>
<Input <Input
placeholder="Account sequence" placeholder="Account sequence"
value={account?.sequence} value={account?.sequence || ""}
disabled={!account} disabled={!account}
type="number" type="number"
readOnly={true} readOnly={true}

View File

@@ -43,7 +43,6 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
(state: Partial<TransactionState> = txState) => { (state: Partial<TransactionState> = txState) => {
const { const {
selectedTransaction, selectedTransaction,
selectedDestAccount,
selectedAccount, selectedAccount,
txFields, txFields,
selectedFlags, selectedFlags,
@@ -52,7 +51,6 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
} = state } = state
const TransactionType = selectedTransaction?.value || null const TransactionType = selectedTransaction?.value || null
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 Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags
const HookParameters = Object.entries(hookParameters || {}).reduce< const HookParameters = Object.entries(hookParameters || {}).reduce<
@@ -75,7 +73,6 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
HookParameters, HookParameters,
Flags, Flags,
TransactionType, TransactionType,
Destination,
Account, Account,
Memos Memos
}) })
@@ -128,11 +125,12 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
throw Error('Account must be selected from imported accounts!') throw Error('Account must be selected from imported accounts!')
} }
const options = prepareOptions(st) const options = prepareOptions(st)
// delete unnecessary fields
const fields = getTxFields(options.TransactionType) Object.keys(options).forEach(field => {
if (fields.Destination && !options.Destination) { if (!options[field]) {
throw Error('Destination account is required!') delete options[field]
} }
})
await sendTransaction(account, options, { logPrefix }) await sendTransaction(account, options, { logPrefix })
} catch (error) { } catch (error) {
@@ -167,13 +165,6 @@ const Transaction: FC<TransactionProps> = ({ header, state: txState, ...props })
selectedTransaction: transactionType selectedTransaction: transactionType
} }
if (fields.Destination !== undefined) {
nwState.selectedDestAccount = null
fields.Destination = ''
} else {
fields.Destination = undefined
}
if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) { if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) {
nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags) nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags)
fields.Flags = undefined fields.Flags = undefined

View File

@@ -1,25 +1,25 @@
import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react' import { FC, ReactNode, useCallback, useEffect, 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'
import Select from '../Select' import Select, { CreatableSelect } from '../Select'
import Text from '../Text' import Text from '../Text'
import { import {
SelectOption, SelectOption,
TransactionState, TransactionState,
transactionsOptions, transactionsOptions,
TxFields, TxFields,
getTxFields,
defaultTransactionType defaultTransactionType
} from '../../state/transactions' } from '../../state/transactions'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import state from '../../state' import state from '../../state'
import { streamState } from '../DebugStream' import { streamState } from '../DebugStream'
import { Button } from '..' import { Box, 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' import AccountSequence from '../Sequence'
import { capitalize, typeIs } from '../../utils/helpers'
interface UIProps { interface UIProps {
setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined setState: (pTx?: Partial<TransactionState> | undefined) => TransactionState | undefined
@@ -37,28 +37,14 @@ export const TxUI: FC<UIProps> = ({
switchToJson switchToJson
}) => { }) => {
const { accounts } = useSnapshot(state) const { accounts } = useSnapshot(state)
const { const { selectedAccount, selectedTransaction, txFields, selectedFlags, hookParameters, memos } =
selectedAccount, txState
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,
value: acc.address value: acc.address
})) }))
const destAccountOptions: SelectOption[] = accounts
.map(acc => ({
label: acc.name,
value: acc.address
}))
.filter(acc => acc.value !== selectedAccount?.value)
const flagsOptions: SelectOption[] = Object.entries( const flagsOptions: SelectOption[] = Object.entries(
getFlags(selectedTransaction?.value) || {} getFlags(selectedTransaction?.value) || {}
).map(([label, value]) => ({ ).map(([label, value]) => ({
@@ -87,6 +73,22 @@ export const TxUI: FC<UIProps> = ({
[setState, txFields] [setState, txFields]
) )
const setRawField = useCallback(
(field: keyof TxFields, type: string, value: any) => {
// TODO $type should be a narrowed type
setState({
txFields: {
...txFields,
[field]: {
$type: type,
$value: value
}
}
})
},
[setState, txFields]
)
const handleEstimateFee = useCallback( const handleEstimateFee = useCallback(
async (state?: TransactionState, silent?: boolean) => { async (state?: TransactionState, silent?: boolean) => {
setFeeLoading(true) setFeeLoading(true)
@@ -119,21 +121,23 @@ export const TxUI: FC<UIProps> = ({
} }
}, [handleChangeTxType, selectedTransaction?.value]) }, [handleChangeTxType, selectedTransaction?.value])
const fields = useMemo(
() => getTxFields(selectedTransaction?.value),
[selectedTransaction?.value]
)
const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos'] const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos']
if (fields.Destination !== undefined) {
richFields.push('Destination')
}
if (flagsOptions.length) { if (flagsOptions.length) {
richFields.push('Flags') richFields.push('Flags')
} }
const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields] const otherFields = Object.keys(txFields).filter(k => !richFields.includes(k)) as [keyof TxFields]
const amountOptions = [
{ label: 'XRP', value: 'xrp' },
{ label: 'Token', value: 'token' }
] as const
const defaultTokenAmount = {
value: '0',
currency: '',
issuer: ''
}
return ( return (
<Container <Container
css={{ css={{
@@ -165,18 +169,6 @@ export const TxUI: FC<UIProps> = ({
<TxField label="Sequence"> <TxField label="Sequence">
<AccountSequence address={selectedAccount?.value} /> <AccountSequence address={selectedAccount?.value} />
</TxField> </TxField>
{richFields.includes('Destination') && (
<TxField label="Destination account">
<Select
instanceId="to-account"
placeholder="Select the destination account"
options={destAccountOptions}
value={selectedDestAccount}
isClearable
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
/>
</TxField>
)}
{richFields.includes('Flags') && ( {richFields.includes('Flags') && (
<TxField label="Flags"> <TxField label="Flags">
<Select <Select
@@ -198,23 +190,133 @@ export const TxUI: FC<UIProps> = ({
let _value = txFields[field] let _value = txFields[field]
let value: string | undefined let value: string | undefined
if (typeof _value === 'object') { if (typeIs(_value, 'object')) {
if (_value.$type === 'json' && typeof _value.$value === 'object') { if (_value.$type === 'json' && typeIs(_value.$value, ['object', 'array'])) {
value = JSON.stringify(_value.$value, null, 2) value = JSON.stringify(_value.$value, null, 2)
} else { } else {
value = _value.$value.toString() value = _value.$value?.toString()
} }
} else { } else {
value = _value?.toString() value = _value?.toString()
} }
const isXrp = typeof _value === 'object' && _value.$type === 'xrp' const isAccount = typeIs(_value, 'object') && _value.$type === 'account'
const isXrpAmount = typeIs(_value, 'object') && _value.$type === 'amount.xrp'
const isTokenAmount = typeIs(_value, 'object') && _value.$type === 'amount.token'
const isJson = typeof _value === 'object' && _value.$type === 'json' const isJson = typeof _value === 'object' && _value.$type === 'json'
const isFee = field === 'Fee' const isFee = field === 'Fee'
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
let tokenAmount = defaultTokenAmount
if (isTokenAmount && typeIs(_value, 'object') && typeIs(_value.$value, 'object')) {
tokenAmount = {
value: _value.$value.value,
currency: _value.$value.currency,
issuer: _value.$value.issuer
}
}
if (isXrpAmount || isTokenAmount) {
return ( return (
<TxField key={field} label={field + (isXrp ? ' (XRP)' : '')}> <TxField key={field} label={field}>
<Flex fluid css={{ alignItems: 'center' }}>
{isTokenAmount ? (
<Flex
fluid
row
align="center"
justify="space-between"
css={{ position: 'relative' }}
>
{/* <Input
type="text"
placeholder="Issuer"
value={tokenAmount.issuer}
onChange={e =>
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: e.target.value
})
}
/> */}
<Input
type="text"
value={tokenAmount.currency}
placeholder="Currency"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
currency: e.target.value
})
}}
/>
<Input
css={{ mx: '$1' }}
type="number"
value={tokenAmount.value}
placeholder="Value"
onChange={e => {
setRawField(field, 'amount.token', {
...tokenAmount,
value: e.target.value
})
}}
/>
<Box css={{ width: '50%' }}>
<CreatableAccount
value={tokenAmount.issuer}
field={'Issuer' as any}
placeholder="Issuer"
setField={(_, value = '') => {
setRawField(field, 'amount.token', {
...tokenAmount,
issuer: value
})
}}
/>
</Box>
</Flex>
) : (
<Input
css={{ flex: 'inherit' }}
type="number"
value={value}
onChange={e => handleSetField(field, e.target.value)}
/>
)}
<Box
css={{
ml: '$2',
width: '150px'
}}
>
<Select
instanceId="currency-type"
options={amountOptions}
value={isXrpAmount ? amountOptions['0'] : amountOptions['1']}
onChange={(e: any) => {
const opt = e as typeof amountOptions[number]
if (opt.value === 'xrp') {
setRawField(field, 'amount.xrp', '0')
} else {
setRawField(field, 'amount.token', defaultTokenAmount)
}
}}
/>
</Box>
</Flex>
</TxField>
)
}
if (isAccount) {
return (
<TxField key={field} label={field}>
<CreatableAccount value={value} field={field} setField={handleSetField} />
</TxField>
)
}
return (
<TxField key={field} label={field}>
{isJson ? ( {isJson ? (
<Textarea <Textarea
rows={rows} rows={rows}
@@ -425,6 +527,35 @@ export const TxUI: FC<UIProps> = ({
) )
} }
export const CreatableAccount: FC<{
value: string | undefined
field: keyof TxFields
placeholder?: string
setField: (field: keyof TxFields, value: string, opFields?: TxFields) => void
}> = ({ value, field, setField, placeholder }) => {
const { accounts } = useSnapshot(state)
const accountOptions: SelectOption[] = accounts.map(acc => ({
label: acc.name,
value: acc.address
}))
const label = accountOptions.find(a => a.value === value)?.label || value
const val = {
value,
label
}
placeholder = placeholder || `${capitalize(field)} account`
return (
<CreatableSelect
isClearable
instanceId={field}
placeholder={placeholder}
options={accountOptions}
value={value ? val : undefined}
onChange={(acc: any) => setField(field, acc?.value)}
/>
)
}
export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({ export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({
label, label,
children, children,
@@ -438,7 +569,7 @@ export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boole
justifyContent: 'flex-end', justifyContent: 'flex-end',
alignItems: multiLine ? 'flex-start' : 'center', alignItems: multiLine ? 'flex-start' : 'center',
position: 'relative', position: 'relative',
mb: '$3', mb: '$2',
mt: '1px', mt: '1px',
pr: '1px' pr: '1px'
}} }}

View File

@@ -2,7 +2,10 @@
{ {
"TransactionType": "AccountDelete", "TransactionType": "AccountDelete",
"Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm",
"Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", "Destination": {
"$type": "account",
"$value": ""
},
"DestinationTag": 13, "DestinationTag": 13,
"Fee": "2000000", "Fee": "2000000",
"Sequence": 2470665, "Sequence": 2470665,
@@ -28,7 +31,11 @@
"TransactionType": "CheckCash", "TransactionType": "CheckCash",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"DeliverMin": {
"$value": "",
"$type": "amount.xrp"
}, },
"CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334", "CheckID": "838766BA2B995C00744175F69A1B11E32C3DBC40E64801A4056FCBD657F57334",
"Fee": "12" "Fee": "12"
@@ -36,7 +43,10 @@
{ {
"TransactionType": "CheckCreate", "TransactionType": "CheckCreate",
"Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo",
"Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", "Destination": {
"$type": "account",
"$value": ""
},
"SendMax": "100000000", "SendMax": "100000000",
"Expiration": 570113521, "Expiration": 570113521,
"InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B", "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B",
@@ -54,7 +64,10 @@
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowCancel", "TransactionType": "EscrowCancel",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Fee": "10" "Fee": "10"
}, },
@@ -63,9 +76,12 @@
"TransactionType": "EscrowCreate", "TransactionType": "EscrowCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"CancelAfter": 533257958, "CancelAfter": 533257958,
"FinishAfter": 533171558, "FinishAfter": 533171558,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
@@ -76,7 +92,10 @@
{ {
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"TransactionType": "EscrowFinish", "TransactionType": "EscrowFinish",
"Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Owner": {
"$type": "account",
"$value": ""
},
"OfferSequence": 7, "OfferSequence": 7,
"Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100",
"Fulfillment": "A0028000", "Fulfillment": "A0028000",
@@ -120,10 +139,17 @@
"NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007", "NFTokenID": "000100001E962F495F07A990F4ED55ACCFEEF365DBAA76B6A048C0A200000007",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Flags": "1", "Flags": "1",
"Destination": "", "Destination": {
"$type": "account",
"$value": ""
},
"Owner": {
"$type": "account",
"$value": ""
},
"Fee": "10" "Fee": "10"
}, },
{ {
@@ -142,19 +168,29 @@
"Flags": "0", "Flags": "0",
"LastLedgerSequence": 7108682, "LastLedgerSequence": 7108682,
"Sequence": 8, "Sequence": 8,
"TakerGets": "6000000", "TakerGets": {
"Amount": { "$type": "amount.xrp",
"$value": "100", "$value": "6000000"
"$type": "xrp" },
"TakerPays": {
"$type": "amount.token",
"$value": {
"currency": "GKO",
"issuer": "ruazs5h1qEsqpke88pcqnaseXdm6od2xc",
"value": "2"
}
} }
}, },
{ {
"TransactionType": "Payment", "TransactionType": "Payment",
"Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
"Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "Destination": {
"$type": "account",
"$value": ""
},
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Fee": "12", "Fee": "12",
"Flags": "2147483648", "Flags": "2147483648",
@@ -165,9 +201,12 @@
"TransactionType": "PaymentChannelCreate", "TransactionType": "PaymentChannelCreate",
"Amount": { "Amount": {
"$value": "100", "$value": "100",
"$type": "xrp" "$type": "amount.xrp"
},
"Destination": {
"$type": "account",
"$value": ""
}, },
"Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW",
"SettleDelay": 86400, "SettleDelay": 86400,
"PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A",
"CancelAfter": 533171558, "CancelAfter": 533171558,
@@ -181,7 +220,7 @@
"Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198", "Channel": "C1AE6DDDEEC05CF2978C0BAD6FE302948E9533691DC749DCDD3B9E5992CA6198",
"Amount": { "Amount": {
"$value": "200", "$value": "200",
"$type": "xrp" "$type": "amount.xrp"
}, },
"Expiration": 543171558, "Expiration": 543171558,
"Fee": "10" "Fee": "10"
@@ -237,7 +276,7 @@
"Flags": "262144", "Flags": "262144",
"LastLedgerSequence": 8007750, "LastLedgerSequence": 8007750,
"LimitAmount": { "LimitAmount": {
"$type": "json", "$type": "amount.token",
"$value": { "$value": {
"currency": "USD", "currency": "USD",
"issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc", "issuer": "rsP3mgGb2tcYUrxiLFiHJiQXhsziegtwBc",
@@ -248,6 +287,10 @@
}, },
{ {
"TransactionType": "Invoke", "TransactionType": "Invoke",
"Destination": {
"$type": "account",
"$value": ""
},
"Fee": "12" "Fee": "12"
}, },
{ {

View File

@@ -14,11 +14,4 @@ export const deleteAccount = (addr?: string) => {
if (!acc) return if (!acc) return
acc.label = acc.value acc.label = acc.value
}) })
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount
if (!acc) return
acc.label = acc.value
})
} }

View File

@@ -10,7 +10,6 @@ interface TransactionOptions {
TransactionType: string TransactionType: string
Account?: string Account?: string
Fee?: string Fee?: string
Destination?: string
[index: string]: any [index: string]: any
} }
interface OtherOptions { interface OtherOptions {

View File

@@ -6,6 +6,7 @@ import { showAlert } from '../state/actions/showAlert'
import { parseJSON } from '../utils/json' import { parseJSON } from '../utils/json'
import { extractFlags, getFlags } from './constants/flags' import { extractFlags, getFlags } from './constants/flags'
import { fromHex } from '../utils/setHook' import { fromHex } from '../utils/setHook'
import { typeIs } from '../utils/helpers'
export type SelectOption = { export type SelectOption = {
value: string value: string
@@ -27,7 +28,6 @@ export type Memos = {
export interface TransactionState { export interface TransactionState {
selectedTransaction: SelectOption | null selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null selectedAccount: SelectOption | null
selectedDestAccount: SelectOption | null
selectedFlags: SelectOption[] | null selectedFlags: SelectOption[] | null
hookParameters: HookParameters hookParameters: HookParameters
memos: Memos memos: Memos
@@ -50,7 +50,6 @@ export type TxFields = Omit<
export const defaultTransaction: TransactionState = { export const defaultTransaction: TransactionState = {
selectedTransaction: null, selectedTransaction: null,
selectedAccount: null, selectedAccount: null,
selectedDestAccount: null,
selectedFlags: null, selectedFlags: null,
hookParameters: {}, hookParameters: {},
memos: {}, memos: {},
@@ -130,46 +129,51 @@ export const modifyTxState = (
return tx.state return tx.state
} }
// state to tx options
export const prepareTransaction = (data: any) => { export const prepareTransaction = (data: any) => {
let options = { ...data } let options = { ...data }
Object.keys(options).forEach(field => { Object.keys(options).forEach(field => {
let _value = options[field] let _value = options[field]
// convert xrp if (!typeIs(_value, 'object')) return
if (_value && typeof _value === 'object' && _value.$type === 'xrp') { // amount.xrp
if (+_value.$value) { if (_value.$type === 'amount.xrp') {
options[field] = (+_value.$value * 1000000 + '') as any if (_value.$value) {
options[field] = (+(_value as any).$value * 1000000 + '')
} else { } else {
options[field] = undefined // 👇 💀 options[field] = ""
} }
} }
// handle type: `json` // amount.token
if (_value && typeof _value === 'object' && _value.$type === 'json') { if (_value.$type === 'amount.token') {
if (typeof _value.$value === 'object') { if (typeIs(_value.$value, 'string')) {
options[field] = parseJSON(_value.$value)
} else if (typeIs(_value.$value, 'object')) {
options[field] = _value.$value options[field] = _value.$value
} else { } else {
try { options[field] = undefined
options[field] = JSON.parse(_value.$value)
} catch (error) {
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ''
}`
console.error(message)
options[field] = _value.$value
} }
} }
// account
if (_value.$type === 'account') {
options[field] = (_value.$value as any)?.toString() || ""
}
// json
if (_value.$type === 'json') {
const val = _value.$value;
let res: any = val;
if (typeIs(val, ["object", "array"])) {
options[field] = res
} else if (typeIs(val, "string") && (res = parseJSON(val))) {
options[field] = res;
} else {
options[field] = res;
} }
// delete unnecessary fields
if (!options[field]) {
delete options[field]
} }
}) })
return options return options
} }
// editor value to state
export const prepareState = (value: string, transactionType?: string) => { export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value) const options = parseJSON(value)
if (!options) { if (!options) {
@@ -179,7 +183,7 @@ export const prepareState = (value: string, transactionType?: string) => {
return return
} }
const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options const { Account, TransactionType, HookParameters, Memos, ...rest } = options
let tx: Partial<TransactionState> = {} let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType) const schema = getTxFields(transactionType)
@@ -225,24 +229,6 @@ export const prepareState = (value: string, transactionType?: string) => {
}, {}) }, {})
} }
if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination)
if (dest) {
tx.selectedDestAccount = {
label: dest.name,
value: dest.address
}
} else if (Destination) {
tx.selectedDestAccount = {
label: Destination,
value: Destination
}
} else {
tx.selectedDestAccount = null
}
} else if (Destination) {
rest.Destination = Destination
}
if (getFlags(TransactionType) && rest.Flags) { if (getFlags(TransactionType) && rest.Flags) {
const flags = extractFlags(TransactionType, rest.Flags) const flags = extractFlags(TransactionType, rest.Flags)
@@ -254,17 +240,31 @@ export const prepareState = (value: string, transactionType?: string) => {
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]
const isXrp =
typeof value !== 'object' && const isAmount = schemaVal &&
schemaVal && typeIs(schemaVal, "object") &&
typeof schemaVal === 'object' && schemaVal.$type.startsWith('amount.');
schemaVal.$type === 'xrp' const isAccount = schemaVal &&
if (isXrp) { typeIs(schemaVal, "object") &&
schemaVal.$type.startsWith("account");
if (isAmount && ["number", "string"].includes(typeof value)) {
rest[field] = { rest[field] = {
$type: 'xrp', $type: 'amount.xrp', // TODO narrow typed $type.
$value: +value / 1000000 // ! maybe use bigint? $value: +value / 1000000 // ! maybe use bigint?
} }
} else if (typeof value === 'object') { } else if (isAmount && typeof value === 'object') {
rest[field] = {
$type: 'amount.token',
$value: value
}
} else if (isAccount) {
rest[field] = {
$type: "account",
$value: value?.toString() || ""
}
}
else if (typeof value === 'object') {
rest[field] = { rest[field] = {
$type: 'json', $type: 'json',
$value: value $value: value

View File

@@ -24,6 +24,9 @@ const estimateFee = async (
const { signedTransaction } = sign(copyTx, keypair) const { signedTransaction } = sign(copyTx, keypair)
const res = await xrplSend({ command: 'fee', tx_blob: signedTransaction }) const res = await xrplSend({ command: 'fee', tx_blob: signedTransaction })
if (res.error) {
throw new Error(`[${res.error}] ${res.error_exception}.`);
}
if (res && res.drops) { if (res && res.drops) {
return res.drops return res.drops
} }
@@ -31,7 +34,8 @@ const estimateFee = async (
} catch (err) { } catch (err) {
if (!opts.silent) { if (!opts.silent) {
console.error(err) console.error(err)
toast.error('Cannot estimate fee.') // ? Some better msg const msg = err instanceof Error ? err.message : 'Error estimating fee!';
toast.error(msg);
} }
return null return null
} }

View File

@@ -19,3 +19,17 @@ export const getFileExtention = (filename?: string): string | undefined => {
const ext = (filename.includes('.') && filename.split('.').pop()) || undefined const ext = (filename.includes('.') && filename.split('.').pop()) || undefined
return ext return ext
} }
type Type = "array" | "undefined" | "object" | "string" | "number" | "bigint" | "boolean" | "symbol" | "function"
type obj = Record<string | number | symbol, unknown>
type arr = unknown[]
export const typeIs = <T extends Type>(arg: any, t: T | T[]): arg is unknown & (T extends "array" ? arr : T extends "undefined" ? undefined | null : T extends "object" ? obj : T extends "string" ? string : T extends "number" ? number : T extends "bigint" ? bigint : T extends "boolean" ? boolean : T extends "symbol" ? symbol : T extends "function" ? Function : never) => {
const types = Array.isArray(t) ? t : [t]
return types.includes(typeOf(arg) as T);
}
export const typeOf = (arg: any): Type => {
const type = arg instanceof Array ? 'array' : arg === null ? 'undefined' : typeof arg
return type;
}

View File

@@ -1,9 +1,8 @@
import { typeIs, typeOf } from './helpers'
export const extractSchemaProps = <O extends object>(obj: O) => export const extractSchemaProps = <O extends object>(obj: O) =>
Object.entries(obj).reduce((prev, [key, val]) => { Object.entries(obj).reduce((prev, [key, val]) => {
const typeOf = <T>(arg: T) => const value = typeIs(val, "object") && '$type' in val && '$value' in val ? val?.$value : val
arg instanceof Array ? 'array' : arg === null ? 'undefined' : typeof arg
const value = typeOf(val) === 'object' && '$type' in val && '$value' in val ? val?.$value : val
const type = typeOf(value) const type = typeOf(value)
let schema: any = { let schema: any = {
@@ -12,19 +11,19 @@ export const extractSchemaProps = <O extends object>(obj: O) =>
default: value default: value
} }
if (typeOf(value) === 'array') { if (typeIs(value, "array")) {
const item = value[0] // TODO merge other item schema's into one const item = value[0] // TODO merge other item schema's into one
if (typeOf(item) !== 'object') { if (typeIs(item, "object")) {
schema.items = { schema.items = {
type: 'object', type: 'object',
properties: extractSchemaProps(item), properties: extractSchemaProps(item),
default: item default: item
} }
} }
// TODO support primitive-value arrays // TODO primitive-value arrays
} }
if (typeOf(value) === 'object') { if (typeIs(value, "object")) {
schema.properties = extractSchemaProps(value) schema.properties = extractSchemaProps(value)
} }
return { return {