Skip to content

Commit

Permalink
feat: add CTID support (#839)
Browse files Browse the repository at this point in the history
Add CTID support to the search and URLs.

Fixes #707 

Co-authored-by: Caleb Kniffen <ckniffen@ripple.com>
  • Loading branch information
mvadari and ckniffen authored Nov 15, 2023
1 parent 032f709 commit 020cb60
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 150 deletions.
3 changes: 2 additions & 1 deletion public/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@
"transaction_empty_title": "No transaction hash supplied",
"transaction_empty_hint": "Enter a transaction hash in the search box",
"validator_not_found": "Validator not found",
"check_transaction_hash": "Please check your transaction hash",
"check_transaction_hash": "Please check your transaction hash or CTID.",
"wrong_network": "This CTID applies to a different network.",
"check_validator_key": "Please check your validator key",
"transaction": "Transaction",
"success": "Success",
Expand Down
1 change: 1 addition & 0 deletions public/locales/es-ES/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
"transaction_empty_hint": "Introduce un hash de transacción en la caja de búsqueda",
"validator_not_found": "Validador no encontrado",
"check_transaction_hash": "Por favor, comprueba tu hash de transacción",
"wrong_network": null,
"check_validator_key": "Por favor, comprueba la clave de tu validador",
"transaction": "Transacción",
"success": "Éxito",
Expand Down
1 change: 1 addition & 0 deletions public/locales/fr-FR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
"transaction_empty_hint": "Entrez un hash de transaction dans la zone de recherche",
"validator_not_found": "Validateur non trouvé",
"check_transaction_hash": "Veuillez vérifier le hash de la transaction",
"wrong_network": null,
"check_validator_key": "Veuillez vérifier la clé du validateur",
"transaction": "Transaction",
"success": "Succès",
Expand Down
1 change: 1 addition & 0 deletions public/locales/ja-JP/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
"transaction_empty_hint": "検索欄にトランザクションハッシュを入力してください",
"validator_not_found": "バリデータが見つかりません",
"check_transaction_hash": "トランザクションのハッシュ値を確認してください",
"wrong_network": null,
"check_validator_key": "バリデータのキーを確認してください",
"transaction": "トランザクション",
"success": "成功",
Expand Down
1 change: 1 addition & 0 deletions public/locales/ko-KR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
"transaction_empty_hint": "검색창에 트랜잭션 해시를 입력해 주세요",
"validator_not_found": "검증자를 찾을 수 없습니다",
"check_transaction_hash": "트랜잭션 해시를 확인해 주세요",
"wrong_network": null,
"check_validator_key": "검증자 키를 확인해주세요",
"transaction": "트랜잭션",
"success": "성공",
Expand Down
7 changes: 7 additions & 0 deletions src/containers/Header/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
FULL_CURRENCY_REGEX,
HASH_REGEX,
VALIDATORS_REGEX,
CTID_REGEX,
} from '../shared/utils'
import './search.scss'
import { isValidPayString } from '../../rippled/payString'
Expand Down Expand Up @@ -109,6 +110,12 @@ const getRoute = async (
path: buildPath(VALIDATOR_ROUTE, { identifier: normalizeAccount(id) }),
}
}
if (CTID_REGEX.test(id)) {
return {
type: 'transactions',
path: buildPath(TRANSACTION_ROUTE, { identifier: id.toUpperCase() }),
}
}

return null
}
Expand Down
144 changes: 49 additions & 95 deletions src/containers/Header/test/Search.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ describe('Search component', () => {
)
}

const oldEnvs = process.env

beforeEach(() => {
process.env = { ...oldEnvs, VITE_ENVIRONMENT: 'mainnet' }
})

afterEach(() => {
process.env = oldEnvs
})

