diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json
index cb84b3477..47abb0806 100644
--- a/public/locales/ca-CA/translations.json
+++ b/public/locales/ca-CA/translations.json
@@ -575,6 +575,7 @@
"check_mpt_id": null,
"outstanding_amount": null,
"locked": null,
+ "active": null,
"can_lock": null,
"require_auth": null,
"can_escrow": null,
@@ -944,5 +945,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json
index 0b4a9d9d5..02721c5b4 100644
--- a/public/locales/en-US/translations.json
+++ b/public/locales/en-US/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": "Please check your MPT Issuance ID",
"outstanding_amount": "Issued Amount",
"locked": "Locked",
+ "active": "Active",
"can_lock": "Can Lock",
"require_auth": "Require Auth",
"can_escrow": "Can Escrow",
@@ -945,5 +946,16 @@
"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_capability_tooltip": "The issuer can still enable this capability. Once enabled, it's permanent.",
+ "mutable_field_tooltip": "The issuer can still change or remove this value.",
+ "immutable": "Immutable",
+ "immutable_capability_tooltip": "Permanently off — the issuer can never enable this capability.",
+ "immutable_field_tooltip": "Permanent — the issuer can never change or remove this value.",
+ "immutable_flags": "Immutable Flags",
+ "enabled_capability_tooltip": "Turned on. MPT capabilities are enable-only, so this can never be turned off again.",
+ "disabled_capability_tooltip": "Currently off. The adjacent pill shows whether this can still change.",
+ "capabilities": "Capabilities",
+ "fields": "Fields"
}
diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json
index 5e80854b4..aa9b52cea 100644
--- a/public/locales/es-ES/translations.json
+++ b/public/locales/es-ES/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": "Por favor, verifica tu ID de Emisión MPT",
"outstanding_amount": "Cantidad Emitida",
"locked": "Bloqueado",
+ "active": null,
"can_lock": "Se Puede Bloquear",
"require_auth": "Requiere Autenticación",
"can_escrow": "Puede Hacer Escrow",
@@ -945,5 +946,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json
index a2e67b4b5..dc63f5644 100644
--- a/public/locales/fr-FR/translations.json
+++ b/public/locales/fr-FR/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": null,
"outstanding_amount": null,
"locked": null,
+ "active": null,
"can_lock": null,
"require_auth": null,
"can_escrow": null,
@@ -945,5 +946,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json
index f64660fdd..c7ad0a474 100644
--- a/public/locales/ja-JP/translations.json
+++ b/public/locales/ja-JP/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": null,
"outstanding_amount": null,
"locked": null,
+ "active": null,
"can_lock": null,
"require_auth": null,
"can_escrow": null,
@@ -945,5 +946,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json
index cfeddbd31..36fc7d2f8 100644
--- a/public/locales/ko-KR/translations.json
+++ b/public/locales/ko-KR/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": null,
"outstanding_amount": null,
"locked": null,
+ "active": null,
"can_lock": null,
"require_auth": null,
"can_escrow": null,
@@ -945,5 +946,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json
index b948a258d..2b2458991 100644
--- a/public/locales/my-MM/translations.json
+++ b/public/locales/my-MM/translations.json
@@ -576,6 +576,7 @@
"check_mpt_id": "ကျေးဇူးပြု၍ သင့် MPT ထုတ်ဝေမှု ID ကို စစ်ဆေးပါ",
"outstanding_amount": "ထုတ်ပေးထားသော ပမာဏ",
"locked": "သော့ခတ်ထား",
+ "active": null,
"can_lock": "သော့ခတ်နိုင်",
"require_auth": "ခွင့်ပြုချက် လိုအပ်",
"can_escrow": "Escrow လုပ်နိုင်",
@@ -945,5 +946,16 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
- "maintenance_banner.countdown_prefix": null
+ "maintenance_banner.countdown_prefix": null,
+ "immutable_flags": null,
+ "mutable": null,
+ "mutable_capability_tooltip": null,
+ "mutable_field_tooltip": null,
+ "immutable": null,
+ "immutable_capability_tooltip": null,
+ "immutable_field_tooltip": null,
+ "enabled_capability_tooltip": null,
+ "disabled_capability_tooltip": null,
+ "capabilities": null,
+ "fields": null
}
diff --git a/src/containers/Token/MPT/Header/GeneralOverview.tsx b/src/containers/Token/MPT/Header/GeneralOverview.tsx
index a73e1df2b..9f5c25523 100644
--- a/src/containers/Token/MPT/Header/GeneralOverview.tsx
+++ b/src/containers/Token/MPT/Header/GeneralOverview.tsx
@@ -16,6 +16,7 @@ interface GeneralOverviewProps {
showMptId: boolean
holdersCount?: number
holdersLoading?: boolean
+ flags?: string[]
}
export const GeneralOverview = ({
@@ -27,6 +28,7 @@ export const GeneralOverview = ({
showMptId: showMptIssuanceId,
holdersCount,
holdersLoading,
+ flags = [],
}: GeneralOverviewProps): JSX.Element => {
const { t } = useTranslation()
@@ -57,6 +59,12 @@ export const GeneralOverview = ({
)}
+
+
{t('status')}
+
+ {flags.includes('lsfMPTLocked') ? t('locked') : t('active')}
+
+
{t('token_page.transfer_fee')}
diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx
index fac9ee031..77a2aeb7a 100644
--- a/src/containers/Token/MPT/Header/Settings.tsx
+++ b/src/containers/Token/MPT/Header/Settings.tsx
@@ -1,58 +1,102 @@
+import { MouseEvent } from 'react'
import { useTranslation } from 'react-i18next'
+import { Tooltip, useTooltip } from '../../../shared/components/Tooltip'
+
+const TOOLTIP_Y_OFFSET = 70
interface Props {
flags?: string[]
+ immutableFlags?: string[]
+ isDynamicMPTEnabled?: boolean
}
interface FlagItem {
key: string
label: string
enabled: boolean
+ immutableFlag: string
+}
+
+interface FieldItem {
+ key: string
+ label: string
+ immutableFlag: string
}
-export const Settings = ({ flags = [] }: Props): JSX.Element => {
+export const Settings = ({
+ flags = [],
+ immutableFlags = [],
+ isDynamicMPTEnabled = false,
+}: Props): JSX.Element => {
const { t } = useTranslation()
+ const { tooltip, showTooltip, hideTooltip } = useTooltip()
+
+ const isLocked = (immutableFlag: string): boolean =>
+ immutableFlags.includes(immutableFlag)
+
+ const showPillTooltip = (e: MouseEvent
, text: string) => {
+ const rect = e.currentTarget.getBoundingClientRect()
+ showTooltip('text', e, text, {
+ x: rect.left + rect.width / 2,
+ y: rect.top - TOOLTIP_Y_OFFSET,
+ })
+ }
const flagItems: FlagItem[] = [
- {
- key: 'locked',
- label: t('locked'),
- enabled: flags.includes('lsfMPTLocked'),
- },
{
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',
label: t('can_confidential_amount'),
- enabled: flags.includes('lsfMPTCanConfidentialAmount'),
+ enabled: flags.includes('lsfMPTCanHoldConfidentialBalance'),
+ immutableFlag: 'lsifMPTCanHoldConfidentialBalance',
+ },
+ ]
+
+ const fieldItems: FieldItem[] = [
+ {
+ key: 'metadata',
+ label: t('metadata'),
+ immutableFlag: 'lsifMPTMetadata',
+ },
+ {
+ key: 'transferFee',
+ label: t('transfer_fee'),
+ immutableFlag: 'lsifMPTTransferFee',
},
]
@@ -60,17 +104,114 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
{t('settings')}
+ {isDynamicMPTEnabled && (
+
{t('capabilities')}
+ )}
{flagItems.map((flag) => (
{flag.label}
-
- {flag.enabled ? t('enabled') : t('disabled')}
+
+ {flag.enabled ? (
+
+ showPillTooltip(e, t('enabled_capability_tooltip'))
+ : undefined
+ }
+ onFocus={() => {}}
+ onMouseLeave={isDynamicMPTEnabled ? hideTooltip : undefined}
+ >
+ {t('enabled')}
+
+ ) : (
+ <>
+
+ showPillTooltip(e, t('disabled_capability_tooltip'))
+ : undefined
+ }
+ onFocus={() => {}}
+ onMouseLeave={isDynamicMPTEnabled ? hideTooltip : undefined}
+ >
+ {t('disabled')}
+
+ {isDynamicMPTEnabled &&
+ (isLocked(flag.immutableFlag) ? (
+
+ showPillTooltip(e, t('immutable_capability_tooltip'))
+ }
+ onFocus={() => {}}
+ onMouseLeave={hideTooltip}
+ >
+ {t('immutable')}
+
+ ) : (
+
+ showPillTooltip(e, t('mutable_capability_tooltip'))
+ }
+ onFocus={() => {}}
+ onMouseLeave={hideTooltip}
+ >
+ {t('mutable')}
+
+ ))}
+ >
+ )}
))}
+ {isDynamicMPTEnabled && (
+ <>
+
{t('fields')}
+ {fieldItems.map((field) => (
+
+
{field.label}
+
+ {isLocked(field.immutableFlag) ? (
+
+ showPillTooltip(e, t('immutable_field_tooltip'))
+ }
+ onFocus={() => {}}
+ onMouseLeave={hideTooltip}
+ >
+ {t('immutable')}
+
+ ) : (
+
+ showPillTooltip(e, t('mutable_field_tooltip'))
+ }
+ onFocus={() => {}}
+ onMouseLeave={hideTooltip}
+ >
+ {t('mutable')}
+
+ )}
+
+
+ ))}
+ >
+ )}
+
)
}
diff --git a/src/containers/Token/MPT/Header/index.tsx b/src/containers/Token/MPT/Header/index.tsx
index 00106edc7..b7e0c4f24 100644
--- a/src/containers/Token/MPT/Header/index.tsx
+++ b/src/containers/Token/MPT/Header/index.tsx
@@ -38,6 +38,7 @@ interface Props {
setError: (error: number | null) => void
holdersCount?: number
holdersLoading?: boolean
+ isDynamicMPTEnabled?: boolean
circulatingSupply?: string
circulatingSupplyLoading?: boolean
}
@@ -51,6 +52,7 @@ export const Header = (props: Props) => {
setError,
holdersCount,
holdersLoading,
+ isDynamicMPTEnabled,
circulatingSupply,
circulatingSupplyLoading,
} = props
@@ -96,6 +98,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt,
transferFee,
flags,
+ immutableFlags,
rawMPTMetadata,
parsedMPTMetadata,
isMPTMetadataCompliant,
@@ -223,6 +226,7 @@ export const Header = (props: Props) => {
showMptId={showMPTIssuanceId}
holdersCount={holdersCount}
holdersLoading={holdersLoading}
+ flags={flags}
/>
{
circulatingSupply={circulatingSupply}
circulatingSupplyLoading={circulatingSupplyLoading}
/>
-
+
{(parsedMPTMetadata || rawMPTMetadata) && (
{
const [error, setError] = useState(null)
const rippledSocket = useContext(SocketContext)
+ // Check whether the DynamicMPT amendment is active on the connected network
+ const { data: isDynamicMPTEnabled } = useQuery(
+ ['isDynamicMPTEnabled', rippledSocket],
+ async () => {
+ const result = await getFeature(rippledSocket, DYNAMIC_MPT_AMENDMENT_ID)
+ return result?.[DYNAMIC_MPT_AMENDMENT_ID]?.enabled === true
+ },
+ { staleTime: Infinity },
+ )
+
// Holders pagination state
const [holdersPage, setHoldersPage] = useState(INITIAL_PAGE)
const holdersPageSize = PAGINATION_CONFIG.HOLDERS_PAGE_SIZE
@@ -200,6 +214,7 @@ export const MPT = () => {
setError={setError}
holdersCount={holdersData?.totalHolders}
holdersLoading={holdersLoading}
+ isDynamicMPTEnabled={isDynamicMPTEnabled}
circulatingSupply={circulatingSupply}
circulatingSupplyLoading={circulatingSupplyLoading}
/>
diff --git a/src/containers/Token/MPT/test/Header/Settings.test.tsx b/src/containers/Token/MPT/test/Header/Settings.test.tsx
index 9d76e084c..11e13ad06 100644
--- a/src/containers/Token/MPT/test/Header/Settings.test.tsx
+++ b/src/containers/Token/MPT/test/Header/Settings.test.tsx
@@ -7,7 +7,11 @@ describe('Settings component', () => {
const renderComponent = (props: any = {}) =>
render(
-
+
,
)
@@ -21,84 +25,238 @@ describe('Settings component', () => {
)
})
- it('renders all 7 flag items', () => {
- const { container } = renderComponent()
- expect(container.querySelectorAll('.header-box-item')).toHaveLength(8)
+ // ── Amendment gate ───────────────────────────────────────────────────────────
+
+ it('renders only 7 capability rows when DynamicMPT is not enabled', () => {
+ const { container } = renderComponent({ isDynamicMPTEnabled: false })
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(7)
+ })
+
+ it('renders 7 capability rows and 2 field rows (9 total) when DynamicMPT is enabled', () => {
+ const { container } = renderComponent({ isDynamicMPTEnabled: true })
+ expect(container.querySelectorAll('.header-box-item')).toHaveLength(9)
})
- it('shows locked flag as disabled by default', () => {
- const { container } = renderComponent({ flags: [] })
- expect(container).toHaveTextContent('locked')
- expect(container).toHaveTextContent('disabled')
+ it('shows no mutable/immutable pills when DynamicMPT is not enabled', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ isDynamicMPTEnabled: false,
+ })
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(0)
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(0)
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(7)
})
- it('shows locked flag as enabled when present', () => {
- const { container } = renderComponent({ flags: ['lsfMPTLocked'] })
- const flagItems = container.querySelectorAll('.header-box-item')
- const lockedItem = flagItems[0]
- expect(lockedItem.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ // ── Capability enabled state ─────────────────────────────────────────────────
+
+ it('shows only Enabled badge for an enabled capability — no second pill', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: ['lsfMPTCanLock'],
+ isDynamicMPTEnabled: true,
+ })
+ // Enabled caps show one pill only; 6 disabled caps each show Disabled + Mutable; 2 fields Mutable
+ expect(queryAllByTestId('enabled-badge')).toHaveLength(1)
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(6)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(6 + 2)
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(0)
})
- it('shows can_lock flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTCanLock'] })
+ it('shows Enabled for can_lock when lsfMPTCanLock present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTCanLock'],
+ isDynamicMPTEnabled: true,
+ })
expect(container).toHaveTextContent('can_lock')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows require_auth flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTRequireAuth'] })
+ it('shows Enabled for require_auth when lsfMPTRequireAuth present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTRequireAuth'],
+ isDynamicMPTEnabled: true,
+ })
expect(container).toHaveTextContent('require_auth')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows can_escrow flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTCanEscrow'] })
- expect(container).toHaveTextContent('can_escrow')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ it('shows Enabled for can_escrow when lsfMPTCanEscrow present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTCanEscrow'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows can_trade flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTCanTrade'] })
- expect(container).toHaveTextContent('can_trade')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ it('shows Enabled for can_trade when lsfMPTCanTrade present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTCanTrade'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows can_transfer flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTCanTransfer'] })
- expect(container).toHaveTextContent('can_transfer')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ it('shows Enabled for can_transfer when lsfMPTCanTransfer present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTCanTransfer'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows can_clawback flag status', () => {
- const { container } = renderComponent({ flags: ['lsfMPTCanClawback'] })
- expect(container).toHaveTextContent('can_clawback')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ it('shows Enabled for can_clawback when lsfMPTCanClawback present', () => {
+ const { container } = renderComponent({
+ flags: ['lsfMPTCanClawback'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('shows can_confidential_amount flag status', () => {
+ it('shows Enabled for can_confidential_amount when lsfMPTCanHoldConfidentialBalance present', () => {
const { container } = renderComponent({
- flags: ['lsfMPTCanConfidentialAmount'],
+ flags: ['lsfMPTCanHoldConfidentialBalance'],
+ isDynamicMPTEnabled: true,
})
- expect(container).toHaveTextContent('can_confidential_amount')
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1)
+ expect(queryAllEnabled(container)).toHaveLength(1)
})
- it('handles multiple flags enabled', () => {
- const { container } = renderComponent({
+ it('handles multiple enabled capabilities', () => {
+ const { queryAllByTestId } = renderComponent({
flags: ['lsfMPTCanTransfer', 'lsfMPTCanTrade', 'lsfMPTCanLock'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllByTestId('enabled-badge')).toHaveLength(3)
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(4)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(4 + 2) // 4 disabled caps + 2 fields
+ })
+
+ // ── Disabled + Mutable (default state, DynamicMPT on) ───────────────────────
+
+ it('shows Disabled + Mutable on every capability when nothing is set', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ isDynamicMPTEnabled: true,
})
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(3)
- expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(5)
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(7)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(7 + 2) // 7 caps + 2 fields
+ expect(queryAllByTestId('enabled-badge')).toHaveLength(0)
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(0)
})
- it('handles empty flags array', () => {
- const { container } = renderComponent({ flags: [] })
- expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(0)
- expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8)
+ it('shows Disabled + Mutable with undefined flags (DynamicMPT on)', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: undefined,
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(7)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(9)
})
- it('handles undefined flags', () => {
- const { container } = renderComponent({ flags: undefined })
- expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8)
+ // ── Disabled + Immutable (locked capabilities) ───────────────────────────────
+
+ it('shows Disabled + Immutable for a locked capability', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTCanLock'],
+ isDynamicMPTEnabled: true,
+ })
+ // canLock locked: Disabled + Immutable; 6 others: Disabled + Mutable; 2 fields: Mutable
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(1)
+ expect(queryAllByTestId('disabled-badge')).toHaveLength(7)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(6 + 2)
+ })
+
+ it('shows Disabled + Immutable for locked confidential capability', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTCanHoldConfidentialBalance'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(1)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(6 + 2)
+ })
+
+ it('shows Immutable on multiple locked capabilities', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: [
+ 'lsifMPTCanLock',
+ 'lsifMPTRequireAuth',
+ 'lsifMPTCanEscrow',
+ ],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(3)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(4 + 2)
+ })
+
+ // ── Field rows (DynamicMPT on) ───────────────────────────────────────────────
+
+ it('shows Mutable on both field rows when neither is locked', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: [],
+ isDynamicMPTEnabled: true,
+ })
+ // 7 caps (Mutable) + 2 fields (Mutable) = 9
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(9)
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(0)
+ })
+
+ it('shows Immutable on metadata field when lsifMPTMetadata is locked', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTMetadata'],
+ isDynamicMPTEnabled: true,
+ })
+ // 7 cap Mutable + 1 field Immutable (metadata) + 1 field Mutable (transferFee)
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(1)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(7 + 1)
+ })
+
+ it('shows Immutable on both field rows when both are locked', () => {
+ const { queryAllByTestId } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTMetadata', 'lsifMPTTransferFee'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(queryAllByTestId('immutable-badge')).toHaveLength(2)
+ expect(queryAllByTestId('mutable-badge')).toHaveLength(7) // only caps
+ })
+
+ it('always renders both field rows regardless of immutableFlags when DynamicMPT is enabled', () => {
+ const { container: locked } = renderComponent({
+ immutableFlags: ['lsifMPTMetadata', 'lsifMPTTransferFee'],
+ isDynamicMPTEnabled: true,
+ })
+ const { container: unlocked } = renderComponent({
+ immutableFlags: [],
+ isDynamicMPTEnabled: true,
+ })
+ expect(locked.querySelectorAll('.header-box-item')).toHaveLength(9)
+ expect(unlocked.querySelectorAll('.header-box-item')).toHaveLength(9)
+ })
+
+ // ── Pill class names ─────────────────────────────────────────────────────────
+
+ it('uses correct CSS class for immutable pill', () => {
+ const { container } = renderComponent({
+ flags: [],
+ immutableFlags: ['lsifMPTCanLock'],
+ isDynamicMPTEnabled: true,
+ })
+ expect(container.querySelectorAll('.flag-status.immutable')).toHaveLength(1)
+ })
+
+ it('uses correct CSS class for mutable pill', () => {
+ const { container } = renderComponent({
+ flags: [],
+ immutableFlags: [],
+ isDynamicMPTEnabled: true,
+ })
+ expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(9)
})
})
+
+function queryAllEnabled(container: HTMLElement) {
+ return container.querySelectorAll('.flag-status.enabled')
+}
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..23cee5929 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..8aa49165d 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts
@@ -1,19 +1,27 @@
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'
export const parser: TransactionParser<
MPTokenIssuanceCreate,
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..39b13537e 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-immutable-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-immutable-flags', 'tifMPTCanLock')
+ expectSimpleRowText(container, 'mpt-immutable-flags', 'tifMPTMetadata')
+ expectSimpleRowText(container, 'mpt-immutable-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..78f43bf5b 100644
--- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
+++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx
@@ -4,23 +4,44 @@ import { SimpleRow } from '../SimpleRow'
import { TransactionSimpleComponent, TransactionSimpleProps } from '../types'
import { Account } from '../../Account'
import { MPTokenLink } from '../../MPTokenLink'
-import { shortenEncryptionKey } from '../../../utils'
-
-interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet {
- IssuerEncryptionKey?: string
- AuditorEncryptionKey?: string
-}
+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'
export const Simple: TransactionSimpleComponent = ({
data,
-}: TransactionSimpleProps) => {
+}: TransactionSimpleProps) => {
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 +53,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 16 (0x10) = tifMPTCanTrade
+ expectSimpleRowText(container, 'mpt-immutable-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..b84dca60e
--- /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": 16,
+ "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..5d0288bda 100644
--- a/src/containers/shared/transactionUtils.ts
+++ b/src/containers/shared/transactionUtils.ts
@@ -74,8 +74,14 @@ 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',
+ 0x00000008: 'tfMPTSetRequireAuth',
+ 0x00000010: 'tfMPTSetCanEscrow',
+ 0x00000020: 'tfMPTSetCanTrade',
+ 0x00000040: 'tfMPTSetCanTransfer',
+ 0x00000080: 'tfMPTSetCanClawback',
+ 0x00000100: 'tfMPTSetCanHoldConfidentialBalance',
},
NFTokenMint: {
0x00000001: 'tfBurnable',
@@ -119,6 +125,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/test/utils.test.ts b/src/rippled/lib/test/utils.test.ts
index 370251961..0b543f0be 100644
--- a/src/rippled/lib/test/utils.test.ts
+++ b/src/rippled/lib/test/utils.test.ts
@@ -188,7 +188,7 @@ describe('rippled utils:', () => {
OutstandingAmount: '500000000000',
TransferFee: 500,
Sequence: 1,
- Flags: 0x7f, // All flags
+ Flags: 0xff, // All lsf flags incl. lsfMPTCanHoldConfidentialBalance (0x80)
MPTokenMetadata: metadataHex,
}
@@ -207,6 +207,8 @@ describe('rippled utils:', () => {
expect(result.flags).toContain('lsfMPTCanTrade')
expect(result.flags).toContain('lsfMPTCanTransfer')
expect(result.flags).toContain('lsfMPTCanClawback')
+ expect(result.flags).toContain('lsfMPTCanHoldConfidentialBalance')
+ expect(result.immutableFlags).toHaveLength(0)
expect(result.rawMPTMetadata).toBe('{"t":"USD","in":"Test Issuer"}')
expect(result.parsedMPTMetadata).toEqual({
ticker: 'USD',
@@ -215,5 +217,53 @@ describe('rippled utils:', () => {
// isMPTMetadataCompliant is false because the test metadata doesn't meet full XLS-89 requirements
expect(result.isMPTMetadataCompliant).toBe(false)
})
+
+ it('should decode ImmutableFlags into lsif* flag names', () => {
+ // 0x10006 = lsifMPTCanLock(0x2) + lsifMPTRequireAuth(0x4) + lsifMPTMetadata(0x10000)
+ const result = formatMPTIssuance({
+ Issuer: 'rMPTTestAccount123456789',
+ AssetScale: 6,
+ TransferFee: 0,
+ Sequence: 1,
+ Flags: 0,
+ ImmutableFlags: 0x00010006,
+ } as any)
+
+ expect(result.immutableFlags).toContain('lsifMPTCanLock')
+ expect(result.immutableFlags).toContain('lsifMPTRequireAuth')
+ expect(result.immutableFlags).toContain('lsifMPTMetadata')
+ expect(result.immutableFlags).not.toContain('lsifMPTCanEscrow')
+ expect(result.immutableFlags).not.toContain('lsifMPTCanTrade')
+ expect(result.immutableFlags).not.toContain('lsifMPTCanTransfer')
+ expect(result.immutableFlags).not.toContain('lsifMPTCanClawback')
+ expect(result.immutableFlags).not.toContain(
+ 'lsifMPTCanHoldConfidentialBalance',
+ )
+ expect(result.immutableFlags).not.toContain('lsifMPTTransferFee')
+ })
+
+ it('should decode all ImmutableFlags when fully set', () => {
+ // 0x300fe = all 7 capability bits (0xfe) + both field bits (0x10000 | 0x20000)
+ const result = formatMPTIssuance({
+ Issuer: 'rMPTTestAccount123456789',
+ AssetScale: 6,
+ TransferFee: 0,
+ Sequence: 1,
+ Flags: 0,
+ ImmutableFlags: 0x000300fe,
+ } as any)
+
+ expect(result.immutableFlags).toContain('lsifMPTCanLock')
+ expect(result.immutableFlags).toContain('lsifMPTRequireAuth')
+ expect(result.immutableFlags).toContain('lsifMPTCanEscrow')
+ expect(result.immutableFlags).toContain('lsifMPTCanTrade')
+ expect(result.immutableFlags).toContain('lsifMPTCanTransfer')
+ expect(result.immutableFlags).toContain('lsifMPTCanClawback')
+ expect(result.immutableFlags).toContain(
+ 'lsifMPTCanHoldConfidentialBalance',
+ )
+ expect(result.immutableFlags).toContain('lsifMPTMetadata')
+ expect(result.immutableFlags).toContain('lsifMPTTransferFee')
+ })
})
})
diff --git a/src/rippled/lib/utils.ts b/src/rippled/lib/utils.ts
index 823870688..3a46d09ad 100644
--- a/src/rippled/lib/utils.ts
+++ b/src/rippled/lib/utils.ts
@@ -45,7 +45,22 @@ const MPT_ISSUANCE_FLAGS: FlagMap = {
0x00000010: 'lsfMPTCanTrade',
0x00000020: 'lsfMPTCanTransfer',
0x00000040: 'lsfMPTCanClawback',
- 0x00000080: 'lsfMPTCanConfidentialAmount',
+ 0x00000080: 'lsfMPTCanHoldConfidentialBalance',
+}
+// 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',
@@ -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,10 @@ 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,
}