diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index e19f7052f..cd422b2c7 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -137,7 +137,7 @@ "close_request": "close channel request", "renew_channel": "renew chanel", "payment_channel_closed": "payment channel closed", - "paychannel_node_line1": "It <1><0>{{action}} a PayChannel node from <3><0>{{account}} to <5><0>{{counterAccount}}", + "paychannel_node_line1": "It <1><0>{{action}} a PayChannel <3><0>{{node}} from <5><0>{{account}} to <7><0>{{counterAccount}}", "paychannel_amount_changed": "Amount changed by <1><0>{{difference}}<1><0>{{currency}} from <3><0>{{previous}}<1><0>{{currency}} to <5><0>{{final}}<1><0>{{currency}}", "paychannel_balance_changed": "Balance changed by <1><0>{{difference}}<1><0>{{currency}} from <3><0>{{previous}}<1><0>{{currency}} to <5><0>{{final}}<1><0>{{currency}}", "setfee_fees_description": "Future transactions will require a minimum fee of .", @@ -273,20 +273,20 @@ "meta": "Meta", "number_of_affected_node": "It affected {{count}} nodes in the ledger:", "nodes_type": "{{action}} nodes", - "node_meta_type": "It {{action}} a node with type", - "transaction_balance_line_one": "It <1><0>{{action}} a <4><0>{{currency}} RippleState node between <6><0>{{account}} and <8><0>{{counterAccount}}", + "node_meta_type": "It {{action}} a node with type", + "transaction_balance_line_one": "It {{action}} a RippleState node between and ", "transaction_balance_line_two": "Balance changed by <1><0>{{change}} from <3><0>{{previousBalance}} to <5><0>{{finalBalance}}", "transaction_outstanding_balance_line_two": "Outstanding balance changed by <1><0>{{change}} from <3><0>{{previousBalance}} to <5><0>{{finalBalance}}", - "transaction_owned_directory": "It {{action}} a DirectoryNode node owned by", - "transaction_unowned_directory": "It {{action}} a DirectoryNode node", + "transaction_owned_directory": "It {{action}} a DirectoryNode node owned by", + "transaction_unowned_directory": "It {{action}} a DirectoryNode node", "transaction_mptoken_line_one": "It <1><0>{{action}} an MPToken node of <3><0>{{account}}", - "transaction_mpt_issuance_line_one": "It <1><0>{{action}} an MPTokenIssuance node of <3><0>{{account}}", + "transaction_mpt_issuance_line_one": "It <1><0>{{action}} an MPTokenIssuance <3><0>node of <5><0>{{account}}", "owned_account_root": "It {{action}} the AccountRoot node of", "unowned_account_root": "It {{action}} the AccountRoot node", "account_balance_increased": "Balance increased by <1><0>{{difference}}<1><0>{{currency}} from <3><0>{{previous}}<1><0>{{currency}} to <5><0>{{final}}<1><0>{{currency}}", "account_balance_decreased": "Balance decreased by <1><0>{{difference}}<1><0>{{currency}} from <3><0>{{previous}}<1><0>{{currency}} to <5><0>{{final}}<1><0>{{currency}}", "decreased_from_to": "decreased by <1><0>{{change}} from <3><0>{{previous}} to <5><0>{{final}}", - "offer_node_meta": "It {{action}} a / offer node owned by with sequence # {{sequence}}", + "offer_node_meta": "It {{action}} a / offer node owned by with sequence # {{sequence}}", "offer_replaces": "This offer replaces the existing offer #", "offer_partially_filled": "The offer was partially filled.", "offer_filled": "The offer was filled.", @@ -802,5 +802,15 @@ "loan_fees_detail": "", "loan_terms_detail": "", "no_limit": "No Limit", - "first_loss_capital": "first-loss capital" + "first_loss_capital": "first-loss capital", + "prev_ledger_index": "Previous Ledger Index", + "prev_tx_id": "Previous Modifying Transaction", + "entry_not_found": "Entry not found", + "entry_empty_title": "No entry hash supplied", + "entry_empty_hint": "Enter a entry hash in the search box", + "check_entry_id": "Please check your entry ID.", + "entry_short": "Entry", + "entry": "Entry", + "invalid_entry_id": "The entry ID is invalid", + "id": "ID" } diff --git a/src/containers/Accounts/AMM/AMMAccounts/index.tsx b/src/containers/Accounts/AMM/AMMAccounts/index.tsx index 5ba391f00..33b20026a 100644 --- a/src/containers/Accounts/AMM/AMMAccounts/index.tsx +++ b/src/containers/Accounts/AMM/AMMAccounts/index.tsx @@ -64,7 +64,7 @@ export const AMMAccounts = () => { */ return getAccountInfo(rippledSocket, accountId) .then((accountInfo) => - getLedgerEntry(rippledSocket, { index: accountInfo.AMMID }) + getLedgerEntry(rippledSocket, accountInfo.AMMID) .then((ammLedgerEntry) => { asset1 = formatAsset(ammLedgerEntry.node.Asset) asset2 = formatAsset(ammLedgerEntry.node.Asset2) diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index 72ffcbbae..1794f8d50 100644 --- a/src/containers/App/index.tsx +++ b/src/containers/App/index.tsx @@ -24,6 +24,7 @@ import { AMENDMENTS_ROUTE, AMENDMENT_ROUTE, MPT_ROUTE, + ENTRY_ROUTE, NODES_ROUTE, VALIDATORS_ROUTE, UPGRADE_STATUS_ROUTE, @@ -39,6 +40,7 @@ import { NFT } from '../NFT/NFT' import { legacyRedirect } from './legacyRedirects' import { useCustomNetworks } from '../shared/hooks' import { Amendments } from '../Amendments' +import { Entry } from '../Entry' import { Amendment } from '../Amendment' import { MPT } from '../Token/MPT' import { Nodes } from '../Network/Nodes' @@ -71,6 +73,7 @@ export const AppWrapper = () => { [LEDGERS_ROUTE, Ledgers], [LEDGER_ROUTE, Ledger], [ACCOUNT_ROUTE, AccountsRouter], + [ENTRY_ROUTE, Entry], [TRANSACTION_ROUTE, Transaction], [NODES_ROUTE, Nodes], [VALIDATORS_ROUTE, Validators], diff --git a/src/containers/App/routes.ts b/src/containers/App/routes.ts index cea34f748..abb70b9a6 100644 --- a/src/containers/App/routes.ts +++ b/src/containers/App/routes.ts @@ -5,7 +5,7 @@ export const ACCOUNT_ROUTE: RouteDefinition<{ tab?: 'assets' | 'transactions' assetType?: 'issued' | 'nfts' | 'mpts' }> = { - path: '/accounts/:id?/:tab?/:assetType?', + path: '/accounts/:id/:tab?/:assetType?', } export const LEDGERS_ROUTE: RouteDefinition = { @@ -49,7 +49,14 @@ export const TRANSACTION_ROUTE: RouteDefinition<{ identifier: string tab?: 'simple' | 'detailed' | 'raw' }> = { - path: `/transactions/:identifier?/:tab?`, + path: `/transactions/:identifier/:tab?`, +} + +export const ENTRY_ROUTE: RouteDefinition<{ + id: string + tab?: 'simple' | 'raw' +}> = { + path: `/entry/:id/:tab?`, } export const VALIDATOR_ROUTE: RouteDefinition<{ diff --git a/src/containers/Entry/Simple/DefaultSimple.tsx b/src/containers/Entry/Simple/DefaultSimple.tsx new file mode 100644 index 000000000..990e813a6 --- /dev/null +++ b/src/containers/Entry/Simple/DefaultSimple.tsx @@ -0,0 +1,195 @@ +import { isValidClassicAddress } from 'ripple-address-codec' +import { formatAmount } from '../../../rippled/lib/txSummary/formatAmount' +import { Account } from '../../shared/components/Account' +import { Amount } from '../../shared/components/Amount' +import Currency from '../../shared/components/Currency' +import { SimpleGroup } from '../../shared/components/Transaction/SimpleGroup' +import { SimpleRow } from '../../shared/components/Transaction/SimpleRow' + +const DEFAULT_ENTRY_ELEMENTS = [ + 'Account', + 'Owner', + 'index', + 'LedgerEntryType', + 'PreviousTxnID', + 'PreviousTxnLgrSeq', + 'Flags', + 'LedgerIndex', + 'OwnerNode', +] + +const displayKey = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1 $2') + +const isCurrency = (value: any) => + typeof value === 'object' && + Object.keys(value).length <= 2 && + (value.issuer == null || typeof value.issuer === 'string') && + typeof value.currency === 'string' + +const isAmount = (amount: any, key: any = null) => + key === 'Amount' || + (typeof amount === 'object' && + Object.keys(amount).length === 3 && + typeof amount.issuer === 'string' && + typeof amount.currency === 'string' && + typeof amount.value === 'string') + +const processValue = (value: any) => { + if (typeof value === 'string') { + if (isValidClassicAddress(value)) { + return + } + if (value.length > 300) { + return `${value.substring(0, 300)}...` + } + if (value === '') { + return {''} + } + if (typeof value === 'object') { + return JSON.stringify(value) + } + return value + } + + if (Array.isArray(value)) { + return value.map((childValue) => { + if ( + typeof childValue === 'object' && + Object.keys(childValue).length === 1 + ) { + const childKey = Object.keys(childValue)[0] + const processed = processValue(childValue[childKey]) + return
{processed}
+ } + const processed = processValue(childValue) + return
{processed}
+ }) + } + + if (typeof value === 'object') { + return ( +
+ {Object.entries(value).map(([childKey, childValue]) => ( +
+ {`${childKey}: `} + {processValue(childValue)} +
+ ))} +
+ ) + } + + return JSON.stringify(value) +} + +const getRowNested = (key: any, value: any, uniqueKey: string = '') => { + if (key === 'Amount') { + return ( + + + + ) + } + + if (isCurrency(value)) { + return ( + + + + ) + } + + if (isAmount(value, key)) { + return ( + + + + ) + } + return ( + + {processValue(value)} + + ) +} + +const getRow = (key: any, value: any) => { + if (Array.isArray(value)) { + return ( +
+ {value.map((innerValue, index) => { + if ( + typeof innerValue === 'object' && + Object.keys(innerValue).length === 1 + ) { + const innerKey = Object.keys(innerValue)[0] + return ( + + {Object.entries(innerValue[innerKey]).map( + ([childKey, childValue], index2) => + getRowNested(childKey, childValue, index2.toString()), + )} + + ) + } + return getRowNested(index.toString(), innerValue, index.toString()) + })} +
+ ) + } + + if ( + typeof value === 'object' && + !isCurrency(value) && + !isAmount(value, key) + ) { + return ( + + {Object.entries(value).map(([childKey, childValue], index) => + getRowNested(childKey, childValue, index.toString()), + )} + + ) + } + + return getRowNested(key, value) +} + +export interface EntrySimpleProps { + data: { + node: I + } +} + +export const DefaultSimple = ({ data }: EntrySimpleProps) => { + const uniqueData = Object.fromEntries( + Object.entries(data.node).filter( + ([key, value]) => !DEFAULT_ENTRY_ELEMENTS.includes(key) && value != null, + ), + ) + + return ( + <>{Object.entries(uniqueData).map(([key, value]) => getRow(key, value))} + ) +} diff --git a/src/containers/Entry/Simple/index.tsx b/src/containers/Entry/Simple/index.tsx new file mode 100644 index 000000000..ed5e56d64 --- /dev/null +++ b/src/containers/Entry/Simple/index.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' +import { ErrorBoundary } from 'react-error-boundary' +import { useTranslation } from 'react-i18next' +import { transactionTypes } from '../../shared/components/Transaction' +import { DefaultSimple } from './DefaultSimple' + +export const Simple: FC<{ + data: any + type: string +}> = ({ data, type }) => { + // Locate the component for the left side of the simple tab that is unique per TransactionType. + const { t } = useTranslation() + const SimpleComponent = transactionTypes[type]?.Simple + if (SimpleComponent) { + return ( + +
{t('component_error')}
+
{t('try_detailed_raw')}
+ + } + > + +
+ ) + } + return +} diff --git a/src/containers/Entry/SimpleTab.tsx b/src/containers/Entry/SimpleTab.tsx new file mode 100644 index 000000000..da72fc71d --- /dev/null +++ b/src/containers/Entry/SimpleTab.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { Account } from '../shared/components/Account' +import { Simple } from './Simple' + +import { RouteLink } from '../shared/routing' +import { SimpleRow } from '../shared/components/Transaction/SimpleRow' +import '../shared/css/simpleTab.scss' +import './simpleTab.scss' +import { LEDGER_ROUTE, TRANSACTION_ROUTE } from '../App/routes' +import { BREAKPOINTS } from '../shared/utils' + +export const SimpleTab: FC<{ data: any; width: number }> = ({ + data, + width, +}) => { + const { t } = useTranslation() + + const renderRowIndex = (owner, prevLedgerIndex, prevTx) => ( + <> + {owner && ( + + + + )} + + + {prevLedgerIndex} + + + + + {prevTx} + + + + ) + + const rowIndex = renderRowIndex( + data?.node?.Owner ?? data?.node?.Account, + data?.node?.PreviousTxnLgrSeq, + data?.node?.PreviousTxnID, + ) + + return ( +
+
+ + {width < BREAKPOINTS.landscape && rowIndex} +
+ {width >= BREAKPOINTS.landscape && ( +
{rowIndex}
+ )} +
+
+ ) +} diff --git a/src/containers/Entry/entry.scss b/src/containers/Entry/entry.scss new file mode 100644 index 000000000..99b8dab2a --- /dev/null +++ b/src/containers/Entry/entry.scss @@ -0,0 +1,60 @@ +@use '../shared/css/variables' as *; + +.entry { + position: relative; + max-width: 912px; + min-height: 600px; + + @include for-size(tablet-landscape-up) { + width: 584px; + margin: 0 auto; + } + + @include for-size(desktop-up) { + width: 820px; + } + + @include for-size(big-desktop-up) { + width: 912px; + } + + .loader { + position: absolute; + z-index: 1; + top: 0px; + } + + .summary { + padding: 0 16px 87px 0; + + .type { + display: inline-block; + margin-bottom: 24px; + color: $white; + font-size: 42px; + @include bold; + } + + .id { + .title { + @include semibold; + } + + display: flex; + overflow: hidden; + margin-top: 8px; + color: $black-40; + font-size: 12px; + letter-spacing: 0px; + line-height: 20px; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + @include medium; + } + + .tx-status { + margin-left: 15px; + } + } +} diff --git a/src/containers/Entry/index.tsx b/src/containers/Entry/index.tsx new file mode 100644 index 000000000..c0cce6ef2 --- /dev/null +++ b/src/containers/Entry/index.tsx @@ -0,0 +1,158 @@ +import { useContext, useEffect } from 'react' +import { Helmet } from 'react-helmet-async' +import { useTranslation } from 'react-i18next' +import { useQuery } from 'react-query' +import { useWindowSize } from 'usehooks-ts' +import { useNavigate } from 'react-router' +import NoMatch from '../NoMatch' +import { Loader } from '../shared/components/Loader' +import { Tabs } from '../shared/components/Tabs' +import { NOT_FOUND, BAD_REQUEST, HASH256_REGEX } from '../shared/utils' +import { SimpleTab } from './SimpleTab' +import './entry.scss' +import { useAnalytics } from '../shared/analytics' +import SocketContext from '../shared/SocketContext' +import { buildPath, useRouteParams } from '../shared/routing' +import { ACCOUNT_ROUTE, ENTRY_ROUTE, MPT_ROUTE } from '../App/routes' +import { JsonView } from '../shared/components/JsonView' +import { getLedgerEntry } from '../../rippled/lib/rippled' + +const ERROR_MESSAGES: Record = {} +ERROR_MESSAGES[NOT_FOUND] = { + title: 'entry_not_found', + hints: ['check_entry_id'], +} +ERROR_MESSAGES[BAD_REQUEST] = { + title: 'invalid_entry_id', + hints: ['check_entry_id'], +} +ERROR_MESSAGES.default = { + title: 'generic_error', + hints: ['not_your_fault'], +} + +const getErrorMessage = (error) => + ERROR_MESSAGES[error] || ERROR_MESSAGES.default + +export const Entry = () => { + const { id = '', tab = 'simple' } = useRouteParams(ENTRY_ROUTE) + const { t } = useTranslation() + const rippledSocket = useContext(SocketContext) + const { trackException, trackScreenLoaded } = useAnalytics() + const navigate = useNavigate() + + const { isLoading, data, error, isError } = useQuery(['entry', id], () => { + if (id === '') { + return undefined + } + if (HASH256_REGEX.test(id)) { + return getLedgerEntry(rippledSocket, id, true).catch( + (ledgerEntryRequestError) => { + const status = ledgerEntryRequestError.code + trackException( + `entry ${id} --- ${JSON.stringify( + ledgerEntryRequestError.message, + )}`, + ) + + return Promise.reject(status) + }, + ) + } + + return Promise.reject(BAD_REQUEST) + }) + const { width } = useWindowSize() + + useEffect(() => { + trackScreenLoaded() + + return () => { + window.scrollTo(0, 0) + } + }, [tab, trackScreenLoaded]) + + useEffect(() => { + if (id != null && data?.index != null) { + if (data.node.LedgerEntryType === 'AccountRoot') { + const path = buildPath(ACCOUNT_ROUTE, { id: data.node.Account }) + navigate(path) + } else if (data.node.LedgerEntryType === 'MPTokenIssuance') { + const path = buildPath(MPT_ROUTE, { id: data.node.mpt_issuance_id }) + navigate(path) + } else if (data.node.LedgerEntryType === 'AMM') { + const path = buildPath(ACCOUNT_ROUTE, { id: data.node.Account }) + navigate(path) + } + } + }, [id, data, navigate]) + + function renderSummary() { + const type = data?.node.LedgerEntryType + return ( +
+
{type}
+ {data?.deleted_ledger_index && 'DELETED'} +
+
+ {t('id')} + {': '}{' '} +
+ {data?.index} +
+
+ ) + } + + function renderTabs() { + const tabs = ['simple', 'raw'] + const mainPath = buildPath(ENTRY_ROUTE, { id }) + return + } + + function renderEntry() { + if (!data) return undefined + + let body + + switch (tab) { + case 'raw': + body = + break + default: + body = + break + } + return ( + <> + {renderSummary()} + {renderTabs()} +
{body}
+ + ) + } + + let body + + if (isError) { + const message = getErrorMessage(error) + body = + } else if (data?.index != null) { + body = renderEntry() + } else if (!id) { + body = ( + + ) + } + return ( +
+ + {isLoading && } + {body} +
+ ) +} diff --git a/src/containers/Entry/simpleTab.scss b/src/containers/Entry/simpleTab.scss new file mode 100644 index 000000000..e29bc8c0a --- /dev/null +++ b/src/containers/Entry/simpleTab.scss @@ -0,0 +1,163 @@ +@use '../shared/css/variables' as *; + +$subdued-color: $black-40; + +.simple-body-tx { + .rows { + padding-top: 0px; + + .partial-row { + display: flex; + overflow: hidden; + flex-grow: 1; + flex-wrap: wrap; + padding-top: 20px; + padding-left: 5px; + color: $red-dark; + float: right; + font-size: 14px; + font-style: italic; + text-align: right; + vertical-align: middle; + @include regular; + } + + .row { + .value { + text-align: right; + + .grant { + .account { + padding-bottom: 8px; + font-size: 11px; + text-align: right; + @include medium; + } + } + + .amount { + text-align: right; + + .amount-localized { + display: block; + } + + .one-line { + display: flex; + } + + .currency { + display: block; + } + + .currency, + .one-line { + margin: -1px 0px -16px; + color: $subdued-color; + font-size: 11px; + text-align: right; + @include medium; + + .account { + font-size: inherit; + line-height: inherit; + vertical-align: bottom; + } + } + + &.list { + margin-bottom: 12px; + + .one-line { + justify-content: flex-end; + margin: -1px 0px 0px; + } + } + } + + ul { + padding: 0; + margin: 0; + list-style: none; + } + + &.partial, + &.closed, + &.unset { + color: $red-dark; + font-size: 14px; + font-style: italic; + @include regular; + } + + &.flag { + color: $blue-purple-30; + font-size: 14px; + font-style: italic; + @include regular; + } + + &.condition, + &.fulfillment, + &.tx, + &.channel { + width: calc(100% - 120px); + color: $subdued-color; + font-size: 14px; + white-space: normal; + word-break: break-all; + @include regular; + } + } + + &:first-child { + padding-top: 40px; + } + } + + .error { + padding: 50px; + color: $orange-40; + font-size: 14px; + text-align: center; + + .type { + margin: 0 5px; + @include bold; + } + } + + .group { + margin: 16px 0 16px -16px; + background: rgba($black-80, 0.7); + gap: 15px; + + &:first-child { + margin-top: 0px; + } + + &:last-child { + margin-bottom: 0px; + } + + .group-title { + display: block; + flex-direction: row; + padding: 16px 0 0 16px; + color: $black-40; + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + } + + .row { + padding: 16px 28px 12px 16px; + + &:last-child { + padding-bottom: 16px; + border: none; + } + } + } + } +} diff --git a/src/containers/Header/Search.tsx b/src/containers/Header/Search.tsx index 7a94883d1..a00b9a1e6 100644 --- a/src/containers/Header/Search.tsx +++ b/src/containers/Header/Search.tsx @@ -19,10 +19,11 @@ import { HASH192_REGEX, } from '../shared/utils' import './search.scss' -import { getTransaction } from '../../rippled/lib/rippled' +import { getLedgerEntry, getTransaction } from '../../rippled/lib/rippled' import { buildPath } from '../shared/routing' import { ACCOUNT_ROUTE, + ENTRY_ROUTE, LEDGER_ROUTE, NFT_ROUTE, TOKEN_ROUTE, @@ -40,7 +41,13 @@ const determineHashType = async ( await getTransaction(rippledContext, id) return 'transactions' } catch (e) { - return 'nft' + try { + await getLedgerEntry(rippledContext, id) + return 'entry' + } catch (e2) { + // TODO: better error message here + return 'nft' + } } } @@ -72,6 +79,8 @@ const getRoute = async ( path = buildPath(TRANSACTION_ROUTE, { identifier: id.toUpperCase() }) } else if (type === 'nft') { path = buildPath(NFT_ROUTE, { id: id.toUpperCase() }) + } else if (type === 'entry') { + path = buildPath(ENTRY_ROUTE, { id: id.toUpperCase() }) } return { diff --git a/src/containers/NFT/NFTTabs/Offers.tsx b/src/containers/NFT/NFTTabs/Offers.tsx index 4a55cfedf..698ba5879 100644 --- a/src/containers/NFT/NFTTabs/Offers.tsx +++ b/src/containers/NFT/NFTTabs/Offers.tsx @@ -10,7 +10,7 @@ import { Amount } from '../../shared/components/Amount' import '../../shared/components/TransactionTable/styles.scss' // Reuse load-more-btn import { formatAmount } from '../../../rippled/lib/txSummary/formatAmount' import { LoadMoreButton } from '../../shared/LoadMoreButton' -import { ACCOUNT_ROUTE } from '../../App/routes' +import { ACCOUNT_ROUTE, ENTRY_ROUTE } from '../../App/routes' import { RouteLink } from '../../shared/routing' interface Props { @@ -55,7 +55,9 @@ export const Offers = (props: Props) => { return ( - {offerIndex} + + {offerIndex} + diff --git a/src/containers/Transactions/DetailTab/Meta/DirectoryNode.tsx b/src/containers/Transactions/DetailTab/Meta/DirectoryNode.tsx index 650384a76..0d594d689 100644 --- a/src/containers/Transactions/DetailTab/Meta/DirectoryNode.tsx +++ b/src/containers/Transactions/DetailTab/Meta/DirectoryNode.tsx @@ -1,18 +1,28 @@ +import { Trans } from 'react-i18next' import { Account } from '../../../shared/components/Account' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' import type { DirectoryNodeRenderFunction } from './types' const render: DirectoryNodeRenderFunction = (t, action, node, index) => { const fields = node.FinalFields || node.NewFields return (
  • - {t( - fields.Owner - ? 'transaction_owned_directory' - : 'transaction_unowned_directory', - { - action, - }, - )} + + {/* The inner text will be replaced by the content of in translations.json */} + + ), + }} + /> {fields.Owner && ( {' '} diff --git a/src/containers/Transactions/DetailTab/Meta/MPToken.tsx b/src/containers/Transactions/DetailTab/Meta/MPToken.tsx index 30a8eb8bc..fbe90faa1 100644 --- a/src/containers/Transactions/DetailTab/Meta/MPToken.tsx +++ b/src/containers/Transactions/DetailTab/Meta/MPToken.tsx @@ -1,6 +1,8 @@ import { Trans } from 'react-i18next' import { Account } from '../../../shared/components/Account' import { computeMPTokenBalanceChange } from '../../../shared/utils' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' import type { MetaRenderFunction } from './types' const render: MetaRenderFunction = (_t, _language, action, node, index) => { @@ -12,7 +14,11 @@ const render: MetaRenderFunction = (_t, _language, action, node, index) => { const line1 = ( - It {action} an MPToken node of + It {action} an MPToken + + node + + of ) diff --git a/src/containers/Transactions/DetailTab/Meta/MPTokenIssuance.tsx b/src/containers/Transactions/DetailTab/Meta/MPTokenIssuance.tsx index 98bb1d9d0..c8c98576d 100644 --- a/src/containers/Transactions/DetailTab/Meta/MPTokenIssuance.tsx +++ b/src/containers/Transactions/DetailTab/Meta/MPTokenIssuance.tsx @@ -1,6 +1,8 @@ import { Trans } from 'react-i18next' import { Account } from '../../../shared/components/Account' import { computeMPTIssuanceBalanceChange } from '../../../shared/utils' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' import type { MetaRenderFunction } from './types' const render: MetaRenderFunction = (_t, _language, action, node, index) => { @@ -12,7 +14,11 @@ const render: MetaRenderFunction = (_t, _language, action, node, index) => { const line1 = ( - It {action} an MPTokenIssuance node of + It {action} an MPTokenIssuance + + node + + of ) diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index e0eb69e24..d569fd87a 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -7,6 +7,8 @@ import { } from '../../../shared/transactionUtils' import { localizeNumber } from '../../../shared/utils' import { Account } from '../../../shared/components/Account' +import { ENTRY_ROUTE } from '../../../App/routes' +import { RouteLink } from '../../../shared/routing' import Currency from '../../../shared/components/Currency' import type { MetaRenderFunctionWithTx, MetaNode } from './types' @@ -209,6 +211,11 @@ const render: MetaRenderFunctionWithTx = ( /> ), Account: , + Link: ( + + {/* Content from i18n */} + + ), }} />
      {lines}
    diff --git a/src/containers/Transactions/DetailTab/Meta/PayChannel.tsx b/src/containers/Transactions/DetailTab/Meta/PayChannel.tsx index 39f8ea5cb..8aaffd8f8 100644 --- a/src/containers/Transactions/DetailTab/Meta/PayChannel.tsx +++ b/src/containers/Transactions/DetailTab/Meta/PayChannel.tsx @@ -2,6 +2,8 @@ import { Trans } from 'react-i18next' import { CURRENCY_OPTIONS } from '../../../shared/transactionUtils' import { localizeNumber } from '../../../shared/utils' import { Account } from '../../../shared/components/Account' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' import type { MetaRenderFunction } from './types' const MILLION = 1000000 @@ -15,7 +17,11 @@ const render: MetaRenderFunction = (_t, language, action, node, index) => { const line1 = ( - It {action} a PayChannel node from + It {action} a PayChannel + + node + + from to diff --git a/src/containers/Transactions/DetailTab/Meta/RippleState.tsx b/src/containers/Transactions/DetailTab/Meta/RippleState.tsx index d6a1decb6..969c6fd1b 100644 --- a/src/containers/Transactions/DetailTab/Meta/RippleState.tsx +++ b/src/containers/Transactions/DetailTab/Meta/RippleState.tsx @@ -4,6 +4,8 @@ import { localizeNumber, computeRippleStateBalanceChange, } from '../../../shared/utils' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' import Currency from '../../../shared/components/Currency' import type { MetaRenderFunction } from './types' @@ -19,16 +21,20 @@ const render: MetaRenderFunction = (_t, language, action, node, index) => { } = computeRippleStateBalanceChange(node) const line1 = ( - - It {action} a{' '} - - - - RippleState node between - - and - - + , + Link: ( + + {/* Content from i18n */} + + ), + Account: , + CounterAccount: , + }} + /> ) const line2 = change ? ( diff --git a/src/containers/Transactions/DetailTab/Meta/index.tsx b/src/containers/Transactions/DetailTab/Meta/index.tsx index 39ebf7ed3..8f3dc2600 100644 --- a/src/containers/Transactions/DetailTab/Meta/index.tsx +++ b/src/containers/Transactions/DetailTab/Meta/index.tsx @@ -1,6 +1,5 @@ import { FC } from 'react' -import { useTranslation } from 'react-i18next' -import { TFunction } from 'i18next' +import { Trans, useTranslation } from 'react-i18next' import renderAccountRoot from './AccountRoot' import renderDirectoryNode from './DirectoryNode' import renderOffer from './Offer' @@ -10,15 +9,23 @@ import renderMPToken from './MPToken' import renderMPTokenIssuance from './MPTokenIssuance' import { groupAffectedNodes } from '../../../shared/transactionUtils' import { useLanguage } from '../../../shared/hooks' +import { RouteLink } from '../../../shared/routing' +import { ENTRY_ROUTE } from '../../../App/routes' -const renderDefault = ( - t: TFunction<'translations', undefined>, - action: string, - node: any, - index: number, -) => ( +const renderDefault = (action: string, node: any, index: number) => (
  • - {t('node_meta_type', { action })} {node.LedgerEntryType} + + {/* The inner text will be replaced by the content of in translations.json */} + + ), + }} + />{' '} + {node.LedgerEntryType}
  • ) @@ -44,7 +51,7 @@ export const TransactionMeta: FC<{ data: any }> = ({ data }) => { case 'MPToken': return renderMPToken(t, language, action, node, index) default: - return renderDefault(t, action, node, index) + return renderDefault(action, node, index) } }) diff --git a/src/containers/Transactions/index.tsx b/src/containers/Transactions/index.tsx index d9de459b1..5396dbc77 100644 --- a/src/containers/Transactions/index.tsx +++ b/src/containers/Transactions/index.tsx @@ -53,6 +53,7 @@ export const Transaction = () => { const { t } = useTranslation() const rippledSocket = useContext(SocketContext) const { trackException, trackScreenLoaded } = useAnalytics() + const { isLoading, data, error, isError } = useQuery( ['transaction', identifier], () => { diff --git a/src/containers/shared/components/Transaction/NFTokenAcceptOffer/Simple.tsx b/src/containers/shared/components/Transaction/NFTokenAcceptOffer/Simple.tsx index e2ca6d49a..aa823ad93 100644 --- a/src/containers/shared/components/Transaction/NFTokenAcceptOffer/Simple.tsx +++ b/src/containers/shared/components/Transaction/NFTokenAcceptOffer/Simple.tsx @@ -5,6 +5,8 @@ import { Amount } from '../../Amount' import { NFTokenAcceptOfferInstructions } from './types' import { TransactionSimpleComponent, TransactionSimpleProps } from '../types' import { NFTokenLink } from '../../NFTokenLink' +import { ENTRY_ROUTE } from '../../../../App/routes' +import { RouteLink } from '../../../routing' export const Simple: TransactionSimpleComponent = ({ data, @@ -20,7 +22,9 @@ export const Simple: TransactionSimpleComponent = ({ className="dt" data-testid="offer-id" > - {offer} + + {offer} + ))} {amount && seller && buyer && tokenID && ( diff --git a/src/containers/shared/components/Transaction/NFTokenCancelOffer/Simple.tsx b/src/containers/shared/components/Transaction/NFTokenCancelOffer/Simple.tsx index 6df38b784..c53488236 100644 --- a/src/containers/shared/components/Transaction/NFTokenCancelOffer/Simple.tsx +++ b/src/containers/shared/components/Transaction/NFTokenCancelOffer/Simple.tsx @@ -5,6 +5,8 @@ import { Amount } from '../../Amount' import { NFTokenCancelOfferInstructions } from './types' import { TransactionSimpleComponent, TransactionSimpleProps } from '../types' import { NFTokenLink } from '../../NFTokenLink' +import { ENTRY_ROUTE } from '../../../../App/routes' +import { RouteLink } from '../../../routing' export const Simple: TransactionSimpleComponent = ({ data, @@ -21,7 +23,9 @@ export const Simple: TransactionSimpleComponent = ({ className="dt" data-testid="offer-id" > - {offerID} + + {offerID} + {offerID && ( - {offerID} + + {offerID} + )} - {channel} + + {channel} + )} diff --git a/src/rippled/lib/rippled.ts b/src/rippled/lib/rippled.ts index c8881f3c3..feb2bfd80 100644 --- a/src/rippled/lib/rippled.ts +++ b/src/rippled/lib/rippled.ts @@ -92,64 +92,66 @@ const getLedger = async ( // get ledger_entry const getLedgerEntry = async ( rippledSocket: ExplorerXrplClient, - { index }: { index: string }, + index: string, + includeDeleted: boolean = false, ): Promise => { const request = { command: 'ledger_entry', index, ledger_index: 'validated', + include_deleted: includeDeleted, } const resp = await query(rippledSocket, request) - if (resp.error_message === 'entryNotFound') { + if (resp.error === 'entryNotFound') { throw new Error('ledger entry not found', 404) } - if (resp.error_message === 'invalidParams') { + if (resp.error === 'invalidParams') { throw new Error('invalidParams for ledger_entry', 404) } - if (resp.error_message === 'lgrNotFound') { + if (resp.error === 'lgrNotFound') { throw new Error('invalid ledger index/hash', 400) } - if (resp.error_message === 'malformedAddress') { + if (resp.error === 'malformedAddress') { throw new Error( 'The ledger_entry request improperly specified an Address field.', 404, ) } - if (resp.error_message === 'malformedCurrency') { + if (resp.error === 'malformedCurrency') { throw new Error( 'The ledger_entry request improperly specified a Currency Code field.', 404, ) } - if (resp.error_message === 'malformedOwner') { + if (resp.error === 'malformedOwner') { throw new Error( 'The ledger_entry request improperly specified the escrow.owner sub-field.', 404, ) } - if (resp.error_message === 'malformedRequest') { + if (resp.error === 'malformedRequest') { throw new Error( 'The ledger_entry request provided an invalid combination of fields, or provided the wrong type for one or more fields.', 404, ) } - if (resp.error_message === 'unknownOption') { + if (resp.error === 'unknownOption') { throw new Error( 'The fields provided in the ledger_entry request did not match any of the expected request formats.', 404, ) } - if (resp.error_message) { - throw new Error(resp.error_message, 500) + if (resp.error) { + throw new Error(resp.error, 500) } return resp