diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json
index 0f97a1a6f..3467d89c4 100644
--- a/public/locales/ca-CA/translations.json
+++ b/public/locales/ca-CA/translations.json
@@ -943,5 +943,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 bf49cd4b1..3dda681f4 100644
--- a/public/locales/en-US/translations.json
+++ b/public/locales/en-US/translations.json
@@ -944,5 +944,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 2970d7e49..8b7384fa2 100644
--- a/public/locales/es-ES/translations.json
+++ b/public/locales/es-ES/translations.json
@@ -944,5 +944,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 f68c80ccb..cb36a2f7d 100644
--- a/public/locales/fr-FR/translations.json
+++ b/public/locales/fr-FR/translations.json
@@ -944,5 +944,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 27359a32f..db73a2754 100644
--- a/public/locales/ja-JP/translations.json
+++ b/public/locales/ja-JP/translations.json
@@ -944,5 +944,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 33b9df6f0..d230bea89 100644
--- a/public/locales/ko-KR/translations.json
+++ b/public/locales/ko-KR/translations.json
@@ -944,5 +944,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 5a597442e..803924dba 100644
--- a/public/locales/my-MM/translations.json
+++ b/public/locales/my-MM/translations.json
@@ -944,5 +944,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..d2a3697fd 100644
--- a/src/containers/Token/MPT/Header/Settings.tsx
+++ b/src/containers/Token/MPT/Header/Settings.tsx
@@ -2,17 +2,27 @@ import { useTranslation } from 'react-i18next'
interface Props {
flags?: string[]
+ mutableFlags?: string[]
}
interface FlagItem {
key: string
label: string
enabled: boolean
+ // The lsfMPTCanMutate* flag name (Dynamic MPT, XLS-94) that marks this setting
+ // mutable; present only for settings that can be declared mutable.
+ mutableFlag?: string
}
-export const Settings = ({ flags = [] }: Props): JSX.Element => {
+export const Settings = ({
+ flags = [],
+ mutableFlags = [],
+}: Props): JSX.Element => {
const { t } = useTranslation()
+ const isMutable = (mutableFlag?: string): boolean =>
+ !!mutableFlag && mutableFlags.includes(mutableFlag)
+
const flagItems: FlagItem[] = [
{
key: 'locked',
@@ -23,31 +33,37 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
key: 'canLock',
label: t('can_lock'),
enabled: flags.includes('lsfMPTCanLock'),
+ mutableFlag: 'lsmfMPTCanEnableCanLock',
},
{
key: 'requireAuth',
label: t('require_auth'),
enabled: flags.includes('lsfMPTRequireAuth'),
+ mutableFlag: 'lsmfMPTCanEnableRequireAuth',
},
{
key: 'canEscrow',
label: t('can_escrow'),
enabled: flags.includes('lsfMPTCanEscrow'),
+ mutableFlag: 'lsmfMPTCanEnableCanEscrow',
},
{
key: 'canTrade',
label: t('can_trade'),
enabled: flags.includes('lsfMPTCanTrade'),
+ mutableFlag: 'lsmfMPTCanEnableCanTrade',
},
{
key: 'canTransfer',
label: t('can_transfer'),
enabled: flags.includes('lsfMPTCanTransfer'),
+ mutableFlag: 'lsmfMPTCanEnableCanTransfer',
},
{
key: 'canClawback',
label: t('can_clawback'),
enabled: flags.includes('lsfMPTCanClawback'),
+ mutableFlag: 'lsmfMPTCanEnableCanClawback',
},
{
key: 'canConfidentialAmount',
@@ -56,6 +72,21 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
},
]
+ // Mutable fields (Dynamic MPT) that are not capability flags. Only shown when
+ // the issuer declared them mutable at creation.
+ const mutableFieldItems = [
+ {
+ key: 'metadata',
+ label: t('metadata'),
+ mutableFlag: 'lsmfMPTCanMutateMetadata',
+ },
+ {
+ key: 'transferFee',
+ label: t('transfer_fee'),
+ mutableFlag: 'lsmfMPTCanMutateTransferFee',
+ },
+ ].filter((item) => isMutable(item.mutableFlag))
+
return (
{t('settings')}
@@ -63,10 +94,39 @@ 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 pill while the flag is still disabled. */}
+ {isMutable(flag.mutableFlag) && !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..7cc089d68 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,
+ mutableFlags,
rawMPTMetadata,
parsedMPTMetadata,
isMPTMetadataCompliant,
@@ -235,7 +236,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt={confidentialOutstandingAmt}
assetScale={assetScale}
/>
-
+
{(parsedMPTMetadata || rawMPTMetadata) && (
{
const renderComponent = (props: any = {}) =>
render(
-
+
,
)
@@ -101,4 +101,38 @@ describe('Settings component', () => {
const { container } = renderComponent({ flags: undefined })
expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8)
})
+
+ it('does not render mutable badges when no mutable flags are set', () => {
+ const { queryAllByTestId } = renderComponent({ flags: [] })
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(0)
+ })
+
+ it('marks a disabled capability flag as mutable', () => {
+ const { container, getAllByTestId } = renderComponent({
+ flags: [],
+ mutableFlags: ['lsmfMPTCanEnableCanLock'],
+ })
+ expect(getAllByTestId('mutable-badge')).toHaveLength(1)
+ expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(1)
+ })
+
+ 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'],
+ mutableFlags: ['lsmfMPTCanEnableCanLock'],
+ })
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(0)
+ })
+
+ it('renders extra rows for mutable metadata and transfer fee', () => {
+ const { container, getAllByTestId } = renderComponent({
+ flags: [],
+ mutableFlags: ['lsmfMPTCanMutateMetadata', 'lsmfMPTCanMutateTransferFee'],
+ })
+ // 8 capability rows + metadata + transfer fee
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(10)
+ expect(getAllByTestId('mutable-badge')).toHaveLength(2)
+ })
})
diff --git a/src/containers/shared/Interfaces.tsx b/src/containers/shared/Interfaces.tsx
index f9d11d00b..75d0066da 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[]
+ mutableFlags?: 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..f3814f4b7 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,
+ mutableFlags,
+ } = data.instructions
const { t } = useTranslation()
const language = useLanguage()
const formattedFee =
@@ -64,6 +70,17 @@ export const Simple: TransactionSimpleComponent = ({
)}
)}
+ {mutableFlags && mutableFlags.length > 0 && (
+
+ {mutableFlags.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..79623c588 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_CREATE_MUTABLE_FLAGS } from '../../../transactionUtils'
+
+// TODO: use MPTokenIssuanceCreate when DynamicMPT is supported on xrpl.js
+interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate {
+ MutableFlags?: 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 mutableFlags = buildFlags(tx.MutableFlags, MPT_CREATE_MUTABLE_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,
+ mutableFlags: mutableFlags.length ? mutableFlags : 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..bbf6a1535 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,30 @@ 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)
+
+ // MutableFlags 196610 = tmfMPTCanEnableCanLock (0x2) +
+ // tmfMPTCanMutateMetadata (0x10000) + tmfMPTCanMutateTransferFee (0x20000)
+ expectSimpleRowText(
+ container,
+ 'mpt-mutable-flags',
+ 'tmfMPTCanEnableCanLock',
+ )
+ expectSimpleRowText(
+ container,
+ 'mpt-mutable-flags',
+ 'tmfMPTCanMutateMetadata',
+ )
+ expectSimpleRowText(
+ container,
+ 'mpt-mutable-flags',
+ 'tmfMPTCanMutateTransferFee',
+ )
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..502acbb39
--- /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",
+ "MutableFlags": 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",
+ "MutableFlags": 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..0239422ff 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 declared mutable via Dynamic MPT (XLS-94).
+ mutableFlags?: string[]
}
diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
index 9b0777e17..0798544d4 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_SET_MUTABLE_FLAGS } from '../../../transactionUtils'
interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet {
+ MPTokenMetadata?: string
+ TransferFee?: number
+ MutableFlags?: number
IssuerEncryptionKey?: string
AuditorEncryptionKey?: string
}
@@ -17,10 +31,25 @@ export const Simple: TransactionSimpleComponent = ({
const {
MPTokenIssuanceID,
Holder,
+ MPTokenMetadata,
+ TransferFee,
+ MutableFlags,
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(MutableFlags, MPT_SET_MUTABLE_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')
+ // MutableFlags 17 = tmfMPTSetCanTransfer (0x10) + tmfMPTSetCanLock (0x1)
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanLock')
+ expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanTransfer')
+ 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..6c732a934
--- /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",
+ "MutableFlags": 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",
+ "MutableFlags": 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..c83a8e173 100644
--- a/src/containers/shared/transactionUtils.ts
+++ b/src/containers/shared/transactionUtils.ts
@@ -119,6 +119,37 @@ export const TX_FLAGS: Record> = {
},
}
+// Dynamic MPT (XLS-94) MutableFlags fields. These are separate UInt32 fields
+// from the standard `Flags` field, so they are decoded with their own maps.
+// Values mirror rippled's tmfMPT* definitions in TxFlags.h.
+
+// MPTokenIssuanceCreate.MutableFlags: declares which fields/flags may be
+// modified after creation. Boolean capabilities use the "CanEnable" form
+// (they may only be turned on later); Metadata/TransferFee use "CanMutate".
+export const MPT_CREATE_MUTABLE_FLAGS: Record = {
+ 0x00000002: 'tmfMPTCanEnableCanLock',
+ 0x00000004: 'tmfMPTCanEnableRequireAuth',
+ 0x00000008: 'tmfMPTCanEnableCanEscrow',
+ 0x00000010: 'tmfMPTCanEnableCanTrade',
+ 0x00000020: 'tmfMPTCanEnableCanTransfer',
+ 0x00000040: 'tmfMPTCanEnableCanClawback',
+ 0x00000080: 'tmfMPTCannotEnableCanHoldConfidentialBalance',
+ 0x00010000: 'tmfMPTCanMutateMetadata',
+ 0x00020000: 'tmfMPTCanMutateTransferFee',
+}
+
+// MPTokenIssuanceSet.MutableFlags: enables a previously-declared mutable
+// capability flag. Single "Set" bit per capability (no clear operation).
+export const MPT_SET_MUTABLE_FLAGS: Record = {
+ 0x00000001: 'tmfMPTSetCanLock',
+ 0x00000002: 'tmfMPTSetRequireAuth',
+ 0x00000004: 'tmfMPTSetCanEscrow',
+ 0x00000008: 'tmfMPTSetCanTrade',
+ 0x00000010: 'tmfMPTSetCanTransfer',
+ 0x00000020: 'tmfMPTSetCanClawback',
+ 0x00000040: 'tmfMPTSetCanHoldConfidentialBalance',
+}
+
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..cc7f63abc 100644
--- a/src/rippled/lib/utils.ts
+++ b/src/rippled/lib/utils.ts
@@ -47,6 +47,20 @@ const MPT_ISSUANCE_FLAGS: FlagMap = {
0x00000040: 'lsfMPTCanClawback',
0x00000080: 'lsfMPTCanConfidentialAmount',
}
+// MutableFlags on the MPTokenIssuance ledger object (Dynamic MPT, XLS-94).
+// Declares which fields/flags the issuer may modify after creation.
+// Values mirror rippled's lsmfMPT* definitions in LedgerFormats.h.
+const MPT_ISSUANCE_MUTABLE_FLAGS: FlagMap = {
+ 0x00000002: 'lsmfMPTCanEnableCanLock',
+ 0x00000004: 'lsmfMPTCanEnableRequireAuth',
+ 0x00000008: 'lsmfMPTCanEnableCanEscrow',
+ 0x00000010: 'lsmfMPTCanEnableCanTrade',
+ 0x00000020: 'lsmfMPTCanEnableCanTransfer',
+ 0x00000040: 'lsmfMPTCanEnableCanClawback',
+ 0x00000080: 'lsmfMPTCannotEnableCanHoldConfidentialBalance',
+ 0x00010000: 'lsmfMPTCanMutateMetadata',
+ 0x00020000: 'lsmfMPTCanMutateTransferFee',
+}
const MPTOKEN_FLAGS: FlagMap = {
0x00000001: 'lsfMPTLocked',
0x00000002: 'lsfMPTAuthorized',
@@ -267,6 +281,7 @@ interface MPTIssuanceInfo {
Sequence: number
MPTokenMetadata?: string
Flags: number
+ MutableFlags?: number
IssuerEncryptionKey?: string
AuditorEncryptionKey?: string
}
@@ -281,6 +296,7 @@ interface FormattedMPTIssuance {
sequence: number
metadata?: any
flags: string[]
+ mutableFlags: string[]
rawMPTMetadata?: string
parsedMPTMetadata?: Record
isMPTMetadataCompliant: boolean
@@ -312,6 +328,7 @@ const formatMPTIssuance = (info: MPTIssuanceInfo): FormattedMPTIssuance => {
parsedMPTMetadata: parseMPTMetadata(rawMPTMetadataHex),
isMPTMetadataCompliant: isMPTMetadataCompliant(rawMPTMetadataHex),
flags: buildFlags(info.Flags, MPT_ISSUANCE_FLAGS),
+ mutableFlags: buildFlags(info.MutableFlags, MPT_ISSUANCE_MUTABLE_FLAGS),
issuerEncryptionKey: info.IssuerEncryptionKey,
auditorEncryptionKey: info.AuditorEncryptionKey,
}