From 508352cc529216056d320fce22b8ee94a6605a75 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Wed, 1 Jul 2026 11:48:16 -0400 Subject: [PATCH 1/3] initial commit --- public/locales/ca-CA/translations.json | 4 +- public/locales/en-US/translations.json | 4 +- public/locales/es-ES/translations.json | 4 +- public/locales/fr-FR/translations.json | 4 +- public/locales/ja-JP/translations.json | 4 +- public/locales/ko-KR/translations.json | 4 +- public/locales/my-MM/translations.json | 4 +- src/containers/Token/MPT/Header/Settings.tsx | 63 ++++++++++++++-- src/containers/Token/MPT/Header/index.tsx | 3 +- src/containers/Token/MPT/Header/styles.scss | 13 ++++ .../Token/MPT/test/Header/Settings.test.tsx | 26 ++++++- src/containers/shared/Interfaces.tsx | 1 + .../MPTokenIssuanceCreate/Simple.tsx | 15 +++- .../MPTokenIssuanceCreate/parser.ts | 39 ++++++---- .../test/MPTokenIssuanceCreateSimple.test.jsx | 17 ++++- .../MPTokenIssuanceCreate_Dynamic.json | 51 +++++++++++++ .../MPTokenIssuanceCreate/types.ts | 2 + .../Transaction/MPTokenIssuanceSet/Simple.tsx | 54 +++++++++++++- .../test/MPTokenIssuanceSetSimple.test.jsx | 21 ++++++ .../mock_data/MPTokenIssuanceSet_Dynamic.json | 73 +++++++++++++++++++ src/containers/shared/transactionUtils.ts | 31 ++++++++ src/rippled/lib/utils.ts | 17 +++++ 22 files changed, 423 insertions(+), 31 deletions(-) create mode 100644 src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json create mode 100644 src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 0f97a1a6f..31ea9f5f1 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -943,5 +943,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index bf49cd4b1..ea10e4970 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -944,5 +944,7 @@ "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" } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 2970d7e49..6e2242beb 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -944,5 +944,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index f68c80ccb..2a304dc7d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -944,5 +944,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index 27359a32f..5cefe5dd3 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -944,5 +944,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index 33b9df6f0..a6e262777 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -944,5 +944,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json index 5a597442e..45c7a4a12 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -944,5 +944,7 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null } diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx index fac9ee031..6f6f9d600 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,32 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { {flagItems.map((flag) => (
{flag.label}
-
- {flag.enabled ? t('enabled') : t('disabled')} +
+ {isMutable(flag.mutableFlag) && ( +
+ {t('mutable')} +
+ )} +
+ {flag.enabled ? t('enabled') : t('disabled')} +
+
+
+ ))} + {mutableFieldItems.map((field) => ( +
+
{field.label}
+
+
+ {t('mutable')} +
))} 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,28 @@ 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 capability flag as mutable', () => { + const { container, getAllByTestId } = renderComponent({ + flags: ['lsfMPTCanLock'], + mutableFlags: ['lsmfMPTCanEnableCanLock'], + }) + expect(getAllByTestId('mutable-badge')).toHaveLength(1) + expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(1) + }) + + 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..d4187819f 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,11 @@ export const Simple: TransactionSimpleComponent = ({ )} )} + {mutableFlags && mutableFlags.length > 0 && ( + + {mutableFlags.join(', ')} + + )} ) } 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..0fdae8b82 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,20 @@ 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', + 'tmfMPTCanMutateTransferFee, tmfMPTCanMutateMetadata, tmfMPTCanEnableCanLock', + ) 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..486aa66a6 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,29 @@ export const Simple: TransactionSimpleComponent = ({ )} + {TransferFee != null && ( + + {formattedFee} + + )} + {metadata && ( + + {isValidJsonString(metadata) ? ( + + ) : ( + metadata + )} + + )} + {flagChanges.length > 0 && ( + + {flagChanges.join(', ')} + + )} {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', + 'tmfMPTSetCanTransfer, tmfMPTSetCanLock', + ) + 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, } From e0548984eb054c61ab33aa629381da87a64e0229 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Mon, 6 Jul 2026 11:44:45 -0400 Subject: [PATCH 2/3] styling updates --- public/locales/ca-CA/translations.json | 4 +++- public/locales/en-US/translations.json | 4 +++- public/locales/es-ES/translations.json | 4 +++- public/locales/fr-FR/translations.json | 4 +++- public/locales/ja-JP/translations.json | 4 +++- public/locales/ko-KR/translations.json | 4 +++- public/locales/my-MM/translations.json | 4 +++- src/containers/Token/MPT/Header/Settings.tsx | 11 +++++++++-- .../Token/MPT/test/Header/Settings.test.tsx | 14 ++++++++++++-- .../Transaction/MPTokenIssuanceCreate/Simple.tsx | 10 ++++++++-- .../test/MPTokenIssuanceCreateSimple.test.jsx | 12 +++++++++++- .../Transaction/MPTokenIssuanceSet/Simple.tsx | 10 ++++++++-- .../test/MPTokenIssuanceSetSimple.test.jsx | 7 ++----- 13 files changed, 71 insertions(+), 21 deletions(-) diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 31ea9f5f1..3467d89c4 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -945,5 +945,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 ea10e4970..3dda681f4 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).", "maintenance_banner.countdown_prefix": "Starts in", "mutable": "Mutable", - "mutable_flags": "Mutable Flags" + "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 6e2242beb..8b7384fa2 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 2a304dc7d..cb36a2f7d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 5cefe5dd3..db73a2754 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 a6e262777..d230bea89 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 45c7a4a12..803924dba 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -946,5 +946,7 @@ "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, "mutable": null, - "mutable_flags": 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 6f6f9d600..d2a3697fd 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -95,10 +95,13 @@ export const Settings = ({
{flag.label}
- {isMutable(flag.mutableFlag) && ( + {/* 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')}
@@ -117,7 +120,11 @@ export const Settings = ({
{field.label}
-
+
{t('mutable')}
diff --git a/src/containers/Token/MPT/test/Header/Settings.test.tsx b/src/containers/Token/MPT/test/Header/Settings.test.tsx index 1b948d418..26f649ada 100644 --- a/src/containers/Token/MPT/test/Header/Settings.test.tsx +++ b/src/containers/Token/MPT/test/Header/Settings.test.tsx @@ -107,15 +107,25 @@ describe('Settings component', () => { expect(queryAllByTestId('mutable-badge')).toHaveLength(0) }) - it('marks a capability flag as mutable', () => { + it('marks a disabled capability flag as mutable', () => { const { container, getAllByTestId } = renderComponent({ - flags: ['lsfMPTCanLock'], + 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: [], diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx index d4187819f..f3814f4b7 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx @@ -71,8 +71,14 @@ export const Simple: TransactionSimpleComponent = ({ )} {mutableFlags && mutableFlags.length > 0 && ( - - {mutableFlags.join(', ')} + + {mutableFlags.map((flag) => ( +
{flag}
+ ))}
)} 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 0fdae8b82..bbf6a1535 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx @@ -35,7 +35,17 @@ describe('MPTokenIssuanceCreate', () => { expectSimpleRowText( container, 'mpt-mutable-flags', - 'tmfMPTCanMutateTransferFee, tmfMPTCanMutateMetadata, tmfMPTCanEnableCanLock', + 'tmfMPTCanEnableCanLock', + ) + expectSimpleRowText( + container, + 'mpt-mutable-flags', + 'tmfMPTCanMutateMetadata', + ) + expectSimpleRowText( + container, + 'mpt-mutable-flags', + 'tmfMPTCanMutateTransferFee', ) unmount() }) diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx index 486aa66a6..0798544d4 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx @@ -80,8 +80,14 @@ export const Simple: TransactionSimpleComponent = ({
)} {flagChanges.length > 0 && ( - - {flagChanges.join(', ')} + + {flagChanges.map((flag) => ( +
{flag}
+ ))}
)} {IssuerEncryptionKey && ( diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx index 75ae64ea3..90dbce75e 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx @@ -48,11 +48,8 @@ describe('MPTokenIssuanceSet', () => { expectSimpleRowText(container, 'mpt-fee', '0.500%') expectSimpleRowText(container, 'mpt-metadata', 'FOO') // MutableFlags 17 = tmfMPTSetCanTransfer (0x10) + tmfMPTSetCanLock (0x1) - expectSimpleRowText( - container, - 'mpt-mutable-flags', - 'tmfMPTSetCanTransfer, tmfMPTSetCanLock', - ) + expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanLock') + expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanTransfer') expectSimpleRowNotToExist(container, 'mpt-holder') unmount() }) From bc04c5a11c8d029d56b03d76781749bdd5c8b6a7 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Wed, 12 Aug 2026 16:13:25 -0400 Subject: [PATCH 3/3] update to use new spec --- src/containers/Token/MPT/Header/Settings.tsx | 42 ++++++++------- src/containers/Token/MPT/Header/index.tsx | 4 +- .../Token/MPT/test/Header/Settings.test.tsx | 53 +++++++++++++------ src/containers/shared/Interfaces.tsx | 2 +- .../MPTokenIssuanceCreate/Simple.tsx | 6 +-- .../MPTokenIssuanceCreate/parser.ts | 8 +-- .../test/MPTokenIssuanceCreateSimple.test.jsx | 22 ++------ .../MPTokenIssuanceCreate_Dynamic.json | 4 +- .../MPTokenIssuanceCreate/types.ts | 4 +- .../Transaction/MPTokenIssuanceSet/Simple.tsx | 8 +-- .../test/MPTokenIssuanceSetSimple.test.jsx | 5 +- .../mock_data/MPTokenIssuanceSet_Dynamic.json | 4 +- src/containers/shared/transactionUtils.ts | 53 +++++++++---------- src/rippled/lib/utils.ts | 33 ++++++------ 14 files changed, 127 insertions(+), 121 deletions(-) diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx index d2a3697fd..582ae96f2 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -2,26 +2,27 @@ import { useTranslation } from 'react-i18next' interface Props { flags?: string[] - mutableFlags?: string[] + immutableFlags?: 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 + // 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 = [], - mutableFlags = [], + immutableFlags = [], }: Props): JSX.Element => { const { t } = useTranslation() - const isMutable = (mutableFlag?: string): boolean => - !!mutableFlag && mutableFlags.includes(mutableFlag) + // 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[] = [ { @@ -33,37 +34,37 @@ export const Settings = ({ key: 'canLock', label: t('can_lock'), enabled: flags.includes('lsfMPTCanLock'), - mutableFlag: 'lsmfMPTCanEnableCanLock', + immutableFlag: 'lsifMPTCanLock', }, { key: 'requireAuth', label: t('require_auth'), enabled: flags.includes('lsfMPTRequireAuth'), - mutableFlag: 'lsmfMPTCanEnableRequireAuth', + immutableFlag: 'lsifMPTRequireAuth', }, { key: 'canEscrow', label: t('can_escrow'), enabled: flags.includes('lsfMPTCanEscrow'), - mutableFlag: 'lsmfMPTCanEnableCanEscrow', + immutableFlag: 'lsifMPTCanEscrow', }, { key: 'canTrade', label: t('can_trade'), enabled: flags.includes('lsfMPTCanTrade'), - mutableFlag: 'lsmfMPTCanEnableCanTrade', + immutableFlag: 'lsifMPTCanTrade', }, { key: 'canTransfer', label: t('can_transfer'), enabled: flags.includes('lsfMPTCanTransfer'), - mutableFlag: 'lsmfMPTCanEnableCanTransfer', + immutableFlag: 'lsifMPTCanTransfer', }, { key: 'canClawback', label: t('can_clawback'), enabled: flags.includes('lsfMPTCanClawback'), - mutableFlag: 'lsmfMPTCanEnableCanClawback', + immutableFlag: 'lsifMPTCanClawback', }, { key: 'canConfidentialAmount', @@ -72,20 +73,20 @@ export const Settings = ({ }, ] - // Mutable fields (Dynamic MPT) that are not capability flags. Only shown when - // the issuer declared them mutable at creation. + // 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'), - mutableFlag: 'lsmfMPTCanMutateMetadata', + immutableFlag: 'lsifMPTMetadata', }, { key: 'transferFee', label: t('transfer_fee'), - mutableFlag: 'lsmfMPTCanMutateTransferFee', + immutableFlag: 'lsifMPTTransferFee', }, - ].filter((item) => isMutable(item.mutableFlag)) + ].filter((item) => isStillMutable(item.immutableFlag)) return (
@@ -96,8 +97,9 @@ export const Settings = ({
{flag.label}
{/* 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 && ( + 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 && (
{ confidentialOutstandingAmt, transferFee, flags, - mutableFlags, + immutableFlags, rawMPTMetadata, parsedMPTMetadata, isMPTMetadataCompliant, @@ -236,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', () => { @@ -102,18 +104,25 @@ describe('Settings component', () => { 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('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('marks a disabled capability flag as mutable', () => { + it('hides the mutable badge for a capability once it is locked (in immutableFlags)', () => { const { container, getAllByTestId } = renderComponent({ flags: [], - mutableFlags: ['lsmfMPTCanEnableCanLock'], + immutableFlags: ['lsifMPTCanLock'], }) - expect(getAllByTestId('mutable-badge')).toHaveLength(1) - expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(1) + // 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', () => { @@ -121,18 +130,30 @@ describe('Settings component', () => { // already-enabled flag would be misleading. const { queryAllByTestId } = renderComponent({ flags: ['lsfMPTCanLock'], - mutableFlags: ['lsmfMPTCanEnableCanLock'], + immutableFlags: [], }) - expect(queryAllByTestId('mutable-badge')).toHaveLength(0) + // 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('renders extra rows for mutable metadata and transfer fee', () => { + it('shows extra rows for mutable metadata and transfer fee when not locked', () => { const { container, getAllByTestId } = renderComponent({ flags: [], - mutableFlags: ['lsmfMPTCanMutateMetadata', 'lsmfMPTCanMutateTransferFee'], + immutableFlags: [], }) - // 8 capability rows + metadata + transfer fee + // 8 capability rows + metadata + transfer fee (both still mutable) expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) - expect(getAllByTestId('mutable-badge')).toHaveLength(2) + // 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 75d0066da..02d0bf7b9 100644 --- a/src/containers/shared/Interfaces.tsx +++ b/src/containers/shared/Interfaces.tsx @@ -48,7 +48,7 @@ export interface FormattedMPTIssuance { outstandingAmt?: string confidentialOutstandingAmt?: string flags?: string[] - mutableFlags?: 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 f3814f4b7..6caec6c41 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx @@ -17,7 +17,7 @@ export const Simple: TransactionSimpleComponent = ({ assetScale, transferFee, maxAmount, - mutableFlags, + immutableFlags, } = data.instructions const { t } = useTranslation() const language = useLanguage() @@ -70,13 +70,13 @@ export const Simple: TransactionSimpleComponent = ({ )} )} - {mutableFlags && mutableFlags.length > 0 && ( + {immutableFlags && immutableFlags.length > 0 && ( - {mutableFlags.map((flag) => ( + {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 79623c588..1bfb17585 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts @@ -5,18 +5,18 @@ import { buildFlags, convertHexToString, } from '../../../../../rippled/lib/utils' -import { MPT_CREATE_MUTABLE_FLAGS } from '../../../transactionUtils' +import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils' // TODO: use MPTokenIssuanceCreate when DynamicMPT is supported on xrpl.js interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate { - MutableFlags?: number + ImmutableFlags?: number } export const parser: TransactionParser< MPTokenIssuanceCreateExtended, MPTokenIssuanceCreateInstructions > = (tx, meta) => { - const mutableFlags = buildFlags(tx.MutableFlags, MPT_CREATE_MUTABLE_FLAGS) + const immutableFlags = buildFlags(tx.ImmutableFlags, MPT_IMMUTABLE_FLAGS) return { issuanceID: meta.mpt_issuance_id, metadata: tx.MPTokenMetadata @@ -27,6 +27,6 @@ export const parser: TransactionParser< maxAmount: tx.MaximumAmount ? BigInt(tx.MaximumAmount).toString(10) : undefined, - mutableFlags: mutableFlags.length ? mutableFlags : 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 bbf6a1535..bb8cbfaa2 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx @@ -30,23 +30,11 @@ describe('MPTokenIssuanceCreate', () => { 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', - ) + // 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 index 502acbb39..be5b44f6e 100644 --- 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 @@ -6,7 +6,7 @@ "Flags": 34, "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70", "MaximumAmount": "9223372036854775807", - "MutableFlags": 196610, + "ImmutableFlags": 196610, "Sequence": 5496, "SigningPubKey": "EDAD408FAEE57EB4A347E6FE395B834DD47C6531C3C37B09ACC35528161CAD4B0E", "TransactionType": "MPTokenIssuanceCreate", @@ -34,7 +34,7 @@ "Issuer": "rfGb6p2kWy3zQweWnYNxSFYoHeymcx7mhg", "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70", "MaximumAmount": "9223372036854775807", - "MutableFlags": 196610, + "ImmutableFlags": 196610, "Sequence": 5496, "TransferFee": 10 } diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts index 0239422ff..caa4d9912 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts @@ -4,6 +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[] + // 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 0798544d4..9bd4e94bf 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx @@ -15,12 +15,12 @@ import { buildFlags, convertHexToString, } from '../../../../../rippled/lib/utils' -import { MPT_SET_MUTABLE_FLAGS } from '../../../transactionUtils' +import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils' interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet { MPTokenMetadata?: string TransferFee?: number - MutableFlags?: number + ImmutableFlags?: number IssuerEncryptionKey?: string AuditorEncryptionKey?: string } @@ -33,7 +33,7 @@ export const Simple: TransactionSimpleComponent = ({ Holder, MPTokenMetadata, TransferFee, - MutableFlags, + ImmutableFlags, IssuerEncryptionKey, AuditorEncryptionKey, } = data.instructions @@ -49,7 +49,7 @@ export const Simple: TransactionSimpleComponent = ({ minimumFractionDigits: 3, })}%` : undefined - const flagChanges = buildFlags(MutableFlags, MPT_SET_MUTABLE_FLAGS) + const flagChanges = buildFlags(ImmutableFlags, MPT_IMMUTABLE_FLAGS) return ( <> diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx index 90dbce75e..daca7bd26 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx @@ -47,9 +47,8 @@ describe('MPTokenIssuanceSet', () => { ) 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') + // ImmutableFlags 17 (0x11) = 0x10 (tifMPTCanTrade) + 0x01 (not in MPT_IMMUTABLE_FLAGS map) + expectSimpleRowText(container, 'mpt-mutable-flags', 'tifMPTCanTrade') expectSimpleRowNotToExist(container, 'mpt-holder') unmount() }) 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 index 6c732a934..f6ec14387 100644 --- 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 @@ -4,7 +4,7 @@ "Fee": "10", "Flags": 0, "MPTokenIssuanceID": "000002609BB39CEC721B5AB337B6BD862ACD2811CBBB5F18", - "MutableFlags": 17, + "ImmutableFlags": 17, "TransferFee": 500, "MPTokenMetadata": "464F4F", "Sequence": 609, @@ -47,7 +47,7 @@ "FinalFields": { "Flags": 32, "Issuer": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM", - "MutableFlags": 131106, + "ImmutableFlags": 131106, "OutstandingAmount": "0", "OwnerNode": "0", "Sequence": 608, diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts index c83a8e173..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,35 +124,25 @@ export const TX_FLAGS: Record> = { }, } -// Dynamic MPT (XLS-94) MutableFlags fields. These are separate UInt32 fields +// Dynamic MPT (XLS-94) ImmutableFlags 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. +// 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.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', +// 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 = { diff --git a/src/rippled/lib/utils.ts b/src/rippled/lib/utils.ts index cc7f63abc..9b3991e6c 100644 --- a/src/rippled/lib/utils.ts +++ b/src/rippled/lib/utils.ts @@ -47,19 +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', +// 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', @@ -281,7 +282,7 @@ interface MPTIssuanceInfo { Sequence: number MPTokenMetadata?: string Flags: number - MutableFlags?: number + ImmutableFlags?: number IssuerEncryptionKey?: string AuditorEncryptionKey?: string } @@ -296,7 +297,7 @@ interface FormattedMPTIssuance { sequence: number metadata?: any flags: string[] - mutableFlags: string[] + immutableFlags: string[] rawMPTMetadata?: string parsedMPTMetadata?: Record isMPTMetadataCompliant: boolean @@ -328,7 +329,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), + immutableFlags: buildFlags(info.ImmutableFlags, MPT_ISSUANCE_IMMUTABLE_FLAGS), issuerEncryptionKey: info.IssuerEncryptionKey, auditorEncryptionKey: info.AuditorEncryptionKey, }