it('renders without crashing', () => {
const wrapper = createWrapper()
wrapper.unmount()
Expand Down Expand Up @@ -53,6 +63,7 @@ describe('Search component', () => {

const hash =
'59239EA78084F6E2F288473F8AE02F3E6FC92F44BDE59668B5CAE361D3D32838'
const ctid = 'C0BF433500020000'
const token1 = 'cny.rJ1adrpGS3xsnQMb9Cw54tWJVFPuSdZHK'
const token1VariantPlus = 'cny.rJ1adrpGS3xsnQMb9Cw54tWJVFPuSdZHK'
const token1VariantMinus = 'cny-rJ1adrpGS3xsnQMb9Cw54tWJVFPuSdZHK'
Expand All @@ -74,140 +85,83 @@ describe('Search component', () => {
// mock getNFTInfo api to test transactions and nfts
const mockAPI = jest.spyOn(rippled, 'getTransaction')

const testValue = async (searchInput, expectedPath) => {
input.instance().value = searchInput
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(expectedPath)
}

input.simulate('keyDown', { key: 'a' })
expect(window.location.pathname).toEqual('/')

input.instance().value = ledgerIndex
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/ledgers/${ledgerIndex}`)
await testValue(ledgerIndex, `/ledgers/${ledgerIndex}`)

input.instance().value = rippleAddress
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleAddress}`)
await testValue(rippleAddress, `/accounts/${rippleAddress}`)

input.instance().value = addressWithQuotes
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleAddress}`)
await testValue(addressWithQuotes, `/accounts/${rippleAddress}`)

input.instance().value = addressWithSingleQuote
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleAddress}`)
await testValue(addressWithSingleQuote, `/accounts/${rippleAddress}`)

input.instance().value = addressWithSpace
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleAddress}`)
await testValue(addressWithSpace, `/accounts/${rippleAddress}`)

input.instance().value = rippleXAddress
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleXAddress}`)
await testValue(rippleXAddress, `/accounts/${rippleXAddress}`)

// Normalize split address format to a X-Address
input.instance().value = rippleSplitAddress
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/accounts/${rippleXAddress}`)
await testValue(rippleSplitAddress, `/accounts/${rippleXAddress}`)

input.instance().value = paystring
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/paystrings/${paystring}`)
await testValue(paystring, `/paystrings/${paystring}`)

// Normalize paystrings with @ to $
input.instance().value = paystringWithAt
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/paystrings/${paystring}`)
await testValue(paystringWithAt, `/paystrings/${paystring}`)

// Validator
input.instance().value = validator
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/validators/${validator}`)
await testValue(validator, `/validators/${validator}`)

mockAPI.mockImplementation(() => {
'123'
})
input.instance().value = hash
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/transactions/${hash}`)
await testValue(hash, `/transactions/${hash}`)

input.instance().value = token1
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token1}`)
await testValue(ctid, `/transactions/${ctid}`)

await testValue(token1, `/token/${token1}`)

// testing multiple variants of token format
input.instance().value = token1VariantColon
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token1}`)

input.instance().value = token1VariantPlus
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token1}`)

input.instance().value = token1VariantMinus
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token1}`)

input.instance().value = token2
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token2}`)
await testValue(token1VariantColon, `/token/${token1}`)

await testValue(token1VariantPlus, `/token/${token1}`)

await testValue(token1VariantMinus, `/token/${token1}`)

await testValue(token2, `/token/${token2}`)

// testing multiple variants of full token format
input.instance().value = token2VariantColon
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token2}`)
await testValue(token2VariantColon, `/token/${token2}`)

input.instance().value = token2VariantPlus
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token2}`)
await testValue(token2VariantPlus, `/token/${token2}`)

input.instance().value = token2VariantMinus
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/token/${token2}`)
await testValue(token2VariantMinus, `/token/${token2}`)

// Returns a response upon a valid nft_id, redirect to NFT
mockAPI.mockImplementation(() => {
throw new Error('Tx not found', 404)
})
input.instance().value = nftoken
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/nft/${nftoken}`)
await testValue(nftoken, `/nft/${nftoken}`)

input.instance().value = invalidString
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/search/${invalidString}`)
await testValue(invalidString, `/search/${invalidString}`)

// ensure strings are trimmed
mockAPI.mockImplementation(() => {
'123'
})
input.instance().value = ` ${hash} `
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/transactions/${hash}`)
await testValue(` ${hash} `, `/transactions/${hash}`)

// handle lower case tx hash
input.instance().value = hash.toLowerCase()
input.simulate('keyDown', { key: 'Enter' })
await flushPromises()
expect(window.location.pathname).toEqual(`/transactions/${hash}`)
await testValue(hash.toLowerCase(), `/transactions/${hash}`)

// handle lower case ctid
await testValue(ctid.toLowerCase(), `/transactions/${ctid}`)
wrapper.unmount()
})

Expand Down
45 changes: 29 additions & 16 deletions src/containers/Transactions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useWindowSize } from 'usehooks-ts'
import NoMatch from '../NoMatch'
import { Loader } from '../shared/components/Loader'
import { Tabs } from '../shared/components/Tabs'
import { NOT_FOUND, BAD_REQUEST, HASH_REGEX } from '../shared/utils'
import { NOT_FOUND, BAD_REQUEST, HASH_REGEX, CTID_REGEX } from '../shared/utils'
import { SimpleTab } from './SimpleTab'
import { DetailTab } from './DetailTab'
import './transaction.scss'
Expand All @@ -20,6 +20,8 @@ import { SUCCESSFUL_TRANSACTION } from '../shared/transactionUtils'
import { getTransaction } from '../../rippled'
import { TRANSACTION_ROUTE } from '../App/routes'

const WRONG_NETWORK = 406

const ERROR_MESSAGES: Record<string, { title: string; hints: string[] }> = {}
ERROR_MESSAGES[NOT_FOUND] = {
title: 'transaction_not_found',
Expand All @@ -29,6 +31,10 @@ ERROR_MESSAGES[BAD_REQUEST] = {
title: 'invalid_transaction_hash',
hints: ['check_transaction_hash'],
}
ERROR_MESSAGES[WRONG_NETWORK] = {
title: 'wrong_network',
hints: ['check_transaction_hash'],
}
ERROR_MESSAGES.default = {
title: 'generic_error',
hints: ['not_your_fault'],
Expand All @@ -48,22 +54,22 @@ export const Transaction = () => {
if (identifier === '') {
return undefined
}
if (!HASH_REGEX.test(identifier)) {
return Promise.reject(BAD_REQUEST)
if (HASH_REGEX.test(identifier) || CTID_REGEX.test(identifier)) {
return getTransaction(identifier, rippledSocket).catch(
(transactionRequestError) => {
const status = transactionRequestError.code
trackException(
`transaction ${identifier} --- ${JSON.stringify(
transactionRequestError.message,
)}`,
)

return Promise.reject(status)
},
)
}

return getTransaction(identifier, rippledSocket).catch(
(transactionRequestError) => {
const status = transactionRequestError.code
trackException(
`transaction ${identifier} --- ${JSON.stringify(
transactionRequestError.message,
)}`,
)

return Promise.reject(status)
},
)
return Promise.reject(BAD_REQUEST)
},
)
const { width } = useWindowSize()
Expand Down Expand Up @@ -93,9 +99,16 @@ export const Transaction = () => {
<div className="summary">
<div className="type">{type}</div>
<TxStatus status={data?.raw.meta.TransactionResult} />
<div className="hash" title={data?.raw.hash}>
<div className="txid" title={data?.raw.hash}>
<div className="title">{t('hash')}: </div>
{data?.raw.hash}
</div>
{data?.raw.tx.ctid && (
<div className="txid" title={data.raw.tx.ctid}>
<div className="title">CTID: </div>
{data.raw.tx.ctid}
</div>
)}
</div>
)
}
Expand Down
Loading

0 comments on commit 020cb60

Please sign in to comment.