From 0b8367b0f5d5ae084cb300292614eb200d6378a4 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 23 Mar 2026 15:29:51 -0700 Subject: [PATCH 01/27] draft: first draft of the MPT-DEX feature support in Explorer --- .../Transactions/DetailTab/Meta/Offer.tsx | 73 +++++-- .../Transaction/AMMClawback/parser.ts | 3 +- .../AMMCreate/test/AMMCreate.test.tsx | 30 +++ .../test/mock_data/amm_create_mpt.json | 101 ++++++++++ .../Transaction/AMMDelete/Description.tsx | 19 +- .../Transaction/AMMDelete/Simple.tsx | 17 +- .../Transaction/AMMDelete/TableDetail.tsx | 17 +- .../components/Transaction/DefaultSimple.tsx | 26 ++- .../Transaction/OfferCreate/Description.tsx | 41 ++-- .../Transaction/OfferCreate/Simple.tsx | 4 + .../Transaction/OfferCreate/TableDetail.tsx | 4 + .../Transaction/OfferCreate/parser.ts | 9 +- .../test/OfferCreateSimple.test.tsx | 50 +++++ .../test/mock_data/OfferCreateMPT.json | 64 +++++++ .../test/mock_data/OfferCreateMPTPayIOU.json | 72 +++++++ src/containers/shared/components/TxToken.tsx | 16 +- src/containers/shared/metaParser.tsx | 36 +++- src/containers/shared/test/metaParser.test.ts | 181 ++++++++++++++++++ src/containers/shared/transactionUtils.ts | 11 +- src/containers/shared/utils.js | 20 +- .../lib/txSummary/formatAmount.test.ts | 127 ++++++++++++ src/rippled/offers.ts | 4 +- 22 files changed, 856 insertions(+), 69 deletions(-) create mode 100644 src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_mpt.json create mode 100644 src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPT.json create mode 100644 src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPTPayIOU.json create mode 100644 src/containers/shared/test/metaParser.test.ts create mode 100644 src/rippled/lib/txSummary/formatAmount.test.ts diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index e0eb69e24..1894d2a3f 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -10,8 +10,28 @@ import { Account } from '../../../shared/components/Account' import Currency from '../../../shared/components/Currency' import type { MetaRenderFunctionWithTx, MetaNode } from './types' -const normalize = (value: number | string, currency: string): string => - currency === 'XRP' ? (Number(value) / XRP_BASE).toString() : String(value) +const getCurrency = (takerAmount: any): string => { + if (takerAmount?.mpt_issuance_id) return takerAmount.mpt_issuance_id + return takerAmount?.currency || 'XRP' +} + +const getIsMPT = (takerAmount: any): boolean => !!takerAmount?.mpt_issuance_id + +const getIssuer = (takerAmount: any): string | undefined => { + if (takerAmount?.mpt_issuance_id) return undefined + return takerAmount?.issuer +} + +const normalize = ( + value: number | string, + currency: string, + isMPT: boolean = false, +): string => { + if (isMPT) return String(value) + return currency === 'XRP' + ? (Number(value) / XRP_BASE).toString() + : String(value) +} const renderChanges = ( _t: any, @@ -22,14 +42,16 @@ const renderChanges = ( const meta: JSX.Element[] = [] const final = node.FinalFields const prev = node?.PreviousFields - const paysCurrency = final.TakerPays.currency || 'XRP' - const getsCurrency = final.TakerGets.currency || 'XRP' + const paysCurrency = getCurrency(final.TakerPays) + const getsCurrency = getCurrency(final.TakerGets) + const paysIsMPT = getIsMPT(final.TakerPays) + const getsIsMPT = getIsMPT(final.TakerGets) const finalPays = final.TakerPays.value || final.TakerPays const finalGets = final.TakerGets.value || final.TakerGets const prevPays = prev?.TakerPays?.value || prev?.TakerPays const prevGets = prev?.TakerGets?.value || prev?.TakerGets - const changePays = normalize(prevPays - finalPays, paysCurrency) - const changeGets = normalize(prevGets - finalGets, getsCurrency) + const changePays = normalize(prevPays - finalPays, paysCurrency, paysIsMPT) + const changeGets = normalize(prevGets - finalGets, getsCurrency, getsIsMPT) if (prevPays && finalPays) { const options = { ...CURRENCY_OPTIONS, currency: paysCurrency } @@ -39,8 +61,10 @@ const renderChanges = ( {' '} @@ -50,7 +74,7 @@ const renderChanges = ( {{ previous: localizeNumber( - normalize(prevPays, paysCurrency), + normalize(prevPays, paysCurrency, paysIsMPT), language, options, ), @@ -60,7 +84,7 @@ const renderChanges = ( {{ final: localizeNumber( - normalize(finalPays, paysCurrency), + normalize(finalPays, paysCurrency, paysIsMPT), language, options, ), @@ -78,8 +102,10 @@ const renderChanges = ( {' '} @@ -89,7 +115,7 @@ const renderChanges = ( {{ previous: localizeNumber( - normalize(prevGets, getsCurrency), + normalize(prevGets, getsCurrency, getsIsMPT), language, options, ), @@ -99,7 +125,7 @@ const renderChanges = ( {{ final: localizeNumber( - normalize(finalGets, getsCurrency), + normalize(finalGets, getsCurrency, getsIsMPT), language, options, ), @@ -123,11 +149,14 @@ const render: MetaRenderFunctionWithTx = ( ) => { const lines: JSX.Element[] = [] const fields = node.FinalFields || node.NewFields - const paysCurrency = fields.TakerPays.currency || 'XRP' - const getsCurrency = fields.TakerGets.currency || 'XRP' + const paysCurrency = getCurrency(fields.TakerPays) + const getsCurrency = getCurrency(fields.TakerGets) + const paysIsMPT = getIsMPT(fields.TakerPays) + const getsIsMPT = getIsMPT(fields.TakerGets) const takerPaysValue = normalize( fields.TakerPays.value || fields.TakerPays, paysCurrency, + paysIsMPT, ) const invert = CURRENCY_ORDER.indexOf(getsCurrency) > CURRENCY_ORDER.indexOf(paysCurrency) @@ -194,18 +223,26 @@ const render: MetaRenderFunctionWithTx = ( components={{ Currency: ( ), Currency2: ( ), Account: , diff --git a/src/containers/shared/components/Transaction/AMMClawback/parser.ts b/src/containers/shared/components/Transaction/AMMClawback/parser.ts index fcc245bb6..7824a7f99 100644 --- a/src/containers/shared/components/Transaction/AMMClawback/parser.ts +++ b/src/containers/shared/components/Transaction/AMMClawback/parser.ts @@ -6,8 +6,7 @@ export function parser(tx: AMMClawback, meta: any) { const holder = tx.Holder const amount = findAssetAmount(meta, tx.Asset, tx) if (tx.Flags) { - // @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged - const amount2 = findAssetAmount(meta, tx.Asset2, tx) + const amount2 = findAssetAmount(meta, tx.Asset2 as any, tx) return { amount, account, diff --git a/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx b/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx index f9a5fc102..790241f02 100644 --- a/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx +++ b/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx @@ -1,10 +1,27 @@ +import { useQuery } from 'react-query' import { Simple } from '../Simple' import { createSimpleRenderFactory, expectSimpleRowText } from '../../test' import createMock from './mock_data/amm_create.json' +import createMockMpt from './mock_data/amm_create_mpt.json' + +jest.mock('react-query', () => ({ + ...jest.requireActual('react-query'), + useQuery: jest.fn(), +})) describe('AMM Create Tests', () => { const renderComponent = createSimpleRenderFactory(Simple) + beforeEach(() => { + // @ts-ignore + useQuery.mockImplementation((args: any, fn: any, opts: any) => { + if (opts?.enabled === false || !opts?.enabled) { + return { data: undefined } + } + return { data: { assetScale: 0 } } + }) + }) + it('renders from transaction', () => { const { container, unmount } = renderComponent(createMock) expectSimpleRowText(container, 'asset1', '\uE90010,000.00 XRP') @@ -21,4 +38,17 @@ describe('AMM Create Tests', () => { ) unmount() }) + + it('renders AMMCreate with XRP + MPT pair', () => { + // @ts-ignore + useQuery.mockImplementation(() => ({ + data: { assetScale: 0 }, + })) + const { container, unmount } = renderComponent(createMockMpt) + expectSimpleRowText(container, 'asset1', '\uE90010,000.00 XRP') + // asset2 should contain MPT amount value + const asset2 = container.querySelector('[data-testid="asset2"] .value') + expect(asset2).toHaveTextContent('10,000') + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_mpt.json b/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_mpt.json new file mode 100644 index 000000000..1f707551b --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_mpt.json @@ -0,0 +1,101 @@ +{ + "tx": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "Amount": "10000000000", + "Amount2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "10000" + }, + "Fee": "10", + "Flags": 0, + "Sequence": 3, + "SigningPubKey": "02BFF00BCB2D25845C1D7C1FC5AAAE465B56CBB966BF034E1F6FFC097E8A6FBD28", + "TradingFee": 1, + "TransactionType": "AMMCreate", + "TxnSignature": "304402200CE5EA35FB7545CA8CD3231C2EE6F23D6B9EF7E146F23016905B9516721D5745022062DCC01BB7617C93F85DAE93917CC585FF09AE690BA78FDC5924565EEC186C70", + "date": "2022-11-21T20:58:11Z" + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "990000" + }, + "PreviousFields": { + "MPTAmount": "1000000" + }, + "PreviousTxnID": "98D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114138 + } + }, + { + "CreatedNode": { + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "471AC5276FBA4916D53017D7073D44C5F4780CC73954B1715DC8A65365E8ACAC", + "NewFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Balance": "10000000000", + "Flags": 59768832, + "OwnerCount": 1, + "Sequence": 1 + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "Balance": "989999999970", + "Flags": 8388608, + "OwnerCount": 2, + "Sequence": 4 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "83A4BB5EA0DB3CFEE89804BC08E1CBD222E869046EE01A7ECCAB57F8433F9655", + "PreviousFields": { + "Balance": "999999999980", + "OwnerCount": 1, + "Sequence": 3 + }, + "PreviousTxnID": "F7998834E30DEB92E62754744328C882CD1C50C59A7568884B14F498B2B91198", + "PreviousTxnLgrSeq": 114136 + } + }, + { + "CreatedNode": { + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "NewFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10000000" + } + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Flags": 0, + "Owner": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "RootIndex": "D69B2631EAE1115AFA8A12284B3780D743CB6CBA74A2F5D878CD68C0352C4CFA" + }, + "LedgerEntryType": "DirectoryNode", + "LedgerIndex": "D69B2631EAE1115AFA8A12284B3780D743CB6CBA74A2F5D878CD68C0352C4CFA" + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + } +} diff --git a/src/containers/shared/components/Transaction/AMMDelete/Description.tsx b/src/containers/shared/components/Transaction/AMMDelete/Description.tsx index 25466d354..a6d74725c 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/Description.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/Description.tsx @@ -2,22 +2,33 @@ import { Trans } from 'react-i18next' import { type AMMDelete } from 'xrpl' import { TransactionDescriptionProps } from '../types' import Currency from '../../Currency' +import { formatAsset } from '../../../../../rippled/lib/txSummary/formatAmount' export const Description = ({ data, }: TransactionDescriptionProps) => { const { Asset, Asset2 } = data.tx + const asset1 = formatAsset(Asset) + const asset2 = formatAsset(Asset2) return (
, + Asset: ( + + ), Asset2: ( - // @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged - + ), }} /> diff --git a/src/containers/shared/components/Transaction/AMMDelete/Simple.tsx b/src/containers/shared/components/Transaction/AMMDelete/Simple.tsx index 6faaede28..26b74e2ce 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/Simple.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/Simple.tsx @@ -4,20 +4,29 @@ import { type AMMDelete } from 'xrpl' import { SimpleRow } from '../SimpleRow' import { TransactionSimpleProps } from '../types' import Currency from '../../Currency' +import { formatAsset } from '../../../../../rippled/lib/txSummary/formatAmount' export const Simple = ({ data }: TransactionSimpleProps) => { const { t } = useTranslation() const { Asset, Asset2 } = data.instructions + const asset1 = formatAsset(Asset) + const asset2 = formatAsset(Asset2) return ( <> - {/* @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged */} - + - {/* @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged */} - + ) diff --git a/src/containers/shared/components/Transaction/AMMDelete/TableDetail.tsx b/src/containers/shared/components/Transaction/AMMDelete/TableDetail.tsx index fcebb71a0..379e13011 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/TableDetail.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/TableDetail.tsx @@ -2,24 +2,33 @@ import { useTranslation } from 'react-i18next' import { type AMMDelete } from 'xrpl' import { TransactionTableDetailProps } from '../types' import Currency from '../../Currency' +import { formatAsset } from '../../../../../rippled/lib/txSummary/formatAmount' export const TableDetail = ({ instructions, }: TransactionTableDetailProps) => { const { t } = useTranslation() const { Asset, Asset2 } = instructions + const asset1 = formatAsset(Asset) + const asset2 = formatAsset(Asset2) return (
{t('asset1')} - {/* @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged */} - +
{t('asset2')} - {/* @ts-expect-error - MPT is not being supported for AMM transactions until https://github.com/XRPLF/rippled/pull/5285 is merged */} - +
) diff --git a/src/containers/shared/components/Transaction/DefaultSimple.tsx b/src/containers/shared/components/Transaction/DefaultSimple.tsx index 481616688..002f0c361 100644 --- a/src/containers/shared/components/Transaction/DefaultSimple.tsx +++ b/src/containers/shared/components/Transaction/DefaultSimple.tsx @@ -29,14 +29,26 @@ const DEFAULT_TX_ELEMENTS = [ const displayKey = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1 $2') -const isCurrency = (value: any) => +const isMPTAsset = (value: any) => + typeof value === 'object' && + typeof value.mpt_issuance_id === 'string' && + !value.value + +const isMPTAmount = (value: any) => typeof value === 'object' && - Object.keys(value).length <= 2 && - (value.issuer == null || typeof value.issuer === 'string') && - typeof value.currency === 'string' + typeof value.mpt_issuance_id === 'string' && + typeof value.value === 'string' + +const isCurrency = (value: any) => + isMPTAsset(value) || + (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' || + isMPTAmount(amount) || (typeof amount === 'object' && Object.keys(amount).length === 3 && typeof amount.issuer === 'string' && @@ -111,7 +123,11 @@ const getRowNested = (key: any, value: any, uniqueKey: string = '') => { label={displayKey(key)} data-testid={key} > - + ) } diff --git a/src/containers/shared/components/Transaction/OfferCreate/Description.tsx b/src/containers/shared/components/Transaction/OfferCreate/Description.tsx index eb3b463ee..3c955cd80 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/Description.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/Description.tsx @@ -13,9 +13,16 @@ import { import { convertRippleDate } from '../../../../../rippled/lib/utils' import Currency from '../../Currency' import { Amount } from '../../Amount' -import { formatAmount } from '../../../../../rippled/lib/txSummary/formatAmount' +import { + formatAmount, + formatAsset, + isMPTAmount, +} from '../../../../../rippled/lib/txSummary/formatAmount' -const normalize = (amount: any) => amount.value || amount / XRP_BASE +const normalize = (amount: any) => { + if (isMPTAmount(amount)) return Number(amount.value) + return amount.value ? Number(amount.value) : amount / XRP_BASE +} const Description: TransactionDescriptionComponent = ( props: TransactionDescriptionProps, @@ -23,8 +30,10 @@ const Description: TransactionDescriptionComponent = ( const { t, i18n } = useTranslation() const language = i18n.resolvedLanguage const { data } = props - const paysCurrency = data.tx.TakerPays.currency || 'XRP' - const getsCurrency = data.tx.TakerGets.currency || 'XRP' + const paysAsset = formatAsset(data.tx.TakerPays) + const getsAsset = formatAsset(data.tx.TakerGets) + const paysCurrency = paysAsset.currency + const getsCurrency = getsAsset.currency const paysValue = normalize(data.tx.TakerPays) const getsValue = normalize(data.tx.TakerGets) const invert = @@ -38,15 +47,19 @@ const Description: TransactionDescriptionComponent = ( pair = ( / ) @@ -54,15 +67,19 @@ const Description: TransactionDescriptionComponent = ( pair = ( / ) diff --git a/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx b/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx index ad079a9d2..8ea005920 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx @@ -19,13 +19,17 @@ const Simple: TransactionSimpleComponent = (props: TransactionSimpleProps) => { /
diff --git a/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx b/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx index f084829c3..6a66c7227 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx @@ -18,13 +18,17 @@ export const TableDetail = (props: any) => { / diff --git a/src/containers/shared/components/Transaction/OfferCreate/parser.ts b/src/containers/shared/components/Transaction/OfferCreate/parser.ts index 742c58fd7..96b8900a8 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/parser.ts +++ b/src/containers/shared/components/Transaction/OfferCreate/parser.ts @@ -1,11 +1,14 @@ import { CURRENCY_ORDER } from '../../../transactionUtils' -import { formatAmount } from '../../../../../rippled/lib/txSummary/formatAmount' +import { + formatAmount, + formatAsset, +} from '../../../../../rippled/lib/txSummary/formatAmount' export function parser(tx: any) { const gets = formatAmount(tx.TakerGets) - const base = tx.TakerGets.currency ? tx.TakerGets : { currency: 'XRP' } - const counter = tx.TakerPays.currency ? tx.TakerPays : { currency: 'XRP' } const pays = formatAmount(tx.TakerPays) + const base = formatAsset(tx.TakerGets) + const counter = formatAsset(tx.TakerPays) const price = Number(pays.amount) / Number(gets.amount) const invert = CURRENCY_ORDER.indexOf(counter.currency) > diff --git a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx index e2980298e..a7a9c7f04 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx @@ -1,12 +1,32 @@ +import { useQuery } from 'react-query' import { Simple } from '../Simple' import mockOfferCreateWithCancel from './mock_data/OfferCreateWithExpirationAndCancel.json' import mockOfferCreate from './mock_data/OfferCreate.json' import mockOfferCreateWithPermissionedDomainID from './mock_data/OfferCreateWithPermissionedDomainID.json' +import mockOfferCreateMPT from './mock_data/OfferCreateMPT.json' +import mockOfferCreateMPTPayIOU from './mock_data/OfferCreateMPTPayIOU.json' import { createSimpleRenderFactory } from '../../test/createWrapperFactory' +jest.mock('react-query', () => ({ + ...jest.requireActual('react-query'), + useQuery: jest.fn(), +})) + const renderComponent = createSimpleRenderFactory(Simple) describe('OfferCreate: Simple', () => { + beforeEach(() => { + // @ts-ignore + useQuery.mockImplementation((args: any, fn: any, opts: any) => { + // Return empty data by default (non-MPT queries) + if (opts?.enabled === false || !opts?.enabled) { + return { data: undefined } + } + // For MPT queries, return assetScale 0 + return { data: { assetScale: 0 } } + }) + }) + it('renders with an expiration and offer', () => { const { container, unmount } = renderComponent(mockOfferCreateWithCancel) expect( @@ -49,4 +69,34 @@ describe('OfferCreate: Simple', () => { '4A4879496CFF23CA32242D50DA04DDB41F4561167276A62AF21899F83DF28812', ) }) + + it('renders OfferCreate with MPT TakerGets and XRP TakerPays', () => { + // @ts-ignore + useQuery.mockImplementation(() => ({ + data: { assetScale: 0 }, + })) + const { container } = renderComponent(mockOfferCreateMPT) + // Amount buy should show XRP value + expect( + container.querySelector('[data-testid="amount-buy"] .value'), + ).toHaveTextContent('XRP') + // Amount sell should show MPT value + expect( + container.querySelector('[data-testid="amount-sell"] .value'), + ).toHaveTextContent('1,000') + }) + + it('renders OfferCreate with MPT TakerPays and IOU TakerGets', () => { + // @ts-ignore + useQuery.mockImplementation(() => ({ + data: { assetScale: 0 }, + })) + const { container } = renderComponent(mockOfferCreateMPTPayIOU) + expect( + container.querySelector('[data-testid="amount-buy"] .value'), + ).toHaveTextContent('500') + expect( + container.querySelector('[data-testid="amount-sell"] .value'), + ).toHaveTextContent('100') + }) }) diff --git a/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPT.json b/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPT.json new file mode 100644 index 000000000..159fab244 --- /dev/null +++ b/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPT.json @@ -0,0 +1,64 @@ +{ + "tx": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "Fee": "5176", + "Flags": 0, + "LastLedgerSequence": 71724753, + "Sequence": 56768893, + "SigningPubKey": "03C48299E57F5AE7C2BE1391B581D313F1967EA2301628C07AC412092FDC15BA22", + "TakerGets": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "1000" + }, + "TakerPays": "50000000000", + "TransactionType": "OfferCreate", + "TxnSignature": "3044022069287DAA493E6C5754D32121408F42034F26BE8C8111EC27D8F9FBD9F01448A60220015FB59A284F388322835F9C4EC3C2C9ABCE763F9327C474D0BA668A8632E922", + "date": "2022-05-18T02:40:11Z" + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "Balance": "220102267009", + "Flags": 0, + "OwnerCount": 108, + "Sequence": 56768894 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "B1B9AAC12B56B1CFC93DDC8AF6958B50E89509F377ED4825A3D970F249892CE3", + "PreviousFields": { + "Balance": "220102272185", + "OwnerCount": 107, + "Sequence": 56768893 + }, + "PreviousTxnID": "41336A151E6F8F91852E03A04204F62BB186B486691B5AB27AB00A70C3B26B37", + "PreviousTxnLgrSeq": 71724751 + } + }, + { + "CreatedNode": { + "LedgerEntryType": "Offer", + "LedgerIndex": "E284EF740006031D49F6EDACA80AD8CC3A7507318ADAAE8781A846CF26351C6F", + "NewFields": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "BookDirectory": "33C81D720DBA84863A1510FD5C6C3E9224F0F5778261CF175D111AFA1F573DF6", + "OwnerNode": "80a6", + "Sequence": 56768893, + "TakerGets": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "1000" + }, + "TakerPays": "50000000000" + } + } + } + ], + "TransactionIndex": 16, + "TransactionResult": "tesSUCCESS" + }, + "hash": "DB244B742B5C4CDD11A53D0F487C616D18E8C0EC60FF38F059E167095506820A", + "ledger_index": 71724751, + "date": "2022-05-18T02:40:11Z" +} diff --git a/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPTPayIOU.json b/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPTPayIOU.json new file mode 100644 index 000000000..57ef45a19 --- /dev/null +++ b/src/containers/shared/components/Transaction/OfferCreate/test/mock_data/OfferCreateMPTPayIOU.json @@ -0,0 +1,72 @@ +{ + "tx": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "Fee": "5176", + "Flags": 0, + "LastLedgerSequence": 71724753, + "Sequence": 56768893, + "SigningPubKey": "03C48299E57F5AE7C2BE1391B581D313F1967EA2301628C07AC412092FDC15BA22", + "TakerGets": { + "currency": "USD", + "issuer": "rcyS4CeCZVYvTiKcxj6Sx32ibKwcDHLds", + "value": "100" + }, + "TakerPays": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "500" + }, + "TransactionType": "OfferCreate", + "TxnSignature": "3044022069287DAA493E6C5754D32121408F42034F26BE8C8111EC27D8F9FBD9F01448A60220015FB59A284F388322835F9C4EC3C2C9ABCE763F9327C474D0BA668A8632E922", + "date": "2022-05-18T02:40:11Z" + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "Balance": "220102267009", + "Flags": 0, + "OwnerCount": 108, + "Sequence": 56768894 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "B1B9AAC12B56B1CFC93DDC8AF6958B50E89509F377ED4825A3D970F249892CE3", + "PreviousFields": { + "Balance": "220102272185", + "OwnerCount": 107, + "Sequence": 56768893 + }, + "PreviousTxnID": "41336A151E6F8F91852E03A04204F62BB186B486691B5AB27AB00A70C3B26B37", + "PreviousTxnLgrSeq": 71724751 + } + }, + { + "CreatedNode": { + "LedgerEntryType": "Offer", + "LedgerIndex": "E284EF740006031D49F6EDACA80AD8CC3A7507318ADAAE8781A846CF26351C6F", + "NewFields": { + "Account": "r3rhWeE31Jt5sWmi4QiGLMZnY3ENgqw96W", + "BookDirectory": "33C81D720DBA84863A1510FD5C6C3E9224F0F5778261CF175D111AFA1F573DF6", + "OwnerNode": "80a6", + "Sequence": 56768893, + "TakerGets": { + "currency": "USD", + "issuer": "rcyS4CeCZVYvTiKcxj6Sx32ibKwcDHLds", + "value": "100" + }, + "TakerPays": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "500" + } + } + } + } + ], + "TransactionIndex": 16, + "TransactionResult": "tesSUCCESS" + }, + "hash": "DB244B742B5C4CDD11A53D0F487C616D18E8C0EC60FF38F059E167095506820A", + "ledger_index": 71724751, + "date": "2022-05-18T02:40:11Z" +} diff --git a/src/containers/shared/components/TxToken.tsx b/src/containers/shared/components/TxToken.tsx index d7127d774..c485ede31 100644 --- a/src/containers/shared/components/TxToken.tsx +++ b/src/containers/shared/components/TxToken.tsx @@ -11,8 +11,8 @@ interface Props { function getTokenPair( type: string, fee: number, - amount: { currency: string; amount: number }, - amount2: { currency: string; amount: number }, + amount: { currency: string; amount: number; isMPT?: boolean }, + amount2: { currency: string; amount: number; isMPT?: boolean }, ) { if ( type === 'AMMWithdraw' || @@ -22,11 +22,19 @@ function getTokenPair( ) { const first = amount?.amount && amount.amount !== fee ? ( - + ) : undefined const second = amount2?.amount && amount2.amount !== fee ? ( - + ) : undefined if (first && second) { diff --git a/src/containers/shared/metaParser.tsx b/src/containers/shared/metaParser.tsx index f071e29c3..09a6711bd 100644 --- a/src/containers/shared/metaParser.tsx +++ b/src/containers/shared/metaParser.tsx @@ -5,6 +5,7 @@ import { ExplorerAmount } from './types' export const LedgerEntryTypes = { AccountRoot: 'AccountRoot', RippledState: 'RippleState', + MPToken: 'MPToken', AMM: 'AMM', } // TODO: fix fee logic - filter out the fee only nodes - make sure fee isnt included in xrp deposits/withdraws @@ -59,9 +60,14 @@ changes. */ export function findAssetAmount( meta: any, - asset: { currency: string; issuer?: string }, + asset: { currency?: string; issuer?: string; mpt_issuance_id?: string }, tx: BaseTransaction, ): ExplorerAmount | undefined { + // Handle MPT assets + if (asset.mpt_issuance_id) { + return findMPTAmount(meta, asset.mpt_issuance_id) + } + if (asset.currency === 'XRP') return findXRPAmount(meta, tx) const assetNode = findNodeWithAsset( @@ -78,7 +84,7 @@ export function findAssetAmount( : Number(assetNode?.NewFields?.Balance) return amount - ? { currency: asset.currency, issuer: asset.issuer, amount } + ? { currency: asset.currency!, issuer: asset.issuer, amount } : undefined } @@ -89,6 +95,30 @@ export function findAssetAmount( i.e. if we deposit into the amm, the amm balance will go up by the same amount that the account balance decreases, therefore it doesnt matter which node we use. */ +function findMPTAmount( + meta: any, + mptIssuanceId: string, +): ExplorerAmount | undefined { + const mptNodes = findNodes(meta, LedgerEntryTypes.MPToken).filter( + (n: any) => + (n.FinalFields?.MPTokenIssuanceID || n.NewFields?.MPTokenIssuanceID) === + mptIssuanceId, + ) + + if (mptNodes.length === 0) return undefined + + const node = mptNodes[0] + const amount = + node.FinalFields?.MPTAmount != null + ? Math.abs( + Number(node.FinalFields.MPTAmount) - + Number(node.PreviousFields?.MPTAmount ?? 0), + ) + : Number(node.NewFields?.MPTAmount ?? 0) + + return amount ? { currency: mptIssuanceId, amount, isMPT: true } : undefined +} + function findXRPAmount( meta: any, tx: BaseTransaction, @@ -117,7 +147,7 @@ function findXRPAmount( export function findNodeWithAsset( meta: any, entryType: string, - asset: { currency: string; issuer?: string; amount?: number }, + asset: { currency?: string; issuer?: string; amount?: number }, ) { return findNodes(meta, entryType)?.filter( (n: any) => diff --git a/src/containers/shared/test/metaParser.test.ts b/src/containers/shared/test/metaParser.test.ts new file mode 100644 index 000000000..5fc41cef0 --- /dev/null +++ b/src/containers/shared/test/metaParser.test.ts @@ -0,0 +1,181 @@ +import { findAssetAmount, findNodes, LedgerEntryTypes } from '../metaParser' + +describe('findAssetAmount', () => { + const baseTx = { Account: 'rTestAccount', Fee: '12' } as any + + it('finds XRP amount from AccountRoot', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'AccountRoot', + LedgerIndex: 'ABC123', + FinalFields: { + Account: 'rTestAccount', + Balance: '990000000', + }, + PreviousFields: { + Balance: '1000000000', + }, + }, + }, + ], + } + const result = findAssetAmount(meta, { currency: 'XRP' }, baseTx) + expect(result).toBeDefined() + expect(result!.currency).toBe('XRP') + // Balance change is 10000000, fee is 12, so amount = (10000000 - 12) / 1000000 + expect(result!.amount).toBeGreaterThan(0) + }) + + it('finds IOU amount from RippleState', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'RippleState', + LedgerIndex: 'DEF456', + FinalFields: { + Balance: { + currency: 'USD', + issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji', + value: '-990000', + }, + }, + PreviousFields: { + Balance: { + currency: 'USD', + issuer: 'rrrrrrrrrrrrrrrrrrrrBZbvji', + value: '-1000000', + }, + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { currency: 'USD', issuer: 'rIssuer' }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.currency).toBe('USD') + expect(result!.amount).toBe(10000) + }) + + it('finds MPT amount from MPToken', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'GHI789', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: + '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + MPTAmount: '990000', + }, + PreviousFields: { + MPTAmount: '1000000', + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.currency).toBe( + '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expect(result!.amount).toBe(10000) + expect(result!.isMPT).toBe(true) + }) + + it('returns undefined for MPT when no matching MPToken node exists', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'AccountRoot', + LedgerIndex: 'ABC123', + FinalFields: { + Account: 'rTestAccount', + Balance: '999999988', + }, + PreviousFields: { Balance: '1000000000' }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + }, + baseTx, + ) + expect(result).toBeUndefined() + }) + + it('finds MPT amount from newly created MPToken', () => { + const meta = { + AffectedNodes: [ + { + CreatedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'JKL012', + NewFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: + '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + MPTAmount: '5000', + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.amount).toBe(5000) + expect(result!.isMPT).toBe(true) + }) +}) + +describe('findNodes', () => { + it('finds MPToken nodes', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'ABC', + FinalFields: { MPTAmount: '100' }, + }, + }, + { + ModifiedNode: { + LedgerEntryType: 'AccountRoot', + LedgerIndex: 'DEF', + FinalFields: { Balance: '1000' }, + }, + }, + ], + } + const nodes = findNodes(meta, LedgerEntryTypes.MPToken) + expect(nodes).toHaveLength(1) + expect(nodes[0].FinalFields.MPTAmount).toBe('100') + }) +}) diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts index 6f3cab9eb..1879c3835 100644 --- a/src/containers/shared/transactionUtils.ts +++ b/src/containers/shared/transactionUtils.ts @@ -274,9 +274,18 @@ export function zeroPad( } export function normalizeAmount( - amount: IssuedCurrencyAmount | number | string, + amount: + | IssuedCurrencyAmount + | number + | string + | { mpt_issuance_id: string; value: string }, language = 'en-US', ): string | null { + if (typeof amount === 'object' && 'mpt_issuance_id' in amount) { + const currency = amount.mpt_issuance_id + const numberOption = { ...CURRENCY_OPTIONS, currency } + return localizeNumber(amount.value, language, numberOption) + } const currency = typeof amount === 'object' ? amount.currency : 'XRP' const value = typeof amount === 'object' ? amount.value : Number(amount) / XRP_BASE diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index ad9362c8c..f468fdd0b 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -421,13 +421,19 @@ export const formatDurationDetailed = (totalSeconds, maxUnits = 4) => { export const removeRoutes = (routes, ...routesToRemove) => routes.filter((route) => !routesToRemove.includes(route.title)) -export const formatAsset = (asset) => - typeof asset === 'string' - ? { currency: 'XRP' } - : { - currency: asset.currency, - issuer: asset.issuer, - } +export const formatAsset = (asset) => { + if (typeof asset === 'string') return { currency: 'XRP' } + if (asset.mpt_issuance_id) { + return { + currency: asset.mpt_issuance_id, + isMPT: true, + } + } + return { + currency: asset.currency, + issuer: asset.issuer, + } +} export const formatTradingFee = (tradingFee) => tradingFee !== undefined diff --git a/src/rippled/lib/txSummary/formatAmount.test.ts b/src/rippled/lib/txSummary/formatAmount.test.ts new file mode 100644 index 000000000..ee58c7189 --- /dev/null +++ b/src/rippled/lib/txSummary/formatAmount.test.ts @@ -0,0 +1,127 @@ +import { + formatAmount, + isMPTAmount, + formatAsset, + formatAmountWithAsset, +} from './formatAmount' + +describe('formatAmount', () => { + it('formats XRP string amount', () => { + const result = formatAmount('24755081083') + expect(result).toEqual({ currency: 'XRP', amount: 24755.081083 }) + }) + + it('formats IOU amount', () => { + const result = formatAmount({ + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '100.5', + }) + expect(result).toEqual({ + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + amount: 100.5, + }) + }) + + it('formats MPTAmount', () => { + const result = formatAmount({ + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + value: '1000', + }) + expect(result).toEqual({ + currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + amount: '1000', + isMPT: true, + }) + }) + + it('handles null and undefined', () => { + expect(formatAmount(null as any)).toBeNull() + expect(formatAmount(undefined as any)).toBeUndefined() + }) +}) + +describe('isMPTAmount', () => { + it('returns true for MPTAmount', () => { + expect( + isMPTAmount({ + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + value: '100', + }), + ).toBe(true) + }) + + it('returns false for XRP string', () => { + expect(isMPTAmount('12345' as any)).toBe(false) + }) + + it('returns false for IOU', () => { + expect( + isMPTAmount({ + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '100', + } as any), + ).toBe(false) + }) + + it('returns false for MPT asset without value', () => { + expect( + isMPTAmount({ + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + } as any), + ).toBe(false) + }) +}) + +describe('formatAsset', () => { + it('formats XRP string', () => { + expect(formatAsset('XRP')).toEqual({ currency: 'XRP' }) + }) + + it('formats IOU asset', () => { + expect(formatAsset({ currency: 'USD', issuer: 'rXXX' })).toEqual({ + currency: 'USD', + issuer: 'rXXX', + }) + }) + + it('formats MPT asset', () => { + const result = formatAsset({ + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + }) + expect(result).toEqual({ + currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + isMPT: true, + }) + }) +}) + +describe('formatAmountWithAsset', () => { + it('formats XRP amount with asset', () => { + expect(formatAmountWithAsset(1000000, { currency: 'XRP' })).toEqual({ + currency: 'XRP', + amount: 1, + }) + }) + + it('formats MPT amount with asset', () => { + const result = formatAmountWithAsset('500', { + currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + isMPT: true, + }) + expect(result).toEqual({ + currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + amount: 500, + isMPT: true, + }) + }) + + it('returns undefined for null amount', () => { + expect( + formatAmountWithAsset(null as any, { currency: 'XRP' }), + ).toBeUndefined() + }) +}) diff --git a/src/rippled/offers.ts b/src/rippled/offers.ts index 41d7ca08e..88ab90347 100644 --- a/src/rippled/offers.ts +++ b/src/rippled/offers.ts @@ -35,8 +35,8 @@ const getBookOffers = async ( let highestExchangeRate = 0 let lowestExchangeRate = Number.MAX_VALUE for (const offer of offers) { - const takerPays = offer.TakerPays.value || offer.TakerPays - const takerGets = offer.TakerGets.value || offer.TakerGets + const takerPays = Number(offer.TakerPays.value || offer.TakerPays) + const takerGets = Number(offer.TakerGets.value || offer.TakerGets) const rate = takerPays / takerGets if (rate > highestExchangeRate) { highestExchangeRate = rate From 9453ff87629c765ebb007423d2885f9b8f4088f1 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 11:42:11 -0700 Subject: [PATCH 02/27] tests: OfferCreate transaction with MPT Amounts --- .../Transaction/OfferCreate/TableDetail.tsx | 2 - .../test/OfferCreateSimple.test.tsx | 4 +- .../test/OfferCreateTableDetail.test.tsx | 58 +++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx b/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx index 6a66c7227..f905a75af 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/TableDetail.tsx @@ -20,7 +20,6 @@ export const TableDetail = (props: any) => { issuer={firstCurrency.issuer} isMPT={firstCurrency.isMPT} shortenIssuer - shortenMPTIssuanceID /> / { issuer={secondCurrency.issuer} isMPT={secondCurrency.isMPT} shortenIssuer - shortenMPTIssuanceID /> diff --git a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx index 937e7f07c..59f261bc8 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateSimple.test.tsx @@ -100,9 +100,7 @@ describe('OfferCreate: Simple', () => { const { container } = renderComponent(mockOfferCreateMPTPayIOU) expect( container.querySelector('[data-testid="amount-buy"] .value'), - ).toHaveTextContent( - '500 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', - ) + ).toHaveTextContent('500 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') expect( container.querySelector('[data-testid="amount-sell"] .value'), ).toHaveTextContent('100') diff --git a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateTableDetail.test.tsx b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateTableDetail.test.tsx index 7a948bd26..2fcdff40e 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/test/OfferCreateTableDetail.test.tsx @@ -3,11 +3,22 @@ import mockOfferCreateInvertedCurrencies from './mock_data/OfferCreateInvertedCu import mockOfferCreateWithCancel from './mock_data/OfferCreateWithExpirationAndCancel.json' import mockOfferCreate from './mock_data/OfferCreate.json' import mockOfferCreateWithPermissionedDomainID from './mock_data/OfferCreateWithPermissionedDomainID.json' +import mockOfferCreateMPT from './mock_data/OfferCreateMPT.json' import { createTableDetailRenderFactory } from '../../test' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createTableDetailRenderFactory(TableDetail) describe('OfferCreate: TableDetail', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders with an expiration and offer', () => { const { container, unmount } = renderComponent(mockOfferCreateWithCancel) @@ -69,4 +80,51 @@ describe('OfferCreate: TableDetail', () => { ) unmount() }) + + it('renders OfferCreate with MPT (no ticker - displays mpt_issuance_id)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(mockOfferCreateMPT) + + // Price pair should show MPT ID + expect(container.querySelector('[data-testid="pair"]')).toHaveTextContent( + 'price:0.02 \uE900 XRP/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + // Sell amount should show full MPT ID + const amounts = container.querySelectorAll('[data-testid="amount"]') + expect(amounts[1]).toHaveTextContent('50,000.00 XRP') + expect(amounts[2]).toHaveTextContent( + '1,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + // MPT link should point to the MPT page + const mptLink = container.querySelector('a[href*="/mpt/"]') + expect(mptLink).toHaveAttribute( + 'href', + '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders OfferCreate with MPT (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockOfferCreateMPT) + + // Price pair should show ticker instead of shortened ID + expect(container.querySelector('[data-testid="pair"]')).toHaveTextContent( + 'price:0.02 \uE900 XRP/XMPT', + ) + // Sell amount should show ticker instead of full ID + const amounts = container.querySelectorAll('[data-testid="amount"]') + expect(amounts[2]).toHaveTextContent('1,000 XMPT') + // MPT link should still point to the MPT page using full ID + const mptLink = container.querySelector('a[href*="/mpt/"]') + expect(mptLink).toHaveAttribute( + 'href', + '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) }) From 67626a29b93e2fc30c4dcf40eb42c893b6de6c80 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 11:53:15 -0700 Subject: [PATCH 03/27] test: Payment transaction Simple, Description and Detailed views with MPT Amounts --- .../Payment/test/PaymentDescription.test.tsx | 37 +++++++++++++++++++ .../Payment/test/PaymentSimple.test.tsx | 22 +++++++++++ .../Payment/test/PaymentTableDetail.test.tsx | 35 ++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/src/containers/shared/components/Transaction/Payment/test/PaymentDescription.test.tsx b/src/containers/shared/components/Transaction/Payment/test/PaymentDescription.test.tsx index 3a47113e4..a9859f72b 100644 --- a/src/containers/shared/components/Transaction/Payment/test/PaymentDescription.test.tsx +++ b/src/containers/shared/components/Transaction/Payment/test/PaymentDescription.test.tsx @@ -8,11 +8,22 @@ import mockPaymentPartial from './mock_data/PaymentWithPartial.json' import mockPaymentSendMax from './mock_data/PaymentWithSendMax.json' import mockPaymentSourceTag from './mock_data/PaymentWithSourceTag.json' import mockPermDomainID from './mock_data/PaymentWithPermDomainID.json' +import mockPaymentMPT from './mock_data/PaymentMPT.json' import { createDescriptionRenderFactory } from '../../test' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createDescriptionRenderFactory(Description, i18n) describe('Payment: Description', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders', () => { const { container, unmount } = renderComponent(mockPayment) @@ -164,4 +175,30 @@ describe('Payment: Description', () => { `Domain ID: D3261DF48CDA3B860ED3FA99F02138856393CD44556E028D5CB66192A18A8D02`, ) }) + + it('renders direct MPT payment (no ticker - displays mpt_issuance_id)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 3 }, + }) + const { container, unmount } = renderComponent(mockPaymentMPT) + + expect( + container.querySelector('[data-testid="amount-line"]'), + ).toHaveTextContent( + 'It was instructed to deliver 0.1 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders direct MPT payment (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 3, parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockPaymentMPT) + + expect( + container.querySelector('[data-testid="amount-line"]'), + ).toHaveTextContent('It was instructed to deliver 0.1 XMPT') + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/Payment/test/PaymentSimple.test.tsx b/src/containers/shared/components/Transaction/Payment/test/PaymentSimple.test.tsx index 3c46135d1..186487635 100644 --- a/src/containers/shared/components/Transaction/Payment/test/PaymentSimple.test.tsx +++ b/src/containers/shared/components/Transaction/Payment/test/PaymentSimple.test.tsx @@ -157,6 +157,28 @@ describe('Payment: Simple', () => { unmount() }) + it('renders direct MPT payment with ticker', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { + assetScale: 3, + parsedMPTMetadata: { ticker: 'XMPT' }, + }, + }) + + const { container, unmount } = renderComponent(mockPaymentMPT) + + expectSimpleRowText(container, 'amount', `0.1 XMPT`) + expectSimpleRowLabel(container, 'amount', `send`) + + expectSimpleRowText( + container, + 'destination', + `rw6UtpfBFaGht6SiC1HpDPNw6Yt25pKvnu`, + ) + + unmount() + }) + it(`renders with Permissioned Domain ID`, () => { const { container, unmount } = renderComponent(mockPermDomainID) diff --git a/src/containers/shared/components/Transaction/Payment/test/PaymentTableDetail.test.tsx b/src/containers/shared/components/Transaction/Payment/test/PaymentTableDetail.test.tsx index 25bdc830b..14ea81abf 100644 --- a/src/containers/shared/components/Transaction/Payment/test/PaymentTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/Payment/test/PaymentTableDetail.test.tsx @@ -8,10 +8,21 @@ import mockPaymentSendMax from './mock_data/PaymentWithSendMax.json' import mockPaymentSourceTag from './mock_data/PaymentWithSourceTag.json' import mockPermDomainID from './mock_data/PaymentWithPermDomainID.json' import mockPaymentCredentialIDs from './mock_data/PaymentWithCredentialIDs.json' +import mockPaymentMPT from './mock_data/PaymentMPT.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createTableDetailRenderFactory(TableDetail) describe('Payment: TableDetail', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders', () => { const { container, unmount } = renderComponent(mockPayment) @@ -103,4 +114,28 @@ describe('Payment: TableDetail', () => { unmount() }) + + it('renders direct MPT payment (no ticker - displays mpt_issuance_id)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 3 }, + }) + const { container, unmount } = renderComponent(mockPaymentMPT) + + expect(container.querySelector('.payment')).toHaveTextContent( + 'send0.1 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5Ftorw6UtpfBFaGht6SiC1HpDPNw6Yt25pKvnu', + ) + unmount() + }) + + it('renders direct MPT payment (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 3, parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockPaymentMPT) + + expect(container.querySelector('.payment')).toHaveTextContent( + 'send0.1 XMPTtorw6UtpfBFaGht6SiC1HpDPNw6Yt25pKvnu', + ) + unmount() + }) }) From 13c81a608fe1b0995a4585dd9ebde95b64bee1c9 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 11:59:25 -0700 Subject: [PATCH 04/27] test: AMMDelete transaction with MPT Amount --- .../test/AMMDeleteDescription.test.tsx | 44 +++++++++++++++++ .../AMMDelete/test/AMMDeleteSimple.test.tsx | 34 +++++++++++++- .../test/AMMDeleteTableDetail.test.tsx | 44 +++++++++++++++++ .../test/mock_data/AMMDeleteMPT.json | 47 +++++++++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/containers/shared/components/Transaction/AMMDelete/test/mock_data/AMMDeleteMPT.json diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx index 9292b7c39..f898b32df 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx @@ -1,11 +1,22 @@ import i18n from '../../../../../../i18n/testConfigEnglish' import mockAMMDelete from './mock_data/AMMDelete.json' +import mockAMMDeleteMPT from './mock_data/AMMDeleteMPT.json' import { Description } from '../Description' import { createDescriptionRenderFactory } from '../../test' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createDescriptionRenderFactory(Description, i18n) describe('AMMDelete: Description', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders description for AMMDelete transaction', () => { const { container, unmount } = renderComponent(mockAMMDelete) @@ -21,4 +32,37 @@ describe('AMMDelete: Description', () => { unmount() }) + + it('renders with MPT asset (no ticker - displays mpt_issuance_id)', () => { + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + + expect( + container.querySelector('[data-testid="amm-delete-description"]'), + ).toHaveTextContent( + 'Attempted to delete the AMM for \uE900 XRP and 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F.', + ) + expect(container.querySelector('a')).toHaveAttribute( + 'href', + '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with MPT asset (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + + expect( + container.querySelector('[data-testid="amm-delete-description"]'), + ).toHaveTextContent( + 'Attempted to delete the AMM for \uE900 XRP and XMPT.', + ) + expect(container.querySelector('a')).toHaveAttribute( + 'href', + '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteSimple.test.tsx b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteSimple.test.tsx index 95a08d3f7..15fa5b212 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteSimple.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteSimple.test.tsx @@ -4,12 +4,23 @@ import { expectSimpleRowText } from '../../test' import { createSimpleRenderFactory } from '../../test/createWrapperFactory' import { Simple } from '../Simple' import mockAMMDelete from './mock_data/AMMDelete.json' +import mockAMMDeleteMPT from './mock_data/AMMDeleteMPT.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createSimpleRenderFactory(Simple, i18n) describe('AMMDelete: Simple', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders', () => { - const { container, unmount } = renderComponent(mockAMMDelete) // TOOD: - Make this look up asset 1 / asset 2 currency codes + const { container, unmount } = renderComponent(mockAMMDelete) expectSimpleRowText(container, 'asset1', '\uE900 XRP') expectSimpleRowText( container, @@ -18,4 +29,25 @@ describe('AMMDelete: Simple', () => { ) unmount() }) + + it('renders with MPT asset (no ticker - displays mpt_issuance_id)', () => { + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + expectSimpleRowText(container, 'asset1', '\uE900 XRP') + expectSimpleRowText( + container, + 'asset2', + '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with MPT asset (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + expectSimpleRowText(container, 'asset1', '\uE900 XRP') + expectSimpleRowText(container, 'asset2', 'XMPT') + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx index 5eecff873..87ae1758a 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx @@ -1,11 +1,22 @@ import { TableDetail } from '../TableDetail' import mockAMMDelete from './mock_data/AMMDelete.json' +import mockAMMDeleteMPT from './mock_data/AMMDeleteMPT.json' import { createTableDetailRenderFactory } from '../../test' import i18n from '../../../../../../i18n/testConfigEnglish' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createTableDetailRenderFactory(TableDetail, i18n) describe('AMMDelete: TableDetail', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders with an expiration and offer', () => { const { container, unmount } = renderComponent(mockAMMDelete) @@ -17,4 +28,37 @@ describe('AMMDelete: TableDetail', () => { ) unmount() }) + + it('renders with MPT asset (no ticker - displays mpt_issuance_id)', () => { + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + + expect(container.querySelector('[data-testid="asset"]')).toHaveTextContent( + 'Asset 1\uE900 XRP', + ) + expect(container.querySelector('[data-testid="asset2"]')).toHaveTextContent( + 'Asset 2000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with MPT asset (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(mockAMMDeleteMPT) + + expect(container.querySelector('[data-testid="asset"]')).toHaveTextContent( + 'Asset 1\uE900 XRP', + ) + expect(container.querySelector('[data-testid="asset2"]')).toHaveTextContent( + 'Asset 2XMPT', + ) + expect( + container.querySelector('[data-testid="asset2"] a'), + ).toHaveAttribute( + 'href', + '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/mock_data/AMMDeleteMPT.json b/src/containers/shared/components/Transaction/AMMDelete/test/mock_data/AMMDeleteMPT.json new file mode 100644 index 000000000..b2e3717a5 --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMDelete/test/mock_data/AMMDeleteMPT.json @@ -0,0 +1,47 @@ +{ + "tx": { + "Account": "rm5c42Crqpdch5fbuCdHmSMV1wrL9arV9", + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "12", + "Flags": 0, + "LastLedgerSequence": 372572, + "Sequence": 372548, + "SigningPubKey": "ED6784394D134E202BCCD957A1A3C5A66647092F3929D388A878A16D1910875435", + "TransactionType": "AMMDelete", + "TxnSignature": "F9AA459D8CE593E6E2E69BB6A6F723A4822FD5F40314F642FA9EC5187F7FD937FBD3E8A214119D04C74358A3478DCB6EAABE02EFAC1E6E125E15310E18A36D0D", + "date": 1693268101000 + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rm5c42Crqpdch5fbuCdHmSMV1wrL9arV9", + "Balance": "9997998976", + "Flags": 8388608, + "OwnerCount": 1, + "Sequence": 372549 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "84CA74ECFDB34F014142013B4CD2FBE3942C7BA9BA7E1FC5A1CB1EF719173812", + "PreviousFields": { + "Balance": "9997998988", + "Sequence": 372548 + }, + "PreviousTxnID": "E5051DA09F143A719521D6ABBB3856EA3E2CA38EF1CFF0E7DF9FE1C31DD73B6D", + "PreviousTxnLgrSeq": 372552 + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tecAMM_NOT_EMPTY" + }, + "hash": "D159883D456646562F51F3E5A2754F7D880D39A6372EDF679A43A7DDB77F735C", + "ledger_index": 372554, + "date": 1693268101000 +} From 54cd50ac55985bf69dcb849a2098402143cca6c6 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 12:07:05 -0700 Subject: [PATCH 05/27] test: AMMDeposit tests with MPT amounts --- .../AMMDeposit/test/AMMDeposit.test.tsx | 81 +++++++++++++ .../test/mock_data/deposit_both_mpt.json | 93 +++++++++++++++ .../test/mock_data/deposit_mpt.json | 111 ++++++++++++++++++ 3 files changed, 285 insertions(+) create mode 100644 src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_both_mpt.json create mode 100644 src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_mpt.json diff --git a/src/containers/shared/components/Transaction/AMMDeposit/test/AMMDeposit.test.tsx b/src/containers/shared/components/Transaction/AMMDeposit/test/AMMDeposit.test.tsx index 3a811f4bc..50b0ea73d 100644 --- a/src/containers/shared/components/Transaction/AMMDeposit/test/AMMDeposit.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDeposit/test/AMMDeposit.test.tsx @@ -11,10 +11,22 @@ import depositEprice from './mock_data/deposit_eprice.json' import depositNonXRP from './mock_data/deposit_nonxrp.json' import depositFail from './mock_data/deposit_fail.json' import depositLPToken from './mock_data/deposit_lptoken.json' +import depositMPT from './mock_data/deposit_mpt.json' +import depositBothMPT from './mock_data/deposit_both_mpt.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) describe('AMM Deposit Tests', () => { const renderComponent = createSimpleRenderFactory(Simple) + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders with both assets', () => { const { container, unmount } = renderComponent(depositBothAssets) expectSimpleRowText(container, 'asset1', '\uE90010,997.290462 XRP') @@ -112,4 +124,73 @@ describe('AMM Deposit Tests', () => { expectSimpleRowText(container, 'lp_tokens', '4,279,342.4') unmount() }) + + it('renders with MPT asset (no ticker - displays mpt_issuance_id)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(depositMPT) + expectSimpleRowText(container, 'asset1', '\uE9001,000.00 XRP') + expectSimpleRowText( + container, + 'asset2', + '10,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'account_id', + 'rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W', + ) + unmount() + }) + + it('renders with MPT asset (with ticker - displays ticker symbol)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XMPT' } }, + }) + const { container, unmount } = renderComponent(depositMPT) + expectSimpleRowText(container, 'asset1', '\uE9001,000.00 XRP') + expectSimpleRowText(container, 'asset2', '10,000 XMPT') + expectSimpleRowText( + container, + 'account_id', + 'rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W', + ) + unmount() + }) + + it('renders with both assets as MPT (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(depositBothMPT) + expectSimpleRowText( + container, + 'asset1', + '5,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'asset2', + '2,000 00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with both assets as MPT (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockImplementation((mptID: string) => { + if (mptID === '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') { + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + } + } + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XUSD' } }, + } + }) + const { container, unmount } = renderComponent(depositBothMPT) + expectSimpleRowText(container, 'asset1', '5,000 XGLD') + expectSimpleRowText(container, 'asset2', '2,000 XUSD') + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_both_mpt.json b/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_both_mpt.json new file mode 100644 index 000000000..1cb5885d3 --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_both_mpt.json @@ -0,0 +1,93 @@ +{ + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "495000" + }, + "PreviousFields": { + "MPTAmount": "500000" + }, + "PreviousTxnID": "98D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114138 + } + }, + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "CCDD00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "248000" + }, + "PreviousFields": { + "MPTAmount": "250000" + }, + "PreviousTxnID": "88D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114139 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Flags": 0, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10515288.25827238" + }, + "TradingFee": 0 + }, + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "PreviousFields": { + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10027169" + } + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "tx": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Amount": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "5000" + }, + "Amount2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "value": "2000" + }, + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "10", + "Flags": 1048576, + "Sequence": 7, + "SigningPubKey": "023CFED4018084296285DD8A321C099134B9CF6DCD8D91DC067BABCFF0E3F2BE1A", + "TransactionType": "AMMDeposit", + "TxnSignature": "3045022100D1363F0A6D7252690820657B6ACCB35245E65D8DCDB48199578C213ED9D3E24B0220697D4DC057ECCD942BC59B0242411839C125696CD9DE9A3EFE45367EE0D1D29D", + "date": "2022-11-26T00:55:02Z" + } +} diff --git a/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_mpt.json b/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_mpt.json new file mode 100644 index 000000000..55d25f56b --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMDeposit/test/mock_data/deposit_mpt.json @@ -0,0 +1,111 @@ +{ + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "990000" + }, + "PreviousFields": { + "MPTAmount": "1000000" + }, + "PreviousTxnID": "98D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114138 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Balance": "11027169000", + "Flags": 59768832, + "OwnerCount": 1, + "Sequence": 1 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "471AC5276FBA4916D53017D7073D44C5F4780CC73954B1715DC8A65365E8ACAC", + "PreviousFields": { + "Balance": "10027169000" + }, + "PreviousTxnID": "209B17403B42271F2D50DEC0F808AE07EC04B8B9605FF52B1093BFF31676AD2C", + "PreviousTxnLgrSeq": 233852 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Balance": "998972830930", + "Flags": 8388608, + "OwnerCount": 2, + "Sequence": 8 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "53383A918D45DEF78DED23CE5141C0FAB44661D602465F5FCFC487792448F1E2", + "PreviousFields": { + "Balance": "999972830940", + "Sequence": 7 + }, + "PreviousTxnID": "209B17403B42271F2D50DEC0F808AE07EC04B8B9605FF52B1093BFF31676AD2C", + "PreviousTxnLgrSeq": 233852 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Flags": 0, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10515288.25827238" + }, + "TradingFee": 0 + }, + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "PreviousFields": { + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10027169" + } + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "tx": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Amount": "1000000000", + "Amount2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "10000" + }, + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "10", + "Flags": 1048576, + "Sequence": 7, + "SigningPubKey": "023CFED4018084296285DD8A321C099134B9CF6DCD8D91DC067BABCFF0E3F2BE1A", + "TransactionType": "AMMDeposit", + "TxnSignature": "3045022100D1363F0A6D7252690820657B6ACCB35245E65D8DCDB48199578C213ED9D3E24B0220697D4DC057ECCD942BC59B0242411839C125696CD9DE9A3EFE45367EE0D1D29D", + "date": "2022-11-26T00:55:02Z" + } +} From 5fc004f34b6acabf4bb3e525b0a8c55973614536 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 12:51:29 -0700 Subject: [PATCH 06/27] test: AMMCreate transaction with two MPT assets --- .../AMMCreate/test/AMMCreate.test.tsx | 77 ++++++++++++++----- .../test/mock_data/amm_create_both_mpt.json | 75 ++++++++++++++++++ 2 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_both_mpt.json diff --git a/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx b/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx index 790241f02..8cb5dd626 100644 --- a/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx +++ b/src/containers/shared/components/Transaction/AMMCreate/test/AMMCreate.test.tsx @@ -1,25 +1,20 @@ -import { useQuery } from 'react-query' import { Simple } from '../Simple' import { createSimpleRenderFactory, expectSimpleRowText } from '../../test' import createMock from './mock_data/amm_create.json' import createMockMpt from './mock_data/amm_create_mpt.json' +import createMockBothMpt from './mock_data/amm_create_both_mpt.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' -jest.mock('react-query', () => ({ - ...jest.requireActual('react-query'), - useQuery: jest.fn(), +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), })) describe('AMM Create Tests', () => { const renderComponent = createSimpleRenderFactory(Simple) beforeEach(() => { - // @ts-ignore - useQuery.mockImplementation((args: any, fn: any, opts: any) => { - if (opts?.enabled === false || !opts?.enabled) { - return { data: undefined } - } - return { data: { assetScale: 0 } } - }) + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) }) it('renders from transaction', () => { @@ -39,16 +34,62 @@ describe('AMM Create Tests', () => { unmount() }) - it('renders AMMCreate with XRP + MPT pair', () => { - // @ts-ignore - useQuery.mockImplementation(() => ({ + it('renders AMMCreate with XRP + MPT pair (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: { assetScale: 0 }, - })) + }) + const { container, unmount } = renderComponent(createMockMpt) + expectSimpleRowText(container, 'asset1', '\uE90010,000.00 XRP') + expectSimpleRowText( + container, + 'asset2', + '10,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders AMMCreate with XRP + MPT pair (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + }) const { container, unmount } = renderComponent(createMockMpt) expectSimpleRowText(container, 'asset1', '\uE90010,000.00 XRP') - // asset2 should contain MPT amount value - const asset2 = container.querySelector('[data-testid="asset2"] .value') - expect(asset2).toHaveTextContent('10,000') + expectSimpleRowText(container, 'asset2', '10,000 XGLD') + unmount() + }) + + it('renders AMMCreate with both MPT assets (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(createMockBothMpt) + expectSimpleRowText( + container, + 'asset1', + '5,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'asset2', + '2,000 00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders AMMCreate with both MPT assets (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockImplementation((mptID: string) => { + if (mptID === '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') { + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + } + } + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XUSD' } }, + } + }) + const { container, unmount } = renderComponent(createMockBothMpt) + expectSimpleRowText(container, 'asset1', '5,000 XGLD') + expectSimpleRowText(container, 'asset2', '2,000 XUSD') unmount() }) }) diff --git a/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_both_mpt.json b/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_both_mpt.json new file mode 100644 index 000000000..ee41d804f --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMCreate/test/mock_data/amm_create_both_mpt.json @@ -0,0 +1,75 @@ +{ + "tx": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "Amount": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "5000" + }, + "Amount2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "value": "2000" + }, + "Fee": "10", + "Flags": 0, + "Sequence": 3, + "SigningPubKey": "02BFF00BCB2D25845C1D7C1FC5AAAE465B56CBB966BF034E1F6FFC097E8A6FBD28", + "TradingFee": 1, + "TransactionType": "AMMCreate", + "TxnSignature": "304402200CE5EA35FB7545CA8CD3231C2EE6F23D6B9EF7E146F23016905B9516721D5745022062DCC01BB7617C93F85DAE93917CC585FF09AE690BA78FDC5924565EEC186C70", + "date": "2022-11-21T20:58:11Z" + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "495000" + }, + "PreviousFields": { + "MPTAmount": "500000" + } + } + }, + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "CCDD00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rwRGF9pmfEGT4GcZ379cYC9p3wpJDozy8w", + "MPTokenIssuanceID": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "248000" + }, + "PreviousFields": { + "MPTAmount": "250000" + } + } + }, + { + "CreatedNode": { + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "NewFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10000000" + } + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + } +} From 5782f1446901a543153c36fce573703e8b04e3b5 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 12:55:55 -0700 Subject: [PATCH 07/27] test: AMMClawback tests with MPT assets --- .../test/AMMClawbackSimple.test.tsx | 54 +++++++++++++ .../test/AMMClawbackTableDetail.test.tsx | 45 +++++++++++ .../AMMClawback/test/mock_data/withMPT.json | 75 +++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 src/containers/shared/components/Transaction/AMMClawback/test/mock_data/withMPT.json diff --git a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackSimple.test.tsx b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackSimple.test.tsx index 098698bbb..edefbe394 100644 --- a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackSimple.test.tsx +++ b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackSimple.test.tsx @@ -5,10 +5,21 @@ import { Simple } from '../Simple' import mockAMMClawbackNoFlag from './mock_data/withoutFlag.json' import mockAMMClawbackWithAmount from './mock_data/withAmount.json' import mockAMMClawbackWithFlag from './mock_data/withFlag.json' +import mockAMMClawbackMPT from './mock_data/withMPT.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderSimple = createSimpleRenderFactory(Simple) describe('AMMClawback: Simple', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders without tfClawTwoAssets flag, only one asset should be clawed back', () => { const { container } = renderSimple(mockAMMClawbackNoFlag) expectSimpleRowText( @@ -51,4 +62,47 @@ describe('AMMClawback: Simple', () => { '$20.00 USD.rK2Du3gUmFbg5UFFHFq9LKywVuGbqNsyyi', ) }) + + it('renders with MPT assets (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container } = renderSimple(mockAMMClawbackMPT) + expectSimpleRowText( + container, + 'asset1', + '260 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'asset2', + '100 00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'holder', + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', + ) + }) + + it('renders with MPT assets (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockImplementation((mptID: string) => { + if (mptID === '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') { + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + } + } + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XUSD' } }, + } + }) + const { container } = renderSimple(mockAMMClawbackMPT) + expectSimpleRowText(container, 'asset1', '260 XGLD') + expectSimpleRowText(container, 'asset2', '100 XUSD') + expectSimpleRowText( + container, + 'holder', + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', + ) + }) }) diff --git a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx index a5fd3bd0d..4820700ac 100644 --- a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx @@ -3,10 +3,21 @@ import { TableDetail } from '../TableDetail' import AMMClawbackNoFlag from './mock_data/withoutFlag.json' import AMMClawbackWithFlag from './mock_data/withFlag.json' import mockAMMClawbackWithAmount from './mock_data/withAmount.json' +import mockAMMClawbackMPT from './mock_data/withMPT.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) const renderComponent = createTableDetailRenderFactory(TableDetail) describe('AMMClawback: TableDetail', () => { + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders without tfClawTwoAssets flag, only one asset should be clawed back', () => { const { container, unmount } = renderComponent(AMMClawbackNoFlag) expect(container).toHaveTextContent( @@ -41,4 +52,38 @@ describe('AMMClawback: TableDetail', () => { ) unmount() }) + + it('renders with MPT assets (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(mockAMMClawbackMPT) + expect(container).toHaveTextContent( + 'claws_back' + + '260 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + + 'and' + + '100 00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F' + + 'from' + + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', + ) + unmount() + }) + + it('renders with MPT assets (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockImplementation((mptID: string) => { + if (mptID === '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') { + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + } + } + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XUSD' } }, + } + }) + const { container, unmount } = renderComponent(mockAMMClawbackMPT) + expect(container).toHaveTextContent( + 'claws_back' + '260 XGLD' + 'and' + '100 XUSD' + 'from' + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', + ) + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMClawback/test/mock_data/withMPT.json b/src/containers/shared/components/Transaction/AMMClawback/test/mock_data/withMPT.json new file mode 100644 index 000000000..250443d7e --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMClawback/test/mock_data/withMPT.json @@ -0,0 +1,75 @@ +{ + "tx": { + "Account": "rUuVtbgagFKjHPTxmN639XYVHLATnB6VNk", + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "12", + "Flags": 1, + "Holder": "r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN", + "LastLedgerSequence": 254460, + "Sequence": 254428, + "SigningPubKey": "ED7FBCDEF465DF8D6D25B50EDF4B10ECB4A50C15B4A7F5EE49437DD17B5B67E39B", + "TransactionType": "AMMClawback", + "TxnSignature": "19846DD492CD9F914333DA06CE17545A8447BDC5F0191F76ABDDEFFECA2261E9A75861D66578AF83569B254D6303C475F92DB84B9C4979EE8C0F20AFAD405304", + "ctid": "C003E1EA00000002", + "date": 1739385880000 + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "740" + }, + "PreviousFields": { + "MPTAmount": "1000" + } + } + }, + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "CCDD00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN", + "MPTokenIssuanceID": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "900" + }, + "PreviousFields": { + "MPTAmount": "1000" + } + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rUuVtbgagFKjHPTxmN639XYVHLATnB6VNk", + "Balance": "99999940", + "Flags": 2155872256, + "OwnerCount": 0, + "Sequence": 254429 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "49648B64B65B612D89AA414A3C352400A3B38DE891D551C7103E4C439F0270FE", + "PreviousFields": { + "Balance": "99999952", + "Sequence": 254428 + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "hash": "9ADAC8C3CF9A7D162EFB93F9CFEBBFFBD8B17F3AFAE26C634E154FF1015AEFC0", + "ledger_index": 254442, + "date": 1739385880000 +} From d31ee1883331e6e5f97a20531a22d034eec01963 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 12:59:32 -0700 Subject: [PATCH 08/27] test: AMMWithdraw transaction with MPT asset/s --- .../AMMWithdraw/test/AMMWithdraw.test.tsx | 71 +++++++++++ .../test/mock_data/withdraw_both_mpt.json | 93 +++++++++++++++ .../test/mock_data/withdraw_mpt.json | 111 ++++++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_both_mpt.json create mode 100644 src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_mpt.json diff --git a/src/containers/shared/components/Transaction/AMMWithdraw/test/AMMWithdraw.test.tsx b/src/containers/shared/components/Transaction/AMMWithdraw/test/AMMWithdraw.test.tsx index 3b7d6b635..1b1a450be 100644 --- a/src/containers/shared/components/Transaction/AMMWithdraw/test/AMMWithdraw.test.tsx +++ b/src/containers/shared/components/Transaction/AMMWithdraw/test/AMMWithdraw.test.tsx @@ -9,10 +9,22 @@ import withdrawUSDMock from './mock_data/withdraw_usd.json' import withdrawXRPMock from './mock_data/withdraw_xrp.json' import withdrawEpriceMock from './mock_data/withdraw_eprice.json' import withdrawAll from './mock_data/withdraw_all.json' +import withdrawMPT from './mock_data/withdraw_mpt.json' +import withdrawBothMPT from './mock_data/withdraw_both_mpt.json' +import { useMPTIssuance } from '../../../../hooks/useMPTIssuance' + +jest.mock('../../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../../hooks/useMPTIssuance'), + useMPTIssuance: jest.fn(), +})) describe('AMM Withdraw Tests', () => { const renderComponent = createSimpleRenderFactory(Simple) + beforeEach(() => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ data: undefined }) + }) + it('renders from transaction', () => { const { container, unmount } = renderComponent(withdrawMock) expectSimpleRowText(container, 'asset1', '\uE9003,666.580862 XRP') @@ -84,4 +96,63 @@ describe('AMM Withdraw Tests', () => { expectSimpleRowText(container, 'asset1', '\uE9000.000005 XRP') unmount() }) + + it('renders with XRP + MPT withdraw (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(withdrawMPT) + expectSimpleRowText(container, 'asset1', '\uE900999.99998 XRP') + expectSimpleRowText( + container, + 'asset2', + '5,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with XRP + MPT withdraw (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + }) + const { container, unmount } = renderComponent(withdrawMPT) + expectSimpleRowText(container, 'asset1', '\uE900999.99998 XRP') + expectSimpleRowText(container, 'asset2', '5,000 XGLD') + unmount() + }) + + it('renders with both MPT assets withdraw (no ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { assetScale: 0 }, + }) + const { container, unmount } = renderComponent(withdrawBothMPT) + expectSimpleRowText( + container, + 'asset1', + '3,000 000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + ) + expectSimpleRowText( + container, + 'asset2', + '1,500 00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F', + ) + unmount() + }) + + it('renders with both MPT assets withdraw (with ticker)', () => { + ;(useMPTIssuance as jest.Mock).mockImplementation((mptID: string) => { + if (mptID === '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F') { + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XGLD' } }, + } + } + return { + data: { assetScale: 0, parsedMPTMetadata: { ticker: 'XUSD' } }, + } + }) + const { container, unmount } = renderComponent(withdrawBothMPT) + expectSimpleRowText(container, 'asset1', '3,000 XGLD') + expectSimpleRowText(container, 'asset2', '1,500 XUSD') + unmount() + }) }) diff --git a/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_both_mpt.json b/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_both_mpt.json new file mode 100644 index 000000000..91605e2c8 --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_both_mpt.json @@ -0,0 +1,93 @@ +{ + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "503000" + }, + "PreviousFields": { + "MPTAmount": "500000" + }, + "PreviousTxnID": "98D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114138 + } + }, + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "CCDD00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "251500" + }, + "PreviousFields": { + "MPTAmount": "250000" + }, + "PreviousTxnID": "88D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114139 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Flags": 0, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "9515288.25827238" + }, + "TradingFee": 0 + }, + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "PreviousFields": { + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10027169" + } + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "tx": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Amount": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "3000" + }, + "Amount2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F", + "value": "1500" + }, + "Asset": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Asset2": { + "mpt_issuance_id": "00000ABC2E631B9DFA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "10", + "Flags": 1048576, + "Sequence": 7, + "SigningPubKey": "023CFED4018084296285DD8A321C099134B9CF6DCD8D91DC067BABCFF0E3F2BE1A", + "TransactionType": "AMMWithdraw", + "TxnSignature": "3045022100D1363F0A6D7252690820657B6ACCB35245E65D8DCDB48199578C213ED9D3E24B0220697D4DC057ECCD942BC59B0242411839C125696CD9DE9A3EFE45367EE0D1D29D", + "date": "2022-11-26T00:55:02Z" + } +} diff --git a/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_mpt.json b/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_mpt.json new file mode 100644 index 000000000..dce212470 --- /dev/null +++ b/src/containers/shared/components/Transaction/AMMWithdraw/test/mock_data/withdraw_mpt.json @@ -0,0 +1,111 @@ +{ + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "LedgerEntryType": "MPToken", + "LedgerIndex": "AABB00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDD", + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "MPTokenIssuanceID": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "MPTAmount": "1005000" + }, + "PreviousFields": { + "MPTAmount": "1000000" + }, + "PreviousTxnID": "98D314D1EC81BE9342EDA1C04BCEFA8F327B0EFE12839F1EAB52065492B20E82", + "PreviousTxnLgrSeq": 114138 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Balance": "9027169000", + "Flags": 59768832, + "OwnerCount": 1, + "Sequence": 1 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "471AC5276FBA4916D53017D7073D44C5F4780CC73954B1715DC8A65365E8ACAC", + "PreviousFields": { + "Balance": "10027169000" + }, + "PreviousTxnID": "209B17403B42271F2D50DEC0F808AE07EC04B8B9605FF52B1093BFF31676AD2C", + "PreviousTxnLgrSeq": 233852 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Balance": "1000972830930", + "Flags": 8388608, + "OwnerCount": 2, + "Sequence": 8 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "53383A918D45DEF78DED23CE5141C0FAB44661D602465F5FCFC487792448F1E2", + "PreviousFields": { + "Balance": "999972830940", + "Sequence": 7 + }, + "PreviousTxnID": "209B17403B42271F2D50DEC0F808AE07EC04B8B9605FF52B1093BFF31676AD2C", + "PreviousTxnLgrSeq": 233852 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Account": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Flags": 0, + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "9515288.25827238" + }, + "TradingFee": 0 + }, + "LedgerEntryType": "AMM", + "LedgerIndex": "C7FD06649235AF4CABD8FA6D8BB0CAF6C6EA5038A74D0DDD5025290683636D02", + "PreviousFields": { + "LPTokenBalance": { + "currency": "03930D02208264E2E40EC1B0C09E4DB96EE197B1", + "issuer": "rMEdVzU8mtEArzjrN9avm3kA675GX7ez8W", + "value": "10027169" + } + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "tx": { + "Account": "rUwaiErsYE5kibUUtaPczXZVVd73VNy4R9", + "Amount": "1000000000", + "Amount2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F", + "value": "5000" + }, + "Asset": { + "currency": "XRP" + }, + "Asset2": { + "mpt_issuance_id": "000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F" + }, + "Fee": "10", + "Flags": 1048576, + "Sequence": 7, + "SigningPubKey": "023CFED4018084296285DD8A321C099134B9CF6DCD8D91DC067BABCFF0E3F2BE1A", + "TransactionType": "AMMWithdraw", + "TxnSignature": "3045022100D1363F0A6D7252690820657B6ACCB35245E65D8DCDB48199578C213ED9D3E24B0220697D4DC057ECCD942BC59B0242411839C125696CD9DE9A3EFE45367EE0D1D29D", + "date": "2022-11-26T00:55:02Z" + } +} From 8ac00cd75a0d656dc3228115c5bd4753c1648a85 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 13:08:36 -0700 Subject: [PATCH 09/27] [trivial] linter updates --- .../AMMClawback/test/AMMClawbackTableDetail.test.tsx | 7 ++++++- .../AMMDelete/test/AMMDeleteDescription.test.tsx | 4 +--- .../AMMDelete/test/AMMDeleteTableDetail.test.tsx | 4 +--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx index 4820700ac..c0955240b 100644 --- a/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/AMMClawback/test/AMMClawbackTableDetail.test.tsx @@ -82,7 +82,12 @@ describe('AMMClawback: TableDetail', () => { }) const { container, unmount } = renderComponent(mockAMMClawbackMPT) expect(container).toHaveTextContent( - 'claws_back' + '260 XGLD' + 'and' + '100 XUSD' + 'from' + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', + 'claws_back' + + '260 XGLD' + + 'and' + + '100 XUSD' + + 'from' + + 'r4eWC5DixP74dpk7FDzXcap1BJ2NaoUeZN', ) unmount() }) diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx index f898b32df..10fa7cf2d 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteDescription.test.tsx @@ -56,9 +56,7 @@ describe('AMMDelete: Description', () => { expect( container.querySelector('[data-testid="amm-delete-description"]'), - ).toHaveTextContent( - 'Attempted to delete the AMM for \uE900 XRP and XMPT.', - ) + ).toHaveTextContent('Attempted to delete the AMM for \uE900 XRP and XMPT.') expect(container.querySelector('a')).toHaveAttribute( 'href', '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', diff --git a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx index 87ae1758a..fe2501edd 100644 --- a/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx +++ b/src/containers/shared/components/Transaction/AMMDelete/test/AMMDeleteTableDetail.test.tsx @@ -53,9 +53,7 @@ describe('AMMDelete: TableDetail', () => { expect(container.querySelector('[data-testid="asset2"]')).toHaveTextContent( 'Asset 2XMPT', ) - expect( - container.querySelector('[data-testid="asset2"] a'), - ).toHaveAttribute( + expect(container.querySelector('[data-testid="asset2"] a')).toHaveAttribute( 'href', '/mpt/000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', ) From ec33f9c33632dcd9418f61e937171cd48e4d2e04 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 13:13:55 -0700 Subject: [PATCH 10/27] minor: strongly typed tx parameter in Payment.parser --- .../shared/components/Transaction/Payment/parser.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/containers/shared/components/Transaction/Payment/parser.ts b/src/containers/shared/components/Transaction/Payment/parser.ts index 44bc5f82c..0422df879 100644 --- a/src/containers/shared/components/Transaction/Payment/parser.ts +++ b/src/containers/shared/components/Transaction/Payment/parser.ts @@ -1,4 +1,4 @@ -// import type { Payment } from 'xrpl' +import type { Payment } from 'xrpl' import { formatAmount } from '../../../../../rippled/lib/txSummary/formatAmount' import { PaymentInstructions } from './types' import { Amount, ExplorerAmount } from '../../../types' @@ -10,8 +10,7 @@ const formatFailedPartialAmount = (d: Amount): ExplorerAmount => ({ export const isPartialPayment = (flags: any) => 0x00020000 & flags -// TODO: use MPTAmount type from xrpl.js -export const parser = (tx: any, meta: any): PaymentInstructions => { +export const parser = (tx: Payment, meta: any): PaymentInstructions => { const max = tx.SendMax ? formatAmount(tx.SendMax) : undefined const partial = !!isPartialPayment(tx.Flags) const failedPartial = partial && meta.TransactionResult !== 'tesSUCCESS' From 805e630c6510d1236eee3ec98009c203ea2892e2 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 13:20:52 -0700 Subject: [PATCH 11/27] minor: do not use shortened MPT-ID values, except in Vault component --- src/containers/Transactions/DetailTab/Meta/Offer.tsx | 4 ---- .../shared/components/Transaction/OfferCreate/Description.tsx | 4 ---- .../shared/components/Transaction/OfferCreate/Simple.tsx | 2 -- src/containers/shared/components/TxToken.tsx | 2 -- 4 files changed, 12 deletions(-) diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index 1894d2a3f..c318ef243 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -64,7 +64,6 @@ const renderChanges = ( issuer={getIssuer(final.TakerPays)} isMPT={paysIsMPT} displaySymbol={false} - shortenMPTIssuanceID />
{' '} @@ -105,7 +104,6 @@ const renderChanges = ( issuer={getIssuer(final.TakerGets)} isMPT={getsIsMPT} displaySymbol={false} - shortenMPTIssuanceID />
{' '} @@ -230,7 +228,6 @@ const render: MetaRenderFunctionWithTx = ( isMPT={invert ? getsIsMPT : paysIsMPT} displaySymbol={false} shortenIssuer - shortenMPTIssuanceID /> ), Currency2: ( @@ -242,7 +239,6 @@ const render: MetaRenderFunctionWithTx = ( isMPT={invert ? paysIsMPT : getsIsMPT} displaySymbol={false} shortenIssuer - shortenMPTIssuanceID /> ), Account: , diff --git a/src/containers/shared/components/Transaction/OfferCreate/Description.tsx b/src/containers/shared/components/Transaction/OfferCreate/Description.tsx index 3c955cd80..1aace8c6e 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/Description.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/Description.tsx @@ -51,7 +51,6 @@ const Description: TransactionDescriptionComponent = ( issuer={getsAsset.issuer} isMPT={getsAsset.isMPT} displaySymbol={false} - shortenMPTIssuanceID /> / ) @@ -71,7 +69,6 @@ const Description: TransactionDescriptionComponent = ( issuer={paysAsset.issuer} isMPT={paysAsset.isMPT} displaySymbol={false} - shortenMPTIssuanceID /> / ) diff --git a/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx b/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx index 8ea005920..26ef3096a 100644 --- a/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/OfferCreate/Simple.tsx @@ -21,7 +21,6 @@ const Simple: TransactionSimpleComponent = (props: TransactionSimpleProps) => { issuer={firstCurrency.issuer} isMPT={firstCurrency.isMPT} shortenIssuer - shortenMPTIssuanceID /> / { issuer={secondCurrency.issuer} isMPT={secondCurrency.isMPT} shortenIssuer - shortenMPTIssuanceID /> diff --git a/src/containers/shared/components/TxToken.tsx b/src/containers/shared/components/TxToken.tsx index c485ede31..0fefb3822 100644 --- a/src/containers/shared/components/TxToken.tsx +++ b/src/containers/shared/components/TxToken.tsx @@ -25,7 +25,6 @@ function getTokenPair( ) : undefined const second = @@ -33,7 +32,6 @@ function getTokenPair( ) : undefined From 7498ce00c92cb19d7d0839d0c51ba68dd9248aaf Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 15:17:57 -0700 Subject: [PATCH 12/27] fix: Ensure IOU and MPT Currencies are displayed in Green inside Transaction-Simple view page --- src/containers/Transactions/simpleTab.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/containers/Transactions/simpleTab.scss b/src/containers/Transactions/simpleTab.scss index 9f2681031..938f5da2e 100644 --- a/src/containers/Transactions/simpleTab.scss +++ b/src/containers/Transactions/simpleTab.scss @@ -74,6 +74,14 @@ $subdued-color: $black-40; } } + a.currency { + color: $green-30; + + &:hover { + color: $green-50; + } + } + &.list { margin-bottom: 12px; From 72a44ea9de5f9646ed369107729bdb0afaec4ece Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 24 Mar 2026 15:20:38 -0700 Subject: [PATCH 13/27] [trivial] linter complaints --- src/containers/shared/components/TxToken.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/containers/shared/components/TxToken.tsx b/src/containers/shared/components/TxToken.tsx index 0fefb3822..b9b36de18 100644 --- a/src/containers/shared/components/TxToken.tsx +++ b/src/containers/shared/components/TxToken.tsx @@ -22,17 +22,11 @@ function getTokenPair( ) { const first = amount?.amount && amount.amount !== fee ? ( - + ) : undefined const second = amount2?.amount && amount2.amount !== fee ? ( - + ) : undefined if (first && second) { From a3641a862783c988ce8633318d3bcf8b454d0042 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 14 Apr 2026 12:53:53 -0700 Subject: [PATCH 14/27] minor: refactor commonly used magic numbers/hashes --- .../components/Transaction/DefaultSimple.tsx | 9 +++++++-- src/containers/shared/test/metaParser.test.ts | 14 +++++++------ .../lib/txSummary/formatAmount.test.ts | 20 ++++++++++--------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/containers/shared/components/Transaction/DefaultSimple.tsx b/src/containers/shared/components/Transaction/DefaultSimple.tsx index 002f0c361..ef1159519 100644 --- a/src/containers/shared/components/Transaction/DefaultSimple.tsx +++ b/src/containers/shared/components/Transaction/DefaultSimple.tsx @@ -27,6 +27,11 @@ const DEFAULT_TX_ELEMENTS = [ 'warnings', ] +// Currency objects have at most 2 keys: `currency` and optionally `issuer` +const MAX_CURRENCY_OBJECT_KEYS = 2 +// Amount objects have exactly 3 keys: `currency`, `issuer`, and `value` +const AMOUNT_OBJECT_KEY_COUNT = 3 + const displayKey = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1 $2') const isMPTAsset = (value: any) => @@ -42,7 +47,7 @@ const isMPTAmount = (value: any) => const isCurrency = (value: any) => isMPTAsset(value) || (typeof value === 'object' && - Object.keys(value).length <= 2 && + Object.keys(value).length <= MAX_CURRENCY_OBJECT_KEYS && (value.issuer == null || typeof value.issuer === 'string') && typeof value.currency === 'string') @@ -50,7 +55,7 @@ const isAmount = (amount: any, key: any = null) => key === 'Amount' || isMPTAmount(amount) || (typeof amount === 'object' && - Object.keys(amount).length === 3 && + Object.keys(amount).length === AMOUNT_OBJECT_KEY_COUNT && typeof amount.issuer === 'string' && typeof amount.currency === 'string' && typeof amount.value === 'string') diff --git a/src/containers/shared/test/metaParser.test.ts b/src/containers/shared/test/metaParser.test.ts index 5fc41cef0..82764959b 100644 --- a/src/containers/shared/test/metaParser.test.ts +++ b/src/containers/shared/test/metaParser.test.ts @@ -1,5 +1,7 @@ import { findAssetAmount, findNodes, LedgerEntryTypes } from '../metaParser' +const TEST_MPT_ID = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + describe('findAssetAmount', () => { const baseTx = { Account: 'rTestAccount', Fee: '12' } as any @@ -73,7 +75,7 @@ describe('findAssetAmount', () => { FinalFields: { Account: 'rTestAccount', MPTokenIssuanceID: - '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + TEST_MPT_ID, MPTAmount: '990000', }, PreviousFields: { @@ -86,13 +88,13 @@ describe('findAssetAmount', () => { const result = findAssetAmount( meta, { - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, }, baseTx, ) expect(result).toBeDefined() expect(result!.currency).toBe( - '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + TEST_MPT_ID, ) expect(result!.amount).toBe(10000) expect(result!.isMPT).toBe(true) @@ -117,7 +119,7 @@ describe('findAssetAmount', () => { const result = findAssetAmount( meta, { - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, }, baseTx, ) @@ -134,7 +136,7 @@ describe('findAssetAmount', () => { NewFields: { Account: 'rTestAccount', MPTokenIssuanceID: - '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + TEST_MPT_ID, MPTAmount: '5000', }, }, @@ -144,7 +146,7 @@ describe('findAssetAmount', () => { const result = findAssetAmount( meta, { - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, }, baseTx, ) diff --git a/src/rippled/lib/txSummary/formatAmount.test.ts b/src/rippled/lib/txSummary/formatAmount.test.ts index ee58c7189..b85cf050a 100644 --- a/src/rippled/lib/txSummary/formatAmount.test.ts +++ b/src/rippled/lib/txSummary/formatAmount.test.ts @@ -5,6 +5,8 @@ import { formatAmountWithAsset, } from './formatAmount' +const TEST_MPT_ID = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + describe('formatAmount', () => { it('formats XRP string amount', () => { const result = formatAmount('24755081083') @@ -26,11 +28,11 @@ describe('formatAmount', () => { it('formats MPTAmount', () => { const result = formatAmount({ - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, value: '1000', }) expect(result).toEqual({ - currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + currency: TEST_MPT_ID, amount: '1000', isMPT: true, }) @@ -46,7 +48,7 @@ describe('isMPTAmount', () => { it('returns true for MPTAmount', () => { expect( isMPTAmount({ - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, value: '100', }), ).toBe(true) @@ -69,7 +71,7 @@ describe('isMPTAmount', () => { it('returns false for MPT asset without value', () => { expect( isMPTAmount({ - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, } as any), ).toBe(false) }) @@ -89,11 +91,11 @@ describe('formatAsset', () => { it('formats MPT asset', () => { const result = formatAsset({ - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + mpt_issuance_id: TEST_MPT_ID, }) expect(result).toEqual({ - currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', - mpt_issuance_id: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + currency: TEST_MPT_ID, + mpt_issuance_id: TEST_MPT_ID, isMPT: true, }) }) @@ -109,11 +111,11 @@ describe('formatAmountWithAsset', () => { it('formats MPT amount with asset', () => { const result = formatAmountWithAsset('500', { - currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + currency: TEST_MPT_ID, isMPT: true, }) expect(result).toEqual({ - currency: '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F', + currency: TEST_MPT_ID, amount: 500, isMPT: true, }) From a8f1f71c82bc36d64f0d2d8bc929529b3f37c875 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 13:50:19 -0700 Subject: [PATCH 15/27] use Amount type instead of any type --- .../Transactions/DetailTab/Meta/Offer.tsx | 23 +++++++++++++------ src/containers/shared/test/metaParser.test.ts | 10 +++----- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index c318ef243..6cff2d9bb 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -8,18 +8,27 @@ import { import { localizeNumber } from '../../../shared/utils' import { Account } from '../../../shared/components/Account' import Currency from '../../../shared/components/Currency' +import type { Amount } from '../../../shared/types' import type { MetaRenderFunctionWithTx, MetaNode } from './types' -const getCurrency = (takerAmount: any): string => { - if (takerAmount?.mpt_issuance_id) return takerAmount.mpt_issuance_id - return takerAmount?.currency || 'XRP' +const isMPTOrIOUAmount = ( + takerAmount: Amount | undefined, +): takerAmount is Exclude => + typeof takerAmount === 'object' && takerAmount !== null + +const getCurrency = (takerAmount: Amount | undefined): string => { + if (!isMPTOrIOUAmount(takerAmount)) return 'XRP' + if ('mpt_issuance_id' in takerAmount) return takerAmount.mpt_issuance_id + return takerAmount.currency || 'XRP' } -const getIsMPT = (takerAmount: any): boolean => !!takerAmount?.mpt_issuance_id +const getIsMPT = (takerAmount: Amount | undefined): boolean => + isMPTOrIOUAmount(takerAmount) && 'mpt_issuance_id' in takerAmount -const getIssuer = (takerAmount: any): string | undefined => { - if (takerAmount?.mpt_issuance_id) return undefined - return takerAmount?.issuer +const getIssuer = (takerAmount: Amount | undefined): string | undefined => { + if (!isMPTOrIOUAmount(takerAmount)) return undefined + if ('mpt_issuance_id' in takerAmount) return undefined + return takerAmount.issuer } const normalize = ( diff --git a/src/containers/shared/test/metaParser.test.ts b/src/containers/shared/test/metaParser.test.ts index 82764959b..be8c9ebb0 100644 --- a/src/containers/shared/test/metaParser.test.ts +++ b/src/containers/shared/test/metaParser.test.ts @@ -74,8 +74,7 @@ describe('findAssetAmount', () => { LedgerIndex: 'GHI789', FinalFields: { Account: 'rTestAccount', - MPTokenIssuanceID: - TEST_MPT_ID, + MPTokenIssuanceID: TEST_MPT_ID, MPTAmount: '990000', }, PreviousFields: { @@ -93,9 +92,7 @@ describe('findAssetAmount', () => { baseTx, ) expect(result).toBeDefined() - expect(result!.currency).toBe( - TEST_MPT_ID, - ) + expect(result!.currency).toBe(TEST_MPT_ID) expect(result!.amount).toBe(10000) expect(result!.isMPT).toBe(true) }) @@ -135,8 +132,7 @@ describe('findAssetAmount', () => { LedgerIndex: 'JKL012', NewFields: { Account: 'rTestAccount', - MPTokenIssuanceID: - TEST_MPT_ID, + MPTokenIssuanceID: TEST_MPT_ID, MPTAmount: '5000', }, }, From ad688b6f58df965be0558cdd647d04ce93c24673 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:03:45 -0700 Subject: [PATCH 16/27] test: unit test file for useMPTIssuance hook --- .../shared/hooks/test/useMPTIssuance.test.tsx | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/containers/shared/hooks/test/useMPTIssuance.test.tsx diff --git a/src/containers/shared/hooks/test/useMPTIssuance.test.tsx b/src/containers/shared/hooks/test/useMPTIssuance.test.tsx new file mode 100644 index 000000000..6c9eb5c25 --- /dev/null +++ b/src/containers/shared/hooks/test/useMPTIssuance.test.tsx @@ -0,0 +1,111 @@ +import { renderHook } from '@testing-library/react-hooks' +import { waitFor } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from 'react-query' +import SocketContext from '../../SocketContext' +import { useMPTIssuance } from '../useMPTIssuance' +import { getMPTIssuance } from '../../../../rippled/lib/rippled' +import { formatMPTIssuance } from '../../../../rippled/lib/utils' +import { useAnalytics } from '../../analytics' +import { FormattedMPTIssuance } from '../../Interfaces' + +jest.mock('../../../../rippled/lib/rippled', () => ({ + getMPTIssuance: jest.fn(), +})) +jest.mock('../../../../rippled/lib/utils', () => ({ + formatMPTIssuance: jest.fn(), +})) +jest.mock('../../analytics', () => ({ + useAnalytics: jest.fn(), +})) + +const mockGetMPTIssuance = getMPTIssuance as jest.Mock +const mockFormatMPTIssuance = formatMPTIssuance as jest.Mock +const mockUseAnalytics = useAnalytics as jest.Mock +const mockSocket = {} as any + +const MPT_ID = '00000001ABC123' + +const formatted: FormattedMPTIssuance = { + issuer: 'rIssuer', + sequence: 1, + isMPTMetadataCompliant: true, +} + +const createWrapper = + ( + queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 0 } }, + }), + ) => + ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + +describe('useMPTIssuance', () => { + const trackException = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + mockUseAnalytics.mockReturnValue({ trackException }) + mockFormatMPTIssuance.mockReturnValue(formatted) + }) + + it('fetches and returns formatted MPT issuance data', async () => { + mockGetMPTIssuance.mockResolvedValueOnce({ node: { foo: 'bar' } }) + + const { result } = renderHook(() => useMPTIssuance(MPT_ID), { + wrapper: createWrapper(), + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(mockGetMPTIssuance).toHaveBeenCalledWith(mockSocket, MPT_ID) + expect(mockFormatMPTIssuance).toHaveBeenCalledWith({ foo: 'bar' }) + expect(result.current.data).toEqual(formatted) + }) + + it('does not fire the query when mptID is null', () => { + const { result } = renderHook(() => useMPTIssuance(null), { + wrapper: createWrapper(), + }) + + expect(mockGetMPTIssuance).not.toHaveBeenCalled() + expect(result.current.data).toBeUndefined() + }) + + it('does not fire the query when mptID is undefined', () => { + const { result } = renderHook(() => useMPTIssuance(undefined), { + wrapper: createWrapper(), + }) + + expect(mockGetMPTIssuance).not.toHaveBeenCalled() + expect(result.current.data).toBeUndefined() + }) + + it('does not fire the query when enabled is false, even with a valid mptID', () => { + const { result } = renderHook(() => useMPTIssuance(MPT_ID, false), { + wrapper: createWrapper(), + }) + + expect(mockGetMPTIssuance).not.toHaveBeenCalled() + expect(result.current.data).toBeUndefined() + }) + + it('calls trackException when the query fails', async () => { + mockGetMPTIssuance.mockRejectedValueOnce(new Error('boom')) + + const { result } = renderHook(() => useMPTIssuance(MPT_ID), { + wrapper: createWrapper(), + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(trackException).toHaveBeenCalledTimes(1) + const message = trackException.mock.calls[0][0] as string + expect(message).toContain(`Error fetching mptIssuanceID metadata ${MPT_ID}`) + }) +}) From 416846f97df4998869ff7c96b0a3870a48743b1c Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:14:11 -0700 Subject: [PATCH 17/27] minor: add unit tests for isMPTAsset and isMPTAmount methods --- .../components/Transaction/DefaultSimple.tsx | 4 +- .../Transaction/test/DefaultSimple.test.tsx | 65 ++++++++++++++++++- .../test/mock_data/MPTTokenSwapPropose.json | 29 +++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/containers/shared/components/Transaction/test/mock_data/MPTTokenSwapPropose.json diff --git a/src/containers/shared/components/Transaction/DefaultSimple.tsx b/src/containers/shared/components/Transaction/DefaultSimple.tsx index ef1159519..a1d3d21ea 100644 --- a/src/containers/shared/components/Transaction/DefaultSimple.tsx +++ b/src/containers/shared/components/Transaction/DefaultSimple.tsx @@ -34,12 +34,12 @@ const AMOUNT_OBJECT_KEY_COUNT = 3 const displayKey = (key: string) => key.replace(/([a-z])([A-Z])/g, '$1 $2') -const isMPTAsset = (value: any) => +export const isMPTAsset = (value: any) => typeof value === 'object' && typeof value.mpt_issuance_id === 'string' && !value.value -const isMPTAmount = (value: any) => +export const isMPTAmount = (value: any) => typeof value === 'object' && typeof value.mpt_issuance_id === 'string' && typeof value.value === 'string' diff --git a/src/containers/shared/components/Transaction/test/DefaultSimple.test.tsx b/src/containers/shared/components/Transaction/test/DefaultSimple.test.tsx index 4027dab8c..39c71ea89 100644 --- a/src/containers/shared/components/Transaction/test/DefaultSimple.test.tsx +++ b/src/containers/shared/components/Transaction/test/DefaultSimple.test.tsx @@ -1,12 +1,18 @@ +import MPTTokenSwapPropose from './mock_data/MPTTokenSwapPropose.json' import NewEscrowCreate from './mock_data/NewEscrowCreate.json' import SetHook from './mock_data/SetHook.json' import SetHook2 from './mock_data/SetHook2.json' import TokenSwapPropose from './mock_data/TokenSwapPropose.json' -import { DefaultSimple } from '../DefaultSimple' +import { DefaultSimple, isMPTAsset, isMPTAmount } from '../DefaultSimple' import { renderWithProviders } from './createWrapperFactory' import { expectSimpleRowText } from './expectations' import summarizeTransaction from '../../../../../rippled/lib/txSummary' +jest.mock('../../../hooks/useMPTIssuance', () => ({ + ...jest.requireActual('../../../hooks/useMPTIssuance'), + useMPTIssuance: () => ({ data: undefined }), +})) + function renderComponent(tx: { tx: any; meta: any }) { // eslint-disable-next-line no-param-reassign -- needed so parsers aren't triggered tx.tx.TransactionType = 'DummyTx' @@ -145,4 +151,61 @@ describe('DefaultSimple', () => { '$33.00 USD.rnz5f1MFcgbVxzYhU5hUKbKquEJHJady5K', ) }) + + it('renders Currency with isMPT=true when given an MPT asset', () => { + const MPT_ID = '00000BDE5B4F868ECE457207E2C1750065987730B8839E0D' + const { container } = renderComponent(MPTTokenSwapPropose) + + // When isMPT=true, Currency renders a RouteLink to MPT_ROUTE (/mpt/:id). + // This is the behavioral proof that DefaultSimple passed isMPT=true. + const assetLink = container.querySelector( + `[data-testid="Asset"] a[href="/mpt/${MPT_ID}"]`, + ) + expect(assetLink).toBeInTheDocument() + }) +}) + +describe('isMPTAsset', () => { + it('returns true for an asset object with mpt_issuance_id and no value', () => { + expect(isMPTAsset({ mpt_issuance_id: 'ABC123' })).toBe(true) + }) + + it('returns false when value is present (that is an amount, not an asset)', () => { + expect(isMPTAsset({ mpt_issuance_id: 'ABC123', value: '10' })).toBe(false) + }) + + it('returns false when mpt_issuance_id is missing', () => { + expect(isMPTAsset({ currency: 'USD' })).toBe(false) + }) + + it('returns false when mpt_issuance_id is not a string', () => { + expect(isMPTAsset({ mpt_issuance_id: 123 })).toBe(false) + }) + + it('returns false for non-objects', () => { + expect(isMPTAsset('ABC123')).toBe(false) + expect(isMPTAsset(42)).toBe(false) + }) +}) + +describe('isMPTAmount', () => { + it('returns true when both mpt_issuance_id and value are strings', () => { + expect(isMPTAmount({ mpt_issuance_id: 'ABC123', value: '10' })).toBe(true) + }) + + it('returns false when value is missing (that is an asset, not an amount)', () => { + expect(isMPTAmount({ mpt_issuance_id: 'ABC123' })).toBe(false) + }) + + it('returns false when value is not a string', () => { + expect(isMPTAmount({ mpt_issuance_id: 'ABC123', value: 10 })).toBe(false) + }) + + it('returns false when mpt_issuance_id is missing', () => { + expect(isMPTAmount({ currency: 'USD', value: '10' })).toBe(false) + }) + + it('returns false for non-objects', () => { + expect(isMPTAmount('ABC123')).toBe(false) + }) }) diff --git a/src/containers/shared/components/Transaction/test/mock_data/MPTTokenSwapPropose.json b/src/containers/shared/components/Transaction/test/mock_data/MPTTokenSwapPropose.json new file mode 100644 index 000000000..85a9cf834 --- /dev/null +++ b/src/containers/shared/components/Transaction/test/mock_data/MPTTokenSwapPropose.json @@ -0,0 +1,29 @@ +{ + "tx": { + "Account": "ratB3Rp7pcid4hzwSYTWiqWXYXFmWUFDv1", + "AccountOther": "rPTScb8m3wq6r3Ys93Ec5at7LYDmWrtndi", + "Amount": { + "mpt_issuance_id": "00000BDE5B4F868ECE457207E2C1750065987730B8839E0D", + "value": "42" + }, + "Asset": { + "mpt_issuance_id": "00000BDE5B4F868ECE457207E2C1750065987730B8839E0D" + }, + "Fee": "10", + "Flags": 2147483648, + "Sequence": 5, + "SigningPubKey": "ED2D7565F8E1432940E5B3BEB4562DE99DB880323B5C8A178376D5B481A7DB6E1C", + "TransactionType": "TokenSwapPropose", + "TxnSignature": "153E5473EB70B6E073F22A8281AE1E5593C96369CACDC1E2DB9FA54C5B3EC7C07F458061858DD7AE061198E7CB8BF56A7034F102016DFDB5F38EF4C28466B308", + "ctid": "C000005800000000", + "date": 749136066 + }, + "meta": { + "AffectedNodes": [], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "hash": "F84CCA4C6948B0D068A2D97A8CA92F557CE3615D98F6E90540369D4A4F122D0E", + "inLedger": 88, + "ledger_index": 88 +} From d14cd921cd3154269248633dd133abd63b460675 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:23:27 -0700 Subject: [PATCH 18/27] revert unnecessary changes to transactionUtils file --- src/containers/shared/transactionUtils.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts index 1879c3835..6f3cab9eb 100644 --- a/src/containers/shared/transactionUtils.ts +++ b/src/containers/shared/transactionUtils.ts @@ -274,18 +274,9 @@ export function zeroPad( } export function normalizeAmount( - amount: - | IssuedCurrencyAmount - | number - | string - | { mpt_issuance_id: string; value: string }, + amount: IssuedCurrencyAmount | number | string, language = 'en-US', ): string | null { - if (typeof amount === 'object' && 'mpt_issuance_id' in amount) { - const currency = amount.mpt_issuance_id - const numberOption = { ...CURRENCY_OPTIONS, currency } - return localizeNumber(amount.value, language, numberOption) - } const currency = typeof amount === 'object' ? amount.currency : 'XRP' const value = typeof amount === 'object' ? amount.value : Number(amount) / XRP_BASE From 599e3bcb2444f578e1abbdae5dc9c0f220891d49 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:28:39 -0700 Subject: [PATCH 19/27] minor: add unit test for formatAsset MPT case --- src/containers/shared/test/utils.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/containers/shared/test/utils.test.ts b/src/containers/shared/test/utils.test.ts index 1227abc40..e15c959af 100644 --- a/src/containers/shared/test/utils.test.ts +++ b/src/containers/shared/test/utils.test.ts @@ -205,6 +205,14 @@ describe('AMM utils format asset', () => { expect(formatted).toEqual({ currency: 'USD', issuer: 'your mom' }) }) + + it('formats MPT asset', () => { + const mptId = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + const asset = { mpt_issuance_id: mptId } + const formatted = formatAsset(asset) + + expect(formatted).toEqual({ currency: mptId, isMPT: true }) + }) }) describe('Shorten utils', () => { From c82d0c321454702e28bc5049b462759dbe8502b3 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:54:24 -0700 Subject: [PATCH 20/27] feat: Use BigInt type to handle potentially large MPTAmount values --- src/containers/shared/metaParser.tsx | 17 +- src/containers/shared/test/metaParser.test.ts | 187 +++++++++++++++++- 2 files changed, 195 insertions(+), 9 deletions(-) diff --git a/src/containers/shared/metaParser.tsx b/src/containers/shared/metaParser.tsx index 09a6711bd..946ccd0e3 100644 --- a/src/containers/shared/metaParser.tsx +++ b/src/containers/shared/metaParser.tsx @@ -108,15 +108,18 @@ function findMPTAmount( if (mptNodes.length === 0) return undefined const node = mptNodes[0] - const amount = + // MPTAmount can be up to 2^63 - 1, beyond Number.MAX_SAFE_INTEGER, + // so parse and subtract with BigInt to preserve precision. + const delta = node.FinalFields?.MPTAmount != null - ? Math.abs( - Number(node.FinalFields.MPTAmount) - - Number(node.PreviousFields?.MPTAmount ?? 0), - ) - : Number(node.NewFields?.MPTAmount ?? 0) + ? BigInt(node.FinalFields.MPTAmount) - + BigInt(node.PreviousFields?.MPTAmount ?? 0) + : BigInt(node.NewFields?.MPTAmount ?? 0) + const amount = (delta < 0n ? -delta : delta).toString() - return amount ? { currency: mptIssuanceId, amount, isMPT: true } : undefined + return amount !== '0' + ? { currency: mptIssuanceId, amount, isMPT: true } + : undefined } function findXRPAmount( diff --git a/src/containers/shared/test/metaParser.test.ts b/src/containers/shared/test/metaParser.test.ts index be8c9ebb0..c4aa3aa66 100644 --- a/src/containers/shared/test/metaParser.test.ts +++ b/src/containers/shared/test/metaParser.test.ts @@ -93,7 +93,7 @@ describe('findAssetAmount', () => { ) expect(result).toBeDefined() expect(result!.currency).toBe(TEST_MPT_ID) - expect(result!.amount).toBe(10000) + expect(result!.amount).toBe('10000') expect(result!.isMPT).toBe(true) }) @@ -147,9 +147,192 @@ describe('findAssetAmount', () => { baseTx, ) expect(result).toBeDefined() - expect(result!.amount).toBe(5000) + expect(result!.amount).toBe('5000') + expect(result!.isMPT).toBe(true) + }) + + it('returns the absolute value when MPTAmount increased', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'INC1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + MPTAmount: '1500', + }, + PreviousFields: { + MPTAmount: '1000', + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.amount).toBe('500') expect(result!.isMPT).toBe(true) }) + + it('returns undefined when MPTAmount did not change (zero delta)', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'ZERO1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + MPTAmount: '1000', + }, + PreviousFields: { + MPTAmount: '1000', + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeUndefined() + }) + + it('treats missing PreviousFields.MPTAmount as zero', () => { + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'NOPREV1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + MPTAmount: '250', + }, + PreviousFields: {}, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.amount).toBe('250') + }) + + it('picks only MPToken nodes matching the requested issuance id', () => { + const OTHER_MPT_ID = '000099999999999999999999999999999999999999999999' + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'OTHER1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: OTHER_MPT_ID, + MPTAmount: '777', + }, + PreviousFields: { + MPTAmount: '0', + }, + }, + }, + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'TARGET1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + MPTAmount: '300', + }, + PreviousFields: { + MPTAmount: '100', + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.currency).toBe(TEST_MPT_ID) + expect(result!.amount).toBe('200') + }) + + it('returns undefined for a newly created MPToken with no MPTAmount', () => { + const meta = { + AffectedNodes: [ + { + CreatedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'NEW_NOAMT', + NewFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeUndefined() + }) + + it('preserves precision for MPT amounts beyond Number.MAX_SAFE_INTEGER', () => { + // MPTAmount can be up to 2^63 - 1 = 9223372036854775807, + // which exceeds Number.MAX_SAFE_INTEGER (2^53 - 1 = 9007199254740991). + const finalAmount = '9223372036854775807' // 2^63 - 1 + const previousAmount = '9223372036854775000' + const expectedDelta = '807' + + const meta = { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'LARGE1', + FinalFields: { + Account: 'rTestAccount', + MPTokenIssuanceID: TEST_MPT_ID, + MPTAmount: finalAmount, + }, + PreviousFields: { + MPTAmount: previousAmount, + }, + }, + }, + ], + } + const result = findAssetAmount( + meta, + { mpt_issuance_id: TEST_MPT_ID }, + baseTx, + ) + expect(result).toBeDefined() + expect(result!.amount).toBe(expectedDelta) + }) }) describe('findNodes', () => { From d20a0c4ce845ca9cf6ab86e61a5e028c27fa6b22 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 14:59:13 -0700 Subject: [PATCH 21/27] feat: remove formatAsset second def in utils.js file --- src/containers/shared/test/utils.test.ts | 25 ------------------------ src/containers/shared/utils.js | 14 ------------- 2 files changed, 39 deletions(-) diff --git a/src/containers/shared/test/utils.test.ts b/src/containers/shared/test/utils.test.ts index e15c959af..1695c763d 100644 --- a/src/containers/shared/test/utils.test.ts +++ b/src/containers/shared/test/utils.test.ts @@ -7,7 +7,6 @@ import { localizeDate, durationToHuman, formatDurationDetailed, - formatAsset, shortenAccount, shortenDomain, shortenNFTTokenID, @@ -191,30 +190,6 @@ describe('utils', () => { }) }) -describe('AMM utils format asset', () => { - it('formats XRP asset', () => { - const asset = '10000000000' - const formatted = formatAsset(asset) - - expect(formatted).toEqual({ currency: 'XRP' }) - }) - - it('formats non XRP asset', () => { - const asset = { currency: 'USD', amount: '100000', issuer: 'your mom' } - const formatted = formatAsset(asset) - - expect(formatted).toEqual({ currency: 'USD', issuer: 'your mom' }) - }) - - it('formats MPT asset', () => { - const mptId = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' - const asset = { mpt_issuance_id: mptId } - const formatted = formatAsset(asset) - - expect(formatted).toEqual({ currency: mptId, isMPT: true }) - }) -}) - describe('Shorten utils', () => { describe('shortenAccount', () => { it('shortens long account addresses', () => { diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index 58b1d2268..56d6ac00a 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -436,20 +436,6 @@ export const formatDurationDetailed = (totalSeconds, maxUnits = 4) => { export const removeRoutes = (routes, ...routesToRemove) => routes.filter((route) => !routesToRemove.includes(route.title)) -export const formatAsset = (asset) => { - if (typeof asset === 'string') return { currency: 'XRP' } - if (asset.mpt_issuance_id) { - return { - currency: asset.mpt_issuance_id, - isMPT: true, - } - } - return { - currency: asset.currency, - issuer: asset.issuer, - } -} - // For AMM, the trading fee is in units of 1/100,000; a value of 1 is equivalent to a 0.001% fee. export const formatTradingFee = (tradingFee) => tradingFee !== undefined From 78c416489db74d3b1cd54f677769fb0fd86323ff Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Thu, 16 Apr 2026 15:12:28 -0700 Subject: [PATCH 22/27] minor: wrap tests with QueryClientProvider whenver Currency component is rendered --- .../InfoCards/test/MarketDataCard.test.tsx | 81 +++++++++++-------- .../AMMPool/test/AMMPoolHeader.test.tsx | 17 ++-- .../DexTradeTable/test/DexTradeTable.test.tsx | 13 ++- 3 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx b/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx index 553637b94..89c51c0a8 100644 --- a/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx +++ b/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx @@ -1,11 +1,16 @@ import { render, screen } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' import { MemoryRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from 'react-query' import i18n from '../../../../i18n/testConfig' import { MarketDataCard } from '../MarketDataCard' import { TooltipProvider } from '../../../shared/components/Tooltip' import { LOSAMMPoolData, FormattedBalance } from '../../types' +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 0 } }, +}) + interface RenderProps { losData?: LOSAMMPoolData balance1?: FormattedBalance | null @@ -44,18 +49,20 @@ const renderComponent = ({ lpTokenBalance = '1000000', }: RenderProps = {}) => render( - - - - - - - , + + + + + + + + + , ) describe('MarketDataCard', () => { @@ -129,18 +136,20 @@ describe('MarketDataCard', () => { it('does not render balance or LP rows when balances and LP are absent', () => { render( - - - - - - - , + + + + + + + + + , ) const labels = document.querySelectorAll('.info-card-label') const balanceLabels = Array.from(labels).filter((l) => @@ -152,17 +161,19 @@ describe('MarketDataCard', () => { it('renders only balances and LP tokens when losData is undefined', () => { const { container } = render( - - - - - - - , + + + + + + + + + , ) // LOS fields hidden expect(screen.queryByText('tvl')).not.toBeInTheDocument() diff --git a/src/containers/AMMPool/test/AMMPoolHeader.test.tsx b/src/containers/AMMPool/test/AMMPoolHeader.test.tsx index df613f6ce..850df8d64 100644 --- a/src/containers/AMMPool/test/AMMPoolHeader.test.tsx +++ b/src/containers/AMMPool/test/AMMPoolHeader.test.tsx @@ -1,20 +1,27 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { I18nextProvider } from 'react-i18next' +import { QueryClient, QueryClientProvider } from 'react-query' import i18n from '../../../i18n/testConfig' import { AMMPoolHeader } from '../AMMPoolHeader' import { FormattedBalance } from '../types' +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 0 } }, +}) + const renderComponent = ( asset1: FormattedBalance | null = null, asset2: FormattedBalance | null = null, ) => render( - - - - - , + + + + + + + , ) describe('AMMPoolHeader', () => { diff --git a/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx b/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx index 85f1cc167..1bb2ce8f1 100644 --- a/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx +++ b/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx @@ -1,6 +1,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' import { BrowserRouter as Router } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from 'react-query' import i18n from '../../../../../i18n/testConfigEnglish' import { DexTradeTable, DexTradeFormatted } from '../DexTradeTable' @@ -9,10 +10,16 @@ jest.mock('../../Amount', () => ({ Amount: ({ value }: any) =>
{value.amount}
, })) +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 0 } }, +}) + const TestWrapper = ({ children }: { children: React.ReactNode }) => ( - - {children} - + + + {children} + + ) const mockDexTrades: DexTradeFormatted[] = [ From 4a58b18cfb2e36752049c70aeda4dfd95ae6ce19 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Wed, 6 May 2026 10:10:46 -0700 Subject: [PATCH 23/27] fix: resolve CI failures from React 18 / react-router 7 migration - Update test imports from react-router-dom to react-router (deprecated post-v7) - Replace @testing-library/react-hooks with @testing-library/react (merged renderHook) - Add `as any` cast to interpolation objects in Offer.tsx, matching the pattern already applied to sibling `change:` blocks during the React 18 upgrade Co-Authored-By: Claude Opus 4.7 (1M context) --- .../InfoCards/test/MarketDataCard.test.tsx | 2 +- .../Transactions/DetailTab/Meta/Offer.tsx | 64 +++++++++++-------- .../DexTradeTable/test/DexTradeTable.test.tsx | 2 +- .../shared/hooks/test/useMPTIssuance.test.tsx | 3 +- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx b/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx index 89c51c0a8..9fc7c8224 100644 --- a/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx +++ b/src/containers/AMMPool/InfoCards/test/MarketDataCard.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' -import { MemoryRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router' import { QueryClient, QueryClientProvider } from 'react-query' import i18n from '../../../../i18n/testConfig' import { MarketDataCard } from '../MarketDataCard' diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index 1b1be4ba2..691ecf6c8 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -86,23 +86,27 @@ const renderChanges = (
from - {{ - previous: localizeNumber( - normalize(prevPays, paysCurrency, paysIsMPT), - language, - options, - ), - }} + { + { + previous: localizeNumber( + normalize(prevPays, paysCurrency, paysIsMPT), + language, + options, + ), + } as any + } to - {{ - final: localizeNumber( - normalize(finalPays, paysCurrency, paysIsMPT), - language, - options, - ), - }} + { + { + final: localizeNumber( + normalize(finalPays, paysCurrency, paysIsMPT), + language, + options, + ), + } as any + }
, @@ -132,23 +136,27 @@ const renderChanges = ( from - {{ - previous: localizeNumber( - normalize(prevGets, getsCurrency, getsIsMPT), - language, - options, - ), - }} + { + { + previous: localizeNumber( + normalize(prevGets, getsCurrency, getsIsMPT), + language, + options, + ), + } as any + } to - {{ - final: localizeNumber( - normalize(finalGets, getsCurrency, getsIsMPT), - language, - options, - ), - }} + { + { + final: localizeNumber( + normalize(finalGets, getsCurrency, getsIsMPT), + language, + options, + ), + } as any + } , diff --git a/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx b/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx index 1bb2ce8f1..6bd8a01aa 100644 --- a/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx +++ b/src/containers/shared/components/DexTradeTable/test/DexTradeTable.test.tsx @@ -1,6 +1,6 @@ import { render, screen, fireEvent } from '@testing-library/react' import { I18nextProvider } from 'react-i18next' -import { BrowserRouter as Router } from 'react-router-dom' +import { BrowserRouter as Router } from 'react-router' import { QueryClient, QueryClientProvider } from 'react-query' import i18n from '../../../../../i18n/testConfigEnglish' import { DexTradeTable, DexTradeFormatted } from '../DexTradeTable' diff --git a/src/containers/shared/hooks/test/useMPTIssuance.test.tsx b/src/containers/shared/hooks/test/useMPTIssuance.test.tsx index 6c9eb5c25..0b55e1c30 100644 --- a/src/containers/shared/hooks/test/useMPTIssuance.test.tsx +++ b/src/containers/shared/hooks/test/useMPTIssuance.test.tsx @@ -1,5 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks' -import { waitFor } from '@testing-library/react' +import { renderHook, waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider } from 'react-query' import SocketContext from '../../SocketContext' import { useMPTIssuance } from '../useMPTIssuance' From 67ba2045efedc7bc31496e46564f3f688195a686 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 12 May 2026 14:46:54 -0700 Subject: [PATCH 24/27] feat: Use BigInt for MPTAmount subtraction in Offer meta renderer Mirror the precision-preserving pattern already used in metaParser.tsx so large MPTAmount values (up to 2^63 - 1) are not truncated when computing the change between previous and final TakerPays / TakerGets. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Transactions/DetailTab/Meta/Offer.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index 691ecf6c8..c318fa701 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -42,6 +42,19 @@ const normalize = ( : String(value) } +// MPTAmount can be up to 2^63 - 1, beyond Number.MAX_SAFE_INTEGER, +// so subtract with BigInt to preserve precision. +const computeChange = ( + prevValue: number | string | undefined, + finalValue: number | string | undefined, + isMPT: boolean, +): number | string => { + if (isMPT && prevValue != null && finalValue != null) { + return (BigInt(prevValue) - BigInt(finalValue)).toString() + } + return Number(prevValue) - Number(finalValue) +} + const renderChanges = ( _t: any, language: string, @@ -59,8 +72,16 @@ const renderChanges = ( const finalGets = final.TakerGets.value || final.TakerGets const prevPays = prev?.TakerPays?.value || prev?.TakerPays const prevGets = prev?.TakerGets?.value || prev?.TakerGets - const changePays = normalize(prevPays - finalPays, paysCurrency, paysIsMPT) - const changeGets = normalize(prevGets - finalGets, getsCurrency, getsIsMPT) + const changePays = normalize( + computeChange(prevPays, finalPays, paysIsMPT), + paysCurrency, + paysIsMPT, + ) + const changeGets = normalize( + computeChange(prevGets, finalGets, getsIsMPT), + getsCurrency, + getsIsMPT, + ) if (prevPays && finalPays) { const options = { ...CURRENCY_OPTIONS, currency: paysCurrency } From 08e5675d224e763b7e003b65ed5c6df04bfb1177 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Tue, 12 May 2026 15:19:26 -0700 Subject: [PATCH 25/27] feat: Preserve MPTAmount precision end-to-end in Amount rendering MPTAmount values can be up to 2^63 - 1, beyond Number.MAX_SAFE_INTEGER. Three layers were silently truncating large MPT values: - Amount.tsx routed `amount` through parseInt before scaling, losing precision before convertScaledPrice could apply BigInt arithmetic. - formatAmountWithAsset parseFloat'd the value before reaching the MPT branch, so the returned `amount` was already lossy. - localizeNumber parseFloat'd its input as the first step, undoing any precision preserved upstream. Fix each layer to keep MPT amounts as exact integer/decimal strings and add a BigInt-backed grouping path in localizeNumber that activates only for isMPT string inputs. Non-MPT call sites are unaffected. Tests cover MPT amounts at the spec maximum (2^63 - 1) at both assetScale 0 and 6, and IOU amounts near the ripple-binary-codec limits. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/containers/shared/components/Amount.tsx | 7 +- .../shared/components/test/Amount.test.tsx | 55 +++++++++++++ src/containers/shared/test/utils.test.ts | 19 +++++ src/containers/shared/utils.js | 28 ++++++- .../lib/txSummary/formatAmount.test.ts | 79 ++++++++++++++++++- src/rippled/lib/txSummary/formatAmount.ts | 19 +++-- 6 files changed, 192 insertions(+), 15 deletions(-) diff --git a/src/containers/shared/components/Amount.tsx b/src/containers/shared/components/Amount.tsx index abd3a320b..4d789edad 100644 --- a/src/containers/shared/components/Amount.tsx +++ b/src/containers/shared/components/Amount.tsx @@ -72,10 +72,9 @@ export const Amount = ({ if (isMPT && typeof value !== 'string') { if (mptIssuanceData) { const scale = mptIssuanceData.assetScale ?? 0 - const scaledAmount = convertScaledPrice( - parseInt(amount as string, 10).toString(16), - scale, - ) + // MPTAmount can be up to 2^63 - 1, beyond Number.MAX_SAFE_INTEGER. + // Use BigInt so the integer value is not truncated before scaling. + const scaledAmount = convertScaledPrice(BigInt(amount as string), scale) return renderAmount(localizeNumber(scaledAmount, language, {}, true)) } diff --git a/src/containers/shared/components/test/Amount.test.tsx b/src/containers/shared/components/test/Amount.test.tsx index bbd5e36d1..0b97c3657 100644 --- a/src/containers/shared/components/test/Amount.test.tsx +++ b/src/containers/shared/components/test/Amount.test.tsx @@ -160,4 +160,59 @@ describe('Amount', () => { '1,043.001', ) }) + + it('preserves MPT precision for value at the spec maximum (2^63 - 1)', async () => { + // 2^63 - 1 scaled by 6 decimals is 9,223,372,036,854.775807. With the + // pre-BigInt parseInt path this overflowed to ~9.223372036854776e18 and + // rendered as "9,223,372,036,854.776" (last three digits lost). + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { + issuer: 'rL2LzUhsBJMqsaVCXVvzedPjePbjVzBCC', + assetScale: 6, + maxAmt: '9223372036854775807', + outstandingAmt: '9223372036854775807', + sequence: 1, + metadata: '', + flags: [], + }, + }) + + const value = { + amount: '9223372036854775807', + currency: '0000098F03B3BCE934EE8CAA1DF25A42032388361B9E5A65', + isMPT: true, + } + renderComponent() + + expect(screen.getByTestId('amount-localized')).toHaveTextContent( + '9,223,372,036,854.775807', + ) + }) + + it('preserves MPT precision when assetScale is 0', async () => { + ;(useMPTIssuance as jest.Mock).mockReturnValue({ + data: { + issuer: 'rL2LzUhsBJMqsaVCXVvzedPjePbjVzBCC', + assetScale: 0, + maxAmt: '9223372036854775807', + outstandingAmt: '9223372036854775807', + sequence: 1, + metadata: '', + flags: [], + }, + }) + + const value = { + amount: '9223372036854775807', + currency: '0000098F03B3BCE934EE8CAA1DF25A42032388361B9E5A65', + isMPT: true, + } + renderComponent() + + // With scale 0 the entire 2^63 - 1 must render as a clean grouped integer + // with no missing low-order digits. + expect(screen.getByTestId('amount-localized')).toHaveTextContent( + '9,223,372,036,854,775,807', + ) + }) }) diff --git a/src/containers/shared/test/utils.test.ts b/src/containers/shared/test/utils.test.ts index 1695c763d..b06a3e47d 100644 --- a/src/containers/shared/test/utils.test.ts +++ b/src/containers/shared/test/utils.test.ts @@ -93,6 +93,25 @@ describe('utils', () => { ).toEqual('12.233400') }) + it('localizeNumber preserves precision for MPT-flagged string values', () => { + // 2^63 - 1 — the max representable MPTAmount per the XRPL spec. Without + // the BigInt branch this would round to "9,223,372,036,854,776,000". + expect(localizeNumber('9223372036854775807', 'en-US', {}, true)).toEqual( + '9,223,372,036,854,775,807', + ) + // Scaled MPTAmount — integer part is formatted via BigInt, fractional + // digits are appended verbatim. + expect(localizeNumber('9223372036854.775807', 'en-US', {}, true)).toEqual( + '9,223,372,036,854.775807', + ) + // Negative MPT-shaped string (defensive — unusual in practice). + expect(localizeNumber('-9223372036854775807', 'en-US', {}, true)).toEqual( + '-9,223,372,036,854,775,807', + ) + // Small values still work through the same path. + expect(localizeNumber('1000', 'en-US', {}, true)).toEqual('1,000') + }) + it('formatPrice', () => { expect(formatPrice(22.35)).toEqual('$22.35') expect( diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index 56d6ac00a..68bb0b8fb 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -167,9 +167,35 @@ export const localizeNumber = ( options = {}, isMPT = false, ) => { - const number = Number.parseFloat(num) const config = { ...NUMBER_DEFAULT_OPTIONS, ...options } + // MPT amounts can exceed Number.MAX_SAFE_INTEGER (up to 2^63 - 1) and their + // scaled forms can keep many fractional digits. Group the integer part via + // BigInt and append the fraction so no digits are lost in Number.parseFloat. + if (isMPT && typeof num === 'string' && /^-?\d+(\.\d+)?$/.test(num)) { + const [intPart, rawFrac = ''] = num.split('.') + const minFrac = config.minimumFractionDigits ?? 0 + const maxFrac = config.maximumFractionDigits ?? rawFrac.length + let fracPart = rawFrac.slice(0, maxFrac) + // Trim trailing zeros down to minFrac so '0.100' renders as '0.1'. + let end = fracPart.length + while (end > minFrac && fracPart[end - 1] === '0') end -= 1 + fracPart = fracPart.slice(0, end) + if (fracPart.length < minFrac) fracPart = fracPart.padEnd(minFrac, '0') + + const sign = intPart.startsWith('-') ? '-' : '' + const absInt = sign ? intPart.slice(1) : intPart + const grouped = new Intl.NumberFormat(lang, { + ...config, + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(BigInt(absInt)) + return fracPart ? `${sign}${grouped}.${fracPart}` : `${sign}${grouped}` + } + + const number = Number.parseFloat(num) + if (Number.isNaN(number)) { return null } diff --git a/src/rippled/lib/txSummary/formatAmount.test.ts b/src/rippled/lib/txSummary/formatAmount.test.ts index b85cf050a..970f564d4 100644 --- a/src/rippled/lib/txSummary/formatAmount.test.ts +++ b/src/rippled/lib/txSummary/formatAmount.test.ts @@ -7,6 +7,14 @@ import { const TEST_MPT_ID = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' +// MPTAmount upper bound from the XRPL spec: 2^63 - 1. +const MAX_MPT_VALUE = '9223372036854775807' + +// IOU amounts (per ripple-binary-codec) are bounded by a 16-digit mantissa and +// an exponent in [-96, 80]. These are representative near-limit values. +const MAX_IOU_VALUE_STR = '9.999999999999999e+80' +const MIN_POSITIVE_IOU_VALUE_STR = '1e-96' + describe('formatAmount', () => { it('formats XRP string amount', () => { const result = formatAmount('24755081083') @@ -38,6 +46,45 @@ describe('formatAmount', () => { }) }) + it('preserves precision for MPTAmount at the spec maximum (2^63 - 1)', () => { + const result = formatAmount({ + mpt_issuance_id: TEST_MPT_ID, + value: MAX_MPT_VALUE, + }) + expect(result).toEqual({ + currency: TEST_MPT_ID, + amount: MAX_MPT_VALUE, + isMPT: true, + }) + // Confirm the digits survived round-trip and weren't coerced through Number. + expect(typeof result!.amount).toBe('string') + expect(BigInt(result!.amount as string)).toBe(BigInt(MAX_MPT_VALUE)) + }) + + it('formats IOU near the spec maximum value', () => { + const result = formatAmount({ + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: MAX_IOU_VALUE_STR, + }) + // IOU amounts intentionally go through Number; the 16-digit mantissa fits + // within JS Number's 15-17 significant-digit range, so the literal survives + // unchanged round-tripping through parseFloat. + expect(result!.currency).toBe('USD') + expect(result!.amount).toBe(parseFloat(MAX_IOU_VALUE_STR)) + expect(Number.isFinite(result!.amount as number)).toBe(true) + }) + + it('formats IOU at the spec minimum positive value', () => { + const result = formatAmount({ + currency: 'USD', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: MIN_POSITIVE_IOU_VALUE_STR, + }) + expect(result!.amount).toBe(parseFloat(MIN_POSITIVE_IOU_VALUE_STR)) + expect(result!.amount).toBeGreaterThan(0) + }) + it('handles null and undefined', () => { expect(formatAmount(null as any)).toBeNull() expect(formatAmount(undefined as any)).toBeUndefined() @@ -109,14 +156,42 @@ describe('formatAmountWithAsset', () => { }) }) - it('formats MPT amount with asset', () => { + it('formats MPT amount with asset (preserves string)', () => { const result = formatAmountWithAsset('500', { currency: TEST_MPT_ID, isMPT: true, }) expect(result).toEqual({ currency: TEST_MPT_ID, - amount: 500, + amount: '500', + isMPT: true, + }) + }) + + it('preserves MPT precision for value at the spec maximum (2^63 - 1)', () => { + const result = formatAmountWithAsset(MAX_MPT_VALUE, { + currency: TEST_MPT_ID, + isMPT: true, + }) + expect(result).toEqual({ + currency: TEST_MPT_ID, + amount: MAX_MPT_VALUE, + isMPT: true, + }) + expect(BigInt(result!.amount as string)).toBe(BigInt(MAX_MPT_VALUE)) + }) + + it('preserves MPT precision when raw amount is given as a numeric input', () => { + // Some upstream callers may pass `amount` as a number for small values. + // We still want the result to be a string so the downstream Amount + // component handles it via BigInt without coercion. + const result = formatAmountWithAsset(1234567890, { + currency: TEST_MPT_ID, + isMPT: true, + }) + expect(result).toEqual({ + currency: TEST_MPT_ID, + amount: '1234567890', isMPT: true, }) }) diff --git a/src/rippled/lib/txSummary/formatAmount.ts b/src/rippled/lib/txSummary/formatAmount.ts index 2839a3c7b..05fecc324 100644 --- a/src/rippled/lib/txSummary/formatAmount.ts +++ b/src/rippled/lib/txSummary/formatAmount.ts @@ -84,6 +84,17 @@ export function formatAmountWithAsset(amount: string | number, asset: Asset) { return undefined } + // MPTAmount values can be up to 2^63 - 1, beyond Number.MAX_SAFE_INTEGER. + // Keep them as strings so downstream consumers (e.g. via BigInt) + // can preserve precision. + if (asset.isMPT) { + return { + currency: asset.currency, + amount: typeof amount === 'string' ? amount : String(amount), + isMPT: true, + } + } + const numericAmount = typeof amount === 'string' ? parseFloat(amount) : amount if (asset.currency === 'XRP') { @@ -93,14 +104,6 @@ export function formatAmountWithAsset(amount: string | number, asset: Asset) { } } - if (asset.isMPT) { - return { - currency: asset.currency, - amount: numericAmount, - isMPT: true, - } - } - return { currency: asset.currency, issuer: asset.issuer!, From 2a15173a2839ff9c070cf79560925ae2b2c1393a Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 18 May 2026 11:21:00 -0700 Subject: [PATCH 26/27] feat: Forward isMPT flag to localizeNumber in Offer meta renderer Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Transactions/DetailTab/Meta/Offer.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/containers/Transactions/DetailTab/Meta/Offer.tsx b/src/containers/Transactions/DetailTab/Meta/Offer.tsx index c318fa701..b3134b27b 100644 --- a/src/containers/Transactions/DetailTab/Meta/Offer.tsx +++ b/src/containers/Transactions/DetailTab/Meta/Offer.tsx @@ -101,7 +101,12 @@ const renderChanges = ( { { - change: localizeNumber(changePays, language, options), + change: localizeNumber( + changePays, + language, + options, + paysIsMPT, + ), } as any } @@ -113,6 +118,7 @@ const renderChanges = ( normalize(prevPays, paysCurrency, paysIsMPT), language, options, + paysIsMPT, ), } as any } @@ -125,6 +131,7 @@ const renderChanges = ( normalize(finalPays, paysCurrency, paysIsMPT), language, options, + paysIsMPT, ), } as any } @@ -151,7 +158,12 @@ const renderChanges = ( { { - change: localizeNumber(changeGets, language, options), + change: localizeNumber( + changeGets, + language, + options, + getsIsMPT, + ), } as any } @@ -163,6 +175,7 @@ const renderChanges = ( normalize(prevGets, getsCurrency, getsIsMPT), language, options, + getsIsMPT, ), } as any } @@ -175,6 +188,7 @@ const renderChanges = ( normalize(finalGets, getsCurrency, getsIsMPT), language, options, + getsIsMPT, ), } as any } From b02b7c62ba96e7ffadb605d27f4ba657a9dd0971 Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S Date: Mon, 18 May 2026 11:43:49 -0700 Subject: [PATCH 27/27] fix: Strip currency from BigInt branch in localizeNumber for MPT amounts Intl.NumberFormat validates `currency` even when `style: 'decimal'`, so the BigInt branch threw on mpt_issuance_id values (48-hex, not ISO). Adds regression tests on the Offer meta renderer covering all 6 localizeNumber calls plus mixed MPT/XRP offers and MPToken / MPTokenIssuance renderers at full precision. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Transactions/test/Meta.test.tsx | 143 ++++++++++++++++++ src/containers/shared/utils.js | 11 +- 2 files changed, 152 insertions(+), 2 deletions(-) diff --git a/src/containers/Transactions/test/Meta.test.tsx b/src/containers/Transactions/test/Meta.test.tsx index 257f7a4d9..827cb5a7f 100644 --- a/src/containers/Transactions/test/Meta.test.tsx +++ b/src/containers/Transactions/test/Meta.test.tsx @@ -185,4 +185,147 @@ describe('TransactionMeta container', () => { 'Domain: 4A4879496CFF23CA32242D50DA04DDB41F4561167276A62AF21899F83DF28812', ) }) + + // Regression: each localizeNumber call in Offer.tsx needs its isMPT flag + // forwarded so the BigInt branch runs. Without it parseFloat rounds values + // above 2^53 - 1, e.g. 9223372036854775807 -> ...,776,000. + it('renders large MPT TakerPays/TakerGets on modified Offer without precision loss', () => { + const MPT_PAYS = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + const MPT_GETS = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D60' + const { container } = renderMeta({ + tx: { TransactionType: 'OfferCreate', Account: 'rEGo', Sequence: 1 }, + meta: { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'Offer', + LedgerIndex: 'A'.repeat(64), + PreviousFields: { + TakerPays: { + mpt_issuance_id: MPT_PAYS, + value: '9223372036854775807', + }, + TakerGets: { + mpt_issuance_id: MPT_GETS, + value: '9876543210987654321', + }, + }, + FinalFields: { + Account: 'rEGo', + Sequence: 1, + TakerPays: { + mpt_issuance_id: MPT_PAYS, + value: '8000000000000000007', + }, + TakerGets: { + mpt_issuance_id: MPT_GETS, + value: '1234567890123456789', + }, + }, + }, + }, + ], + TransactionResult: 'tesSUCCESS', + }, + }) + // TakerPays: previous, final, and change (prev - final via BigInt). + expect(container.textContent).toContain('9,223,372,036,854,775,807') + expect(container.textContent).toContain('8,000,000,000,000,000,007') + expect(container.textContent).toContain('1,223,372,036,854,775,800') + // TakerGets: previous, final, and change. + expect(container.textContent).toContain('9,876,543,210,987,654,321') + expect(container.textContent).toContain('1,234,567,890,123,456,789') + expect(container.textContent).toContain('8,641,975,320,864,197,532') + }) + + // Mixed MPT/XRP offer. Catches a flag-swap regression: the pays side must + // hit the BigInt branch (grouped digits at full precision), and the gets + // side must hit the currency branch (XRP_BASE-scaled value with fractions). + it('renders mixed MPT/XRP modified Offer correctly on both branches', () => { + const MPT_ID = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + const { container } = renderMeta({ + tx: { TransactionType: 'OfferCreate', Account: 'rEGo', Sequence: 1 }, + meta: { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'Offer', + LedgerIndex: 'A'.repeat(64), + PreviousFields: { + TakerPays: { + mpt_issuance_id: MPT_ID, + value: '9223372036854775807', + }, + TakerGets: '2000000000', // 2,000 XRP in drops + }, + FinalFields: { + Account: 'rEGo', + Sequence: 1, + TakerPays: { + mpt_issuance_id: MPT_ID, + value: '8000000000000000007', + }, + TakerGets: '1000000000', // 1,000 XRP in drops + }, + }, + }, + ], + TransactionResult: 'tesSUCCESS', + }, + }) + // MPT pays side: BigInt branch preserves precision. + expect(container.textContent).toContain('9,223,372,036,854,775,807') + expect(container.textContent).toContain('1,223,372,036,854,775,800') + // XRP gets side: drops / XRP_BASE rendered with fractional digits via + // the currency branch (BigInt branch outputs no decimals, so the ".00" + // here is what distinguishes the two paths). + expect(container.textContent).toContain('2,000.00') + expect(container.textContent).toContain('1,000.00') + }) + + // MPToken/MPTokenIssuance renderers use BigInt end-to-end with .toString(10), + // so raw digit strings (no thousands separators) appear at full precision. + it('renders MPToken and MPTokenIssuance balances at full precision', () => { + const MPT_ID = '000003C31D321B7DDA58324DC38CDF18934FAFFFCDF69D5F' + const { container } = renderMeta({ + tx: { TransactionType: 'Payment', Account: 'rIssuer', Sequence: 1 }, + meta: { + AffectedNodes: [ + { + ModifiedNode: { + LedgerEntryType: 'MPToken', + LedgerIndex: 'B'.repeat(64), + PreviousFields: { MPTAmount: '9223372036854775000' }, + FinalFields: { + Account: 'rPayer', + MPTokenIssuanceID: MPT_ID, + MPTAmount: '7000000000000000000', + }, + }, + }, + { + ModifiedNode: { + LedgerEntryType: 'MPTokenIssuance', + LedgerIndex: 'C'.repeat(64), + PreviousFields: { OutstandingAmount: '9223372036854775807' }, + FinalFields: { + Issuer: 'rIssuer', + MPTokenIssuanceID: MPT_ID, + OutstandingAmount: '8000000000000000000', + }, + }, + }, + ], + TransactionResult: 'tesSUCCESS', + }, + }) + // MPToken: prev, final, and change = final - prev (negative since balance dropped). + expect(container.textContent).toContain('9223372036854775000') + expect(container.textContent).toContain('7000000000000000000') + expect(container.textContent).toContain('-2223372036854775000') + // MPTokenIssuance: prev=2^63-1, final, change = final - prev. + expect(container.textContent).toContain('9223372036854775807') + expect(container.textContent).toContain('8000000000000000000') + expect(container.textContent).toContain('-1223372036854775807') + }) }) diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index 68bb0b8fb..2618e2fb6 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -185,12 +185,19 @@ export const localizeNumber = ( const sign = intPart.startsWith('-') ? '-' : '' const absInt = sign ? intPart.slice(1) : intPart - const grouped = new Intl.NumberFormat(lang, { + // Intl.NumberFormat validates `currency` regardless of `style`, and for + // MPT amounts the "currency" is an mpt_issuance_id (48-hex), which is + // not a valid ISO code — strip it before formatting. + const groupedConfig = { ...config, style: 'decimal', minimumFractionDigits: 0, maximumFractionDigits: 0, - }).format(BigInt(absInt)) + } + delete groupedConfig.currency + const grouped = new Intl.NumberFormat(lang, groupedConfig).format( + BigInt(absInt), + ) return fracPart ? `${sign}${grouped}.${fracPart}` : `${sign}${grouped}` }