diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json
index bc4553c11..1d5137cb9 100644
--- a/public/locales/ca-CA/translations.json
+++ b/public/locales/ca-CA/translations.json
@@ -945,5 +945,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json
index 8c55a6a7f..0bb6039f9 100644
--- a/public/locales/en-US/translations.json
+++ b/public/locales/en-US/translations.json
@@ -946,5 +946,9 @@
"registered": "Registered",
"included": "Included",
"maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).",
- "maintenance_banner.countdown_prefix": "Starts in"
+ "maintenance_banner.countdown_prefix": "Starts in",
+ "mutable": "Mutable",
+ "mutable_flags": "Mutable Flags",
+ "mutable_flag_tooltip": "The issuer can enable this flag later",
+ "mutable_field_tooltip": "The issuer can change this value"
}
diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json
index 12e1a48f4..d6c0633c1 100644
--- a/public/locales/es-ES/translations.json
+++ b/public/locales/es-ES/translations.json
@@ -946,5 +946,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json
index 99bb71f9e..6c4e18bf9 100644
--- a/public/locales/fr-FR/translations.json
+++ b/public/locales/fr-FR/translations.json
@@ -946,5 +946,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json
index 8229d9e98..685db8c96 100644
--- a/public/locales/ja-JP/translations.json
+++ b/public/locales/ja-JP/translations.json
@@ -946,5 +946,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json
index d02b1dd5c..c97d77f12 100644
--- a/public/locales/ko-KR/translations.json
+++ b/public/locales/ko-KR/translations.json
@@ -946,5 +946,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json
index c1ef818ab..c039f7cfb 100644
--- a/public/locales/my-MM/translations.json
+++ b/public/locales/my-MM/translations.json
@@ -946,5 +946,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "mutable": null,
+ "mutable_flags": null,
+ "mutable_flag_tooltip": null,
+ "mutable_field_tooltip": null
}
diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx
index fac9ee031..582ae96f2 100644
--- a/src/containers/Token/MPT/Header/Settings.tsx
+++ b/src/containers/Token/MPT/Header/Settings.tsx
@@ -2,17 +2,28 @@ import { useTranslation } from 'react-i18next'
interface Props {
flags?: string[]
+ immutableFlags?: string[]
}
interface FlagItem {
key: string
label: string
enabled: boolean
+ // The lsifMPT* flag name (Dynamic MPT, XLS-94) that permanently locks this
+ // capability; present only for settings that can be declared immutable.
+ immutableFlag?: string
}
-export const Settings = ({ flags = [] }: Props): JSX.Element => {
+export const Settings = ({
+ flags = [],
+ immutableFlags = [],
+}: Props): JSX.Element => {
const { t } = useTranslation()
+ // Returns true when the flag is NOT locked in immutableFlags (i.e. still mutable).
+ const isStillMutable = (immutableFlag?: string): boolean =>
+ !!immutableFlag && !immutableFlags.includes(immutableFlag)
+
const flagItems: FlagItem[] = [
{
key: 'locked',
@@ -23,31 +34,37 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
key: 'canLock',
label: t('can_lock'),
enabled: flags.includes('lsfMPTCanLock'),
+ immutableFlag: 'lsifMPTCanLock',
},
{
key: 'requireAuth',
label: t('require_auth'),
enabled: flags.includes('lsfMPTRequireAuth'),
+ immutableFlag: 'lsifMPTRequireAuth',
},
{
key: 'canEscrow',
label: t('can_escrow'),
enabled: flags.includes('lsfMPTCanEscrow'),
+ immutableFlag: 'lsifMPTCanEscrow',
},
{
key: 'canTrade',
label: t('can_trade'),
enabled: flags.includes('lsfMPTCanTrade'),
+ immutableFlag: 'lsifMPTCanTrade',
},
{
key: 'canTransfer',
label: t('can_transfer'),
enabled: flags.includes('lsfMPTCanTransfer'),
+ immutableFlag: 'lsifMPTCanTransfer',
},
{
key: 'canClawback',
label: t('can_clawback'),
enabled: flags.includes('lsfMPTCanClawback'),
+ immutableFlag: 'lsifMPTCanClawback',
},
{
key: 'canConfidentialAmount',
@@ -56,6 +73,21 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
},
]
+ // Field items (Dynamic MPT) that are not capability flags. Shown only when
+ // the flag is NOT locked in immutableFlags (i.e. the field is still mutable).
+ const mutableFieldItems = [
+ {
+ key: 'metadata',
+ label: t('metadata'),
+ immutableFlag: 'lsifMPTMetadata',
+ },
+ {
+ key: 'transferFee',
+ label: t('transfer_fee'),
+ immutableFlag: 'lsifMPTTransferFee',
+ },
+ ].filter((item) => isStillMutable(item.immutableFlag))
+
return (
{t('settings')}
@@ -63,10 +95,40 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
{flagItems.map((flag) => (
{flag.label}
-
- {flag.enabled ? t('enabled') : t('disabled')}
+
+ {/* Capabilities are one-directional (can only be enabled later),
+ so only surface the "Mutable" pill while the flag is still
+ not locked (not in immutableFlags) and not yet enabled. */}
+ {isStillMutable(flag.immutableFlag) && !flag.enabled && (
+
+ {t('mutable')}
+
+ )}
+
+ {flag.enabled ? t('enabled') : t('disabled')}
+
+
+
+ ))}
+ {mutableFieldItems.map((field) => (
+
))}
diff --git a/src/containers/Token/MPT/Header/index.tsx b/src/containers/Token/MPT/Header/index.tsx
index 74bed94d9..7c3642b35 100644
--- a/src/containers/Token/MPT/Header/index.tsx
+++ b/src/containers/Token/MPT/Header/index.tsx
@@ -92,6 +92,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt,
transferFee,
flags,
+ immutableFlags,
rawMPTMetadata,
parsedMPTMetadata,
isMPTMetadataCompliant,
@@ -235,7 +236,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt={confidentialOutstandingAmt}
assetScale={assetScale}
/>
-
+
{(parsedMPTMetadata || rawMPTMetadata) && (
{
const renderComponent = (props: any = {}) =>
render(
-
+
,
)
@@ -21,9 +21,11 @@ describe('Settings component', () => {
)
})
- it('renders all 7 flag items', () => {
+ it('renders 8 capability flag items plus 2 field rows by default', () => {
+ // With no immutableFlags, metadata and transferFee field rows are shown
+ // (they are still mutable), giving 8 capability rows + 2 field rows = 10.
const { container } = renderComponent()
- expect(container.querySelectorAll('.header-box-item')).toHaveLength(8)
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(10)
})
it('shows locked flag as disabled by default', () => {
@@ -101,4 +103,57 @@ describe('Settings component', () => {
const { container } = renderComponent({ flags: undefined })
expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8)
})
+
+ it('shows mutable badges for all unlocked disabled caps and field rows when no immutableFlags set', () => {
+ // With no immutableFlags, all capabilities are still mutable and both field
+ // rows (metadata, transferFee) are visible. 6 disabled cap badges + 2 field
+ // row badges = 8 total.
+ const { queryAllByTestId, container } = renderComponent({ flags: [] })
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(8)
+ // 8 capability rows + metadata + transferFee field rows
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(10)
+ })
+
+ it('hides the mutable badge for a capability once it is locked (in immutableFlags)', () => {
+ const { container, getAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTCanLock'],
+ })
+ // lsifMPTCanLock is locked → its badge is hidden; 5 other disabled caps +
+ // 2 field rows still show badges = 7 total
+ expect(getAllByTestId('mutable-badge')).toHaveLength(7)
+ expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(7)
+ })
+
+ it('hides the mutable badge once a capability is enabled', () => {
+ // Capabilities are one-directional (enable-only), so a mutable badge on an
+ // already-enabled flag would be misleading.
+ const { queryAllByTestId } = renderComponent({
+ flags: ['lsfMPTCanLock'],
+ immutableFlags: [],
+ })
+ // canLock is enabled → no badge for it; 5 other disabled caps + 2 field
+ // rows still show badges = 7 total
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(7)
+ })
+
+ it('hides extra field rows for metadata and transferFee when locked in immutableFlags', () => {
+ const { container } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTMetadata', 'lsifMPTTransferFee'],
+ })
+ // 8 capability rows only (field rows are hidden when locked)
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(8)
+ })
+
+ it('shows extra rows for mutable metadata and transfer fee when not locked', () => {
+ const { container, getAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: [],
+ })
+ // 8 capability rows + metadata + transfer fee (both still mutable)
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(10)
+ // badges: 6 disabled cap flags (not locked) + 2 field row badges = 8
+ expect(getAllByTestId('mutable-badge')).toHaveLength(8)
+ })
})
diff --git a/src/containers/shared/Interfaces.tsx b/src/containers/shared/Interfaces.tsx
index f9d11d00b..02d0bf7b9 100644
--- a/src/containers/shared/Interfaces.tsx
+++ b/src/containers/shared/Interfaces.tsx
@@ -48,6 +48,7 @@ export interface FormattedMPTIssuance {
outstandingAmt?: string
confidentialOutstandingAmt?: string
flags?: string[]
+ immutableFlags?: string[]
transferFee?: number
rawMPTMetadata?: string
parsedMPTMetadata?: Record
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx
index 28a9c62a6..6caec6c41 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx
@@ -11,8 +11,14 @@ import './styles.scss'
export const Simple: TransactionSimpleComponent = ({
data,
}: TransactionSimpleProps) => {
- const { issuanceID, metadata, assetScale, transferFee, maxAmount } =
- data.instructions
+ const {
+ issuanceID,
+ metadata,
+ assetScale,
+ transferFee,
+ maxAmount,
+ immutableFlags,
+ } = data.instructions
const { t } = useTranslation()
const language = useLanguage()
const formattedFee =
@@ -64,6 +70,17 @@ export const Simple: TransactionSimpleComponent = ({
)}
)}
+ {immutableFlags && immutableFlags.length > 0 && (
+
+ {immutableFlags.map((flag) => (
+ {flag}
+ ))}
+
+ )}
>
)
}
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts
index fd08c599f..1bfb17585 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts
@@ -1,19 +1,32 @@
import type { MPTokenIssuanceCreate } from 'xrpl'
import { MPTokenIssuanceCreateInstructions } from './types'
import { TransactionParser } from '../types'
-import { convertHexToString } from '../../../../../rippled/lib/utils'
+import {
+ buildFlags,
+ convertHexToString,
+} from '../../../../../rippled/lib/utils'
+import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils'
+
+// TODO: use MPTokenIssuanceCreate when DynamicMPT is supported on xrpl.js
+interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate {
+ ImmutableFlags?: number
+}
export const parser: TransactionParser<
- MPTokenIssuanceCreate,
+ MPTokenIssuanceCreateExtended,
MPTokenIssuanceCreateInstructions
-> = (tx, meta) => ({
- issuanceID: meta.mpt_issuance_id,
- metadata: tx.MPTokenMetadata
- ? convertHexToString(tx.MPTokenMetadata)
- : undefined,
- transferFee: tx.TransferFee,
- assetScale: tx.AssetScale,
- maxAmount: tx.MaximumAmount
- ? BigInt(tx.MaximumAmount).toString(10)
- : undefined,
-})
+> = (tx, meta) => {
+ const immutableFlags = buildFlags(tx.ImmutableFlags, MPT_IMMUTABLE_FLAGS)
+ return {
+ issuanceID: meta.mpt_issuance_id,
+ metadata: tx.MPTokenMetadata
+ ? convertHexToString(tx.MPTokenMetadata)
+ : undefined,
+ transferFee: tx.TransferFee,
+ assetScale: tx.AssetScale,
+ maxAmount: tx.MaximumAmount
+ ? BigInt(tx.MaximumAmount).toString(10)
+ : undefined,
+ immutableFlags: immutableFlags.length ? immutableFlags : undefined,
+ }
+}
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx
index be888d0d6..bb8cbfaa2 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx
@@ -1,7 +1,8 @@
import { createSimpleRenderFactory } from '../../test/createWrapperFactory'
import { Simple } from '../Simple'
-import { expectSimpleRowText } from '../../test'
+import { expectSimpleRowText, expectSimpleRowNotToExist } from '../../test'
import transactionSuccess from './mock_data/MPTokenIssuanceCreate.json'
+import transactionDynamic from './mock_data/MPTokenIssuanceCreate_Dynamic.json'
const renderComponent = createSimpleRenderFactory(Simple)
@@ -22,6 +23,18 @@ describe('MPTokenIssuanceCreate', () => {
'https://ipfs.io/ipfs/QmZnjmB9Tk4xaA9E679ytrPXda3beWMLUnMB5RFj1eStLp',
)
expectSimpleRowText(container, 'mpt-fee', '0.010%')
+ expectSimpleRowNotToExist(container, 'mpt-mutable-flags')
+ unmount()
+ })
+
+ it('handles Dynamic MPT MPTokenIssuanceCreate simple view', () => {
+ const { container, unmount } = renderComponent(transactionDynamic)
+
+ // ImmutableFlags 196610 = tifMPTCanLock (0x2) +
+ // tifMPTMetadata (0x10000) + tifMPTTransferFee (0x20000)
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tifMPTCanLock')
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tifMPTMetadata')
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tifMPTTransferFee')
unmount()
})
})
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json
new file mode 100644
index 000000000..be5b44f6e
--- /dev/null
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json
@@ -0,0 +1,51 @@
+{
+ "tx": {
+ "Account": "rfGb6p2kWy3zQweWnYNxSFYoHeymcx7mhg",
+ "AssetScale": 2,
+ "Fee": "10",
+ "Flags": 34,
+ "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70",
+ "MaximumAmount": "9223372036854775807",
+ "ImmutableFlags": 196610,
+ "Sequence": 5496,
+ "SigningPubKey": "EDAD408FAEE57EB4A347E6FE395B834DD47C6531C3C37B09ACC35528161CAD4B0E",
+ "TransactionType": "MPTokenIssuanceCreate",
+ "TransferFee": 10,
+ "TxnSignature": "F7AA8083EE7D7EFD10E11FF5A12B73D2D45A80094AEED4B41FBF2A90C9E03E5E4D162E91FA1EC156BF94E770E70E7633DF09665A5C2D5408178FC376BFC9B100",
+ "ctid": "C000157A00000000",
+ "date": 1710949602000,
+ "hash": "undefined",
+ "inLedger": "undefined",
+ "ledger_index": "undefined",
+ "meta": "undefined",
+ "validated": "undefined",
+ "metaData": "undefined",
+ "status": "undefined"
+ },
+ "meta": {
+ "AffectedNodes": [
+ {
+ "CreatedNode": {
+ "LedgerEntryType": "MPTokenIssuance",
+ "LedgerIndex": "265CEA78D8246B8B51D5CCC20AF4DB95569DE09E53115C888B176A3D1D05048A",
+ "NewFields": {
+ "AssetScale": 2,
+ "Flags": 34,
+ "Issuer": "rfGb6p2kWy3zQweWnYNxSFYoHeymcx7mhg",
+ "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70",
+ "MaximumAmount": "9223372036854775807",
+ "ImmutableFlags": 196610,
+ "Sequence": 5496,
+ "TransferFee": 10
+ }
+ }
+ }
+ ],
+ "TransactionIndex": 0,
+ "TransactionResult": "tesSUCCESS",
+ "mpt_issuance_id": "0000157844C3F3B57A8B579FEE1033CC8E8498729D063617"
+ },
+ "hash": "9686DA1322D2D8F9CD97C5848A8E3CADB9D3F73154DA59BB3A3CACC4CA43671C",
+ "ledger_index": 5498,
+ "date": 1710949602000
+}
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts
index 065d02976..caa4d9912 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts
@@ -4,4 +4,6 @@ export interface MPTokenIssuanceCreateInstructions {
transferFee?: number
assetScale?: number
maxAmount?: string
+ // i18n keys of the fields/flags permanently locked via Dynamic MPT (XLS-94).
+ immutableFlags?: string[]
}
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
index 9b0777e17..9bd4e94bf 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
@@ -4,9 +4,23 @@ import { SimpleRow } from '../SimpleRow'
import { TransactionSimpleComponent, TransactionSimpleProps } from '../types'
import { Account } from '../../Account'
import { MPTokenLink } from '../../MPTokenLink'
-import { shortenEncryptionKey } from '../../../utils'
+import {
+ isValidJsonString,
+ localizeNumber,
+ shortenEncryptionKey,
+} from '../../../utils'
+import { useLanguage } from '../../../hooks'
+import { JsonView } from '../../JsonView'
+import {
+ buildFlags,
+ convertHexToString,
+} from '../../../../../rippled/lib/utils'
+import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils'
interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet {
+ MPTokenMetadata?: string
+ TransferFee?: number
+ ImmutableFlags?: number
IssuerEncryptionKey?: string
AuditorEncryptionKey?: string
}
@@ -17,10 +31,25 @@ export const Simple: TransactionSimpleComponent = ({
const {
MPTokenIssuanceID,
Holder,
+ MPTokenMetadata,
+ TransferFee,
+ ImmutableFlags,
IssuerEncryptionKey,
AuditorEncryptionKey,
} = data.instructions
const { t } = useTranslation()
+ const language = useLanguage()
+
+ const metadata = MPTokenMetadata
+ ? convertHexToString(MPTokenMetadata)
+ : undefined
+ const formattedFee =
+ TransferFee != null
+ ? `${localizeNumber((TransferFee / 1000).toPrecision(5), language, {
+ minimumFractionDigits: 3,
+ })}%`
+ : undefined
+ const flagChanges = buildFlags(ImmutableFlags, MPT_IMMUTABLE_FLAGS)
return (
<>
@@ -32,6 +61,35 @@ export const Simple: TransactionSimpleComponent = ({
)}
+ {TransferFee != null && (
+
+ {formattedFee}
+
+ )}
+ {metadata && (
+
+ {isValidJsonString(metadata) ? (
+
+ ) : (
+ metadata
+ )}
+
+ )}
+ {flagChanges.length > 0 && (
+
+ {flagChanges.map((flag) => (
+ {flag}
+ ))}
+
+ )}
{IssuerEncryptionKey && (
{
unmount()
})
+ it('handles Dynamic MPT MPTokenIssuanceSet simple view', () => {
+ const { container, unmount } = renderComponent(transactionDynamic)
+
+ expectSimpleRowText(
+ container,
+ 'mpt-issuance-id',
+ '000002609BB39CEC721B5AB337B6BD862ACD2811CBBB5F18',
+ )
+ expectSimpleRowText(container, 'mpt-fee', '0.500%')
+ expectSimpleRowText(container, 'mpt-metadata', 'FOO')
+ // ImmutableFlags 17 (0x11) = 0x10 (tifMPTCanTrade) + 0x01 (not in MPT_IMMUTABLE_FLAGS map)
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tifMPTCanTrade')
+ expectSimpleRowNotToExist(container, 'mpt-holder')
+ unmount()
+ })
+
it('handles failed MPTokenIssuanceSet simple view ', () => {
const { container, unmount } = renderComponent(transactionFail)
expectSimpleRowText(
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json
new file mode 100644
index 000000000..f6ec14387
--- /dev/null
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json
@@ -0,0 +1,73 @@
+{
+ "tx": {
+ "Account": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM",
+ "Fee": "10",
+ "Flags": 0,
+ "MPTokenIssuanceID": "000002609BB39CEC721B5AB337B6BD862ACD2811CBBB5F18",
+ "ImmutableFlags": 17,
+ "TransferFee": 500,
+ "MPTokenMetadata": "464F4F",
+ "Sequence": 609,
+ "SigningPubKey": "ED92DDE49AA689EC63589623067968B85E4885A9874B3CAB89E07D192EBFA42FF9",
+ "TransactionType": "MPTokenIssuanceSet",
+ "TxnSignature": "6678BF017A62360DAA39156112960D934440467D2E4098958F053B6758A2D237DA82CD27FDA3B35BAB610D74669BF6C4DEB12D77984E48158B2D79C8BAEC3303",
+ "ctid": "C000026400000000",
+ "date": 1711133159000,
+ "hash": "undefined",
+ "inLedger": "undefined",
+ "ledger_index": "undefined",
+ "meta": "undefined",
+ "validated": "undefined",
+ "metaData": "undefined",
+ "status": "undefined"
+ },
+ "meta": {
+ "AffectedNodes": [
+ {
+ "ModifiedNode": {
+ "FinalFields": {
+ "Account": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM",
+ "Balance": "99999980",
+ "Flags": 0,
+ "OwnerCount": 1,
+ "Sequence": 610
+ },
+ "LedgerEntryType": "AccountRoot",
+ "LedgerIndex": "1D54D263727856612FC7C7A27D93532ED0C41B74FA651992C125AD19C900669D",
+ "PreviousFields": {
+ "Balance": "99999990",
+ "Sequence": 609
+ },
+ "PreviousTxnID": "CF68A8D929F089F6F53B071250935DD7C3F52F6E175D82E53118265D776D4BF7",
+ "PreviousTxnLgrSeq": 610
+ }
+ },
+ {
+ "ModifiedNode": {
+ "FinalFields": {
+ "Flags": 32,
+ "Issuer": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM",
+ "ImmutableFlags": 131106,
+ "OutstandingAmount": "0",
+ "OwnerNode": "0",
+ "Sequence": 608,
+ "TransferFee": 500
+ },
+ "LedgerEntryType": "MPTokenIssuance",
+ "LedgerIndex": "E8DF796110B1FBD9086A4637491E49843D33D897FBC32F03A33140F76378EE86",
+ "PreviousFields": {
+ "Flags": 34,
+ "TransferFee": 0
+ },
+ "PreviousTxnID": "CF68A8D929F089F6F53B071250935DD7C3F52F6E175D82E53118265D776D4BF7",
+ "PreviousTxnLgrSeq": 610
+ }
+ }
+ ],
+ "TransactionIndex": 0,
+ "TransactionResult": "tesSUCCESS"
+ },
+ "hash": "73C21D8B5DFBF5DE03FCAF0D69C00E9E2918280561E9898C556A1C743A566D47",
+ "ledger_index": 612,
+ "date": 1711133159000
+}
diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts
index 6b3c4900c..c86064a6b 100644
--- a/src/containers/shared/transactionUtils.ts
+++ b/src/containers/shared/transactionUtils.ts
@@ -74,8 +74,13 @@ export const TX_FLAGS: Record> = {
MPTokenIssuanceSet: {
0x00000001: 'tfMPTLock',
0x00000002: 'tfMPTUnlock',
- 0x00001000: 'tfMPTSetCanConfidentialAmount',
- 0x00002000: 'tfMPTClearCanConfidentialAmount',
+ // Capability-setting flags (XLS-94 DynamicMPT): one-way, moved into Flags
+ 0x00000004: 'tfMPTSetCanLock',
+ 0x00000010: 'tfMPTSetCanEscrow',
+ 0x00000020: 'tfMPTSetCanTrade',
+ 0x00000040: 'tfMPTSetCanTransfer',
+ 0x00000080: 'tfMPTSetCanClawback',
+ 0x00000100: 'tfMPTSetCanHoldConfidentialBalance',
},
NFTokenMint: {
0x00000001: 'tfBurnable',
@@ -119,6 +124,27 @@ export const TX_FLAGS: Record> = {
},
}
+// Dynamic MPT (XLS-94) ImmutableFlags fields. These are separate UInt32 fields
+// from the standard `Flags` field, so they are decoded with their own maps.
+// Semantics: by default all fields/capabilities are mutable; setting a bit in
+// ImmutableFlags permanently locks that field/capability so it can never be
+// changed again via MPTokenIssuanceSet.
+
+// MPTokenIssuanceCreate.ImmutableFlags / MPTokenIssuanceSet.ImmutableFlags:
+// `tif` prefix (Transaction ImmutableFlag). Setting a bit permanently locks
+// the corresponding capability/field. Same bit values as the ledger `lsif*` map.
+export const MPT_IMMUTABLE_FLAGS: Record = {
+ 0x00000002: 'tifMPTCanLock',
+ 0x00000004: 'tifMPTRequireAuth',
+ 0x00000008: 'tifMPTCanEscrow',
+ 0x00000010: 'tifMPTCanTrade',
+ 0x00000020: 'tifMPTCanTransfer',
+ 0x00000040: 'tifMPTCanClawback',
+ 0x00000080: 'tifMPTCanHoldConfidentialBalance',
+ 0x00010000: 'tifMPTMetadata',
+ 0x00020000: 'tifMPTTransferFee',
+}
+
export const ACCOUNT_FLAGS: Record = {
17: 'asfAllowTrustLineLocking',
16: 'asfAllowTrustLineClawback',
diff --git a/src/rippled/lib/utils.ts b/src/rippled/lib/utils.ts
index 823870688..9b3991e6c 100644
--- a/src/rippled/lib/utils.ts
+++ b/src/rippled/lib/utils.ts
@@ -47,6 +47,21 @@ const MPT_ISSUANCE_FLAGS: FlagMap = {
0x00000040: 'lsfMPTCanClawback',
0x00000080: 'lsfMPTCanConfidentialAmount',
}
+// ImmutableFlags on the MPTokenIssuance ledger object (Dynamic MPT, XLS-94).
+// A set bit permanently locks the corresponding field/capability — the issuer
+// can no longer change it via MPTokenIssuanceSet. Absent = everything mutable.
+// `lsif` prefix mirrors the `tif` transaction-level names; bit values are shared.
+const MPT_ISSUANCE_IMMUTABLE_FLAGS: FlagMap = {
+ 0x00000002: 'lsifMPTCanLock',
+ 0x00000004: 'lsifMPTRequireAuth',
+ 0x00000008: 'lsifMPTCanEscrow',
+ 0x00000010: 'lsifMPTCanTrade',
+ 0x00000020: 'lsifMPTCanTransfer',
+ 0x00000040: 'lsifMPTCanClawback',
+ 0x00000080: 'lsifMPTCanHoldConfidentialBalance',
+ 0x00010000: 'lsifMPTMetadata',
+ 0x00020000: 'lsifMPTTransferFee',
+}
const MPTOKEN_FLAGS: FlagMap = {
0x00000001: 'lsfMPTLocked',
0x00000002: 'lsfMPTAuthorized',
@@ -267,6 +282,7 @@ interface MPTIssuanceInfo {
Sequence: number
MPTokenMetadata?: string
Flags: number
+ ImmutableFlags?: number
IssuerEncryptionKey?: string
AuditorEncryptionKey?: string
}
@@ -281,6 +297,7 @@ interface FormattedMPTIssuance {
sequence: number
metadata?: any
flags: string[]
+ immutableFlags: string[]
rawMPTMetadata?: string
parsedMPTMetadata?: Record
isMPTMetadataCompliant: boolean
@@ -312,6 +329,7 @@ const formatMPTIssuance = (info: MPTIssuanceInfo): FormattedMPTIssuance => {
parsedMPTMetadata: parseMPTMetadata(rawMPTMetadataHex),
isMPTMetadataCompliant: isMPTMetadataCompliant(rawMPTMetadataHex),
flags: buildFlags(info.Flags, MPT_ISSUANCE_FLAGS),
+ immutableFlags: buildFlags(info.ImmutableFlags, MPT_ISSUANCE_IMMUTABLE_FLAGS),
issuerEncryptionKey: info.IssuerEncryptionKey,
auditorEncryptionKey: info.AuditorEncryptionKey,
}