diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 0f97a1a6f..3467d89c4 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -943,5 +943,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index bf49cd4b1..3dda681f4 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -944,5 +944,9 @@ "registered": "Registered", "included": "Included", "maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).", - "maintenance_banner.countdown_prefix": "Starts in" + "maintenance_banner.countdown_prefix": "Starts in", + "mutable": "Mutable", + "mutable_flags": "Mutable Flags", + "mutable_flag_tooltip": "The issuer can enable this flag later", + "mutable_field_tooltip": "The issuer can change this value" } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 2970d7e49..8b7384fa2 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -944,5 +944,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index f68c80ccb..cb36a2f7d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -944,5 +944,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index 27359a32f..db73a2754 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -944,5 +944,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index 33b9df6f0..d230bea89 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -944,5 +944,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json index 5a597442e..803924dba 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -944,5 +944,9 @@ "registered": null, "included": null, "maintenance_banner.notice": null, - "maintenance_banner.countdown_prefix": null + "maintenance_banner.countdown_prefix": null, + "mutable": null, + "mutable_flags": null, + "mutable_flag_tooltip": null, + "mutable_field_tooltip": null } diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx index fac9ee031..d2a3697fd 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -2,17 +2,27 @@ import { useTranslation } from 'react-i18next' interface Props { flags?: string[] + mutableFlags?: string[] } interface FlagItem { key: string label: string enabled: boolean + // The lsfMPTCanMutate* flag name (Dynamic MPT, XLS-94) that marks this setting + // mutable; present only for settings that can be declared mutable. + mutableFlag?: string } -export const Settings = ({ flags = [] }: Props): JSX.Element => { +export const Settings = ({ + flags = [], + mutableFlags = [], +}: Props): JSX.Element => { const { t } = useTranslation() + const isMutable = (mutableFlag?: string): boolean => + !!mutableFlag && mutableFlags.includes(mutableFlag) + const flagItems: FlagItem[] = [ { key: 'locked', @@ -23,31 +33,37 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { key: 'canLock', label: t('can_lock'), enabled: flags.includes('lsfMPTCanLock'), + mutableFlag: 'lsmfMPTCanEnableCanLock', }, { key: 'requireAuth', label: t('require_auth'), enabled: flags.includes('lsfMPTRequireAuth'), + mutableFlag: 'lsmfMPTCanEnableRequireAuth', }, { key: 'canEscrow', label: t('can_escrow'), enabled: flags.includes('lsfMPTCanEscrow'), + mutableFlag: 'lsmfMPTCanEnableCanEscrow', }, { key: 'canTrade', label: t('can_trade'), enabled: flags.includes('lsfMPTCanTrade'), + mutableFlag: 'lsmfMPTCanEnableCanTrade', }, { key: 'canTransfer', label: t('can_transfer'), enabled: flags.includes('lsfMPTCanTransfer'), + mutableFlag: 'lsmfMPTCanEnableCanTransfer', }, { key: 'canClawback', label: t('can_clawback'), enabled: flags.includes('lsfMPTCanClawback'), + mutableFlag: 'lsmfMPTCanEnableCanClawback', }, { key: 'canConfidentialAmount', @@ -56,6 +72,21 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { }, ] + // Mutable fields (Dynamic MPT) that are not capability flags. Only shown when + // the issuer declared them mutable at creation. + const mutableFieldItems = [ + { + key: 'metadata', + label: t('metadata'), + mutableFlag: 'lsmfMPTCanMutateMetadata', + }, + { + key: 'transferFee', + label: t('transfer_fee'), + mutableFlag: 'lsmfMPTCanMutateTransferFee', + }, + ].filter((item) => isMutable(item.mutableFlag)) + return (
{t('settings')}
@@ -63,10 +94,39 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => { {flagItems.map((flag) => (
{flag.label}
-
- {flag.enabled ? t('enabled') : t('disabled')} +
+ {/* Capabilities are one-directional (can only be enabled later), + so only surface the pill while the flag is still disabled. */} + {isMutable(flag.mutableFlag) && !flag.enabled && ( +
+ {t('mutable')} +
+ )} +
+ {flag.enabled ? t('enabled') : t('disabled')} +
+
+
+ ))} + {mutableFieldItems.map((field) => ( +
+
{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,38 @@ describe('Settings component', () => { const { container } = renderComponent({ flags: undefined }) expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8) }) + + it('does not render mutable badges when no mutable flags are set', () => { + const { queryAllByTestId } = renderComponent({ flags: [] }) + expect(queryAllByTestId('mutable-badge')).toHaveLength(0) + }) + + it('marks a disabled capability flag as mutable', () => { + const { container, getAllByTestId } = renderComponent({ + flags: [], + mutableFlags: ['lsmfMPTCanEnableCanLock'], + }) + expect(getAllByTestId('mutable-badge')).toHaveLength(1) + expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(1) + }) + + it('hides the mutable badge once a capability is enabled', () => { + // Capabilities are one-directional (enable-only), so a mutable badge on an + // already-enabled flag would be misleading. + const { queryAllByTestId } = renderComponent({ + flags: ['lsfMPTCanLock'], + mutableFlags: ['lsmfMPTCanEnableCanLock'], + }) + expect(queryAllByTestId('mutable-badge')).toHaveLength(0) + }) + + it('renders extra rows for mutable metadata and transfer fee', () => { + const { container, getAllByTestId } = renderComponent({ + flags: [], + mutableFlags: ['lsmfMPTCanMutateMetadata', 'lsmfMPTCanMutateTransferFee'], + }) + // 8 capability rows + metadata + transfer fee + expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) + expect(getAllByTestId('mutable-badge')).toHaveLength(2) + }) }) diff --git a/src/containers/shared/Interfaces.tsx b/src/containers/shared/Interfaces.tsx index f9d11d00b..75d0066da 100644 --- a/src/containers/shared/Interfaces.tsx +++ b/src/containers/shared/Interfaces.tsx @@ -48,6 +48,7 @@ export interface FormattedMPTIssuance { outstandingAmt?: string confidentialOutstandingAmt?: string flags?: string[] + mutableFlags?: string[] transferFee?: number rawMPTMetadata?: string parsedMPTMetadata?: Record diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx index 28a9c62a6..f3814f4b7 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx @@ -11,8 +11,14 @@ import './styles.scss' export const Simple: TransactionSimpleComponent = ({ data, }: TransactionSimpleProps) => { - const { issuanceID, metadata, assetScale, transferFee, maxAmount } = - data.instructions + const { + issuanceID, + metadata, + assetScale, + transferFee, + maxAmount, + mutableFlags, + } = data.instructions const { t } = useTranslation() const language = useLanguage() const formattedFee = @@ -64,6 +70,17 @@ export const Simple: TransactionSimpleComponent = ({ )} )} + {mutableFlags && mutableFlags.length > 0 && ( + + {mutableFlags.map((flag) => ( +
{flag}
+ ))} +
+ )} ) } diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts index fd08c599f..79623c588 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts @@ -1,19 +1,32 @@ import type { MPTokenIssuanceCreate } from 'xrpl' import { MPTokenIssuanceCreateInstructions } from './types' import { TransactionParser } from '../types' -import { convertHexToString } from '../../../../../rippled/lib/utils' +import { + buildFlags, + convertHexToString, +} from '../../../../../rippled/lib/utils' +import { MPT_CREATE_MUTABLE_FLAGS } from '../../../transactionUtils' + +// TODO: use MPTokenIssuanceCreate when DynamicMPT is supported on xrpl.js +interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate { + MutableFlags?: number +} export const parser: TransactionParser< - MPTokenIssuanceCreate, + MPTokenIssuanceCreateExtended, MPTokenIssuanceCreateInstructions -> = (tx, meta) => ({ - issuanceID: meta.mpt_issuance_id, - metadata: tx.MPTokenMetadata - ? convertHexToString(tx.MPTokenMetadata) - : undefined, - transferFee: tx.TransferFee, - assetScale: tx.AssetScale, - maxAmount: tx.MaximumAmount - ? BigInt(tx.MaximumAmount).toString(10) - : undefined, -}) +> = (tx, meta) => { + const mutableFlags = buildFlags(tx.MutableFlags, MPT_CREATE_MUTABLE_FLAGS) + return { + issuanceID: meta.mpt_issuance_id, + metadata: tx.MPTokenMetadata + ? convertHexToString(tx.MPTokenMetadata) + : undefined, + transferFee: tx.TransferFee, + assetScale: tx.AssetScale, + maxAmount: tx.MaximumAmount + ? BigInt(tx.MaximumAmount).toString(10) + : undefined, + mutableFlags: mutableFlags.length ? mutableFlags : undefined, + } +} diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx index be888d0d6..bbf6a1535 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/MPTokenIssuanceCreateSimple.test.jsx @@ -1,7 +1,8 @@ import { createSimpleRenderFactory } from '../../test/createWrapperFactory' import { Simple } from '../Simple' -import { expectSimpleRowText } from '../../test' +import { expectSimpleRowText, expectSimpleRowNotToExist } from '../../test' import transactionSuccess from './mock_data/MPTokenIssuanceCreate.json' +import transactionDynamic from './mock_data/MPTokenIssuanceCreate_Dynamic.json' const renderComponent = createSimpleRenderFactory(Simple) @@ -22,6 +23,30 @@ describe('MPTokenIssuanceCreate', () => { 'https://ipfs.io/ipfs/QmZnjmB9Tk4xaA9E679ytrPXda3beWMLUnMB5RFj1eStLp', ) expectSimpleRowText(container, 'mpt-fee', '0.010%') + expectSimpleRowNotToExist(container, 'mpt-mutable-flags') + unmount() + }) + + it('handles Dynamic MPT MPTokenIssuanceCreate simple view', () => { + const { container, unmount } = renderComponent(transactionDynamic) + + // MutableFlags 196610 = tmfMPTCanEnableCanLock (0x2) + + // tmfMPTCanMutateMetadata (0x10000) + tmfMPTCanMutateTransferFee (0x20000) + expectSimpleRowText( + container, + 'mpt-mutable-flags', + 'tmfMPTCanEnableCanLock', + ) + expectSimpleRowText( + container, + 'mpt-mutable-flags', + 'tmfMPTCanMutateMetadata', + ) + expectSimpleRowText( + container, + 'mpt-mutable-flags', + 'tmfMPTCanMutateTransferFee', + ) unmount() }) }) diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json new file mode 100644 index 000000000..502acbb39 --- /dev/null +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/test/mock_data/MPTokenIssuanceCreate_Dynamic.json @@ -0,0 +1,51 @@ +{ + "tx": { + "Account": "rfGb6p2kWy3zQweWnYNxSFYoHeymcx7mhg", + "AssetScale": 2, + "Fee": "10", + "Flags": 34, + "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70", + "MaximumAmount": "9223372036854775807", + "MutableFlags": 196610, + "Sequence": 5496, + "SigningPubKey": "EDAD408FAEE57EB4A347E6FE395B834DD47C6531C3C37B09ACC35528161CAD4B0E", + "TransactionType": "MPTokenIssuanceCreate", + "TransferFee": 10, + "TxnSignature": "F7AA8083EE7D7EFD10E11FF5A12B73D2D45A80094AEED4B41FBF2A90C9E03E5E4D162E91FA1EC156BF94E770E70E7633DF09665A5C2D5408178FC376BFC9B100", + "ctid": "C000157A00000000", + "date": 1710949602000, + "hash": "undefined", + "inLedger": "undefined", + "ledger_index": "undefined", + "meta": "undefined", + "validated": "undefined", + "metaData": "undefined", + "status": "undefined" + }, + "meta": { + "AffectedNodes": [ + { + "CreatedNode": { + "LedgerEntryType": "MPTokenIssuance", + "LedgerIndex": "265CEA78D8246B8B51D5CCC20AF4DB95569DE09E53115C888B176A3D1D05048A", + "NewFields": { + "AssetScale": 2, + "Flags": 34, + "Issuer": "rfGb6p2kWy3zQweWnYNxSFYoHeymcx7mhg", + "MPTokenMetadata": "68747470733A2F2F697066732E696F2F697066732F516D5A6E6A6D4239546B34786141394536373979747250586461336265574D4C556E4D423552466A316553744C70", + "MaximumAmount": "9223372036854775807", + "MutableFlags": 196610, + "Sequence": 5496, + "TransferFee": 10 + } + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS", + "mpt_issuance_id": "0000157844C3F3B57A8B579FEE1033CC8E8498729D063617" + }, + "hash": "9686DA1322D2D8F9CD97C5848A8E3CADB9D3F73154DA59BB3A3CACC4CA43671C", + "ledger_index": 5498, + "date": 1710949602000 +} diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts index 065d02976..0239422ff 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/types.ts @@ -4,4 +4,6 @@ export interface MPTokenIssuanceCreateInstructions { transferFee?: number assetScale?: number maxAmount?: string + // i18n keys of the fields/flags declared mutable via Dynamic MPT (XLS-94). + mutableFlags?: string[] } diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx index 9b0777e17..0798544d4 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx @@ -4,9 +4,23 @@ import { SimpleRow } from '../SimpleRow' import { TransactionSimpleComponent, TransactionSimpleProps } from '../types' import { Account } from '../../Account' import { MPTokenLink } from '../../MPTokenLink' -import { shortenEncryptionKey } from '../../../utils' +import { + isValidJsonString, + localizeNumber, + shortenEncryptionKey, +} from '../../../utils' +import { useLanguage } from '../../../hooks' +import { JsonView } from '../../JsonView' +import { + buildFlags, + convertHexToString, +} from '../../../../../rippled/lib/utils' +import { MPT_SET_MUTABLE_FLAGS } from '../../../transactionUtils' interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet { + MPTokenMetadata?: string + TransferFee?: number + MutableFlags?: number IssuerEncryptionKey?: string AuditorEncryptionKey?: string } @@ -17,10 +31,25 @@ export const Simple: TransactionSimpleComponent = ({ const { MPTokenIssuanceID, Holder, + MPTokenMetadata, + TransferFee, + MutableFlags, IssuerEncryptionKey, AuditorEncryptionKey, } = data.instructions const { t } = useTranslation() + const language = useLanguage() + + const metadata = MPTokenMetadata + ? convertHexToString(MPTokenMetadata) + : undefined + const formattedFee = + TransferFee != null + ? `${localizeNumber((TransferFee / 1000).toPrecision(5), language, { + minimumFractionDigits: 3, + })}%` + : undefined + const flagChanges = buildFlags(MutableFlags, MPT_SET_MUTABLE_FLAGS) return ( <> @@ -32,6 +61,35 @@ export const Simple: TransactionSimpleComponent = ({ )} + {TransferFee != null && ( + + {formattedFee} + + )} + {metadata && ( + + {isValidJsonString(metadata) ? ( + + ) : ( + metadata + )} + + )} + {flagChanges.length > 0 && ( + + {flagChanges.map((flag) => ( +
{flag}
+ ))} +
+ )} {IssuerEncryptionKey && ( { unmount() }) + it('handles Dynamic MPT MPTokenIssuanceSet simple view', () => { + const { container, unmount } = renderComponent(transactionDynamic) + + expectSimpleRowText( + container, + 'mpt-issuance-id', + '000002609BB39CEC721B5AB337B6BD862ACD2811CBBB5F18', + ) + expectSimpleRowText(container, 'mpt-fee', '0.500%') + expectSimpleRowText(container, 'mpt-metadata', 'FOO') + // MutableFlags 17 = tmfMPTSetCanTransfer (0x10) + tmfMPTSetCanLock (0x1) + expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanLock') + expectSimpleRowText(container, 'mpt-mutable-flags', 'tmfMPTSetCanTransfer') + expectSimpleRowNotToExist(container, 'mpt-holder') + unmount() + }) + it('handles failed MPTokenIssuanceSet simple view ', () => { const { container, unmount } = renderComponent(transactionFail) expectSimpleRowText( diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json new file mode 100644 index 000000000..6c732a934 --- /dev/null +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/mock_data/MPTokenIssuanceSet_Dynamic.json @@ -0,0 +1,73 @@ +{ + "tx": { + "Account": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM", + "Fee": "10", + "Flags": 0, + "MPTokenIssuanceID": "000002609BB39CEC721B5AB337B6BD862ACD2811CBBB5F18", + "MutableFlags": 17, + "TransferFee": 500, + "MPTokenMetadata": "464F4F", + "Sequence": 609, + "SigningPubKey": "ED92DDE49AA689EC63589623067968B85E4885A9874B3CAB89E07D192EBFA42FF9", + "TransactionType": "MPTokenIssuanceSet", + "TxnSignature": "6678BF017A62360DAA39156112960D934440467D2E4098958F053B6758A2D237DA82CD27FDA3B35BAB610D74669BF6C4DEB12D77984E48158B2D79C8BAEC3303", + "ctid": "C000026400000000", + "date": 1711133159000, + "hash": "undefined", + "inLedger": "undefined", + "ledger_index": "undefined", + "meta": "undefined", + "validated": "undefined", + "metaData": "undefined", + "status": "undefined" + }, + "meta": { + "AffectedNodes": [ + { + "ModifiedNode": { + "FinalFields": { + "Account": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM", + "Balance": "99999980", + "Flags": 0, + "OwnerCount": 1, + "Sequence": 610 + }, + "LedgerEntryType": "AccountRoot", + "LedgerIndex": "1D54D263727856612FC7C7A27D93532ED0C41B74FA651992C125AD19C900669D", + "PreviousFields": { + "Balance": "99999990", + "Sequence": 609 + }, + "PreviousTxnID": "CF68A8D929F089F6F53B071250935DD7C3F52F6E175D82E53118265D776D4BF7", + "PreviousTxnLgrSeq": 610 + } + }, + { + "ModifiedNode": { + "FinalFields": { + "Flags": 32, + "Issuer": "rEUGuTqrySk9o1rZSVx8seuvcsEZymeEYM", + "MutableFlags": 131106, + "OutstandingAmount": "0", + "OwnerNode": "0", + "Sequence": 608, + "TransferFee": 500 + }, + "LedgerEntryType": "MPTokenIssuance", + "LedgerIndex": "E8DF796110B1FBD9086A4637491E49843D33D897FBC32F03A33140F76378EE86", + "PreviousFields": { + "Flags": 34, + "TransferFee": 0 + }, + "PreviousTxnID": "CF68A8D929F089F6F53B071250935DD7C3F52F6E175D82E53118265D776D4BF7", + "PreviousTxnLgrSeq": 610 + } + } + ], + "TransactionIndex": 0, + "TransactionResult": "tesSUCCESS" + }, + "hash": "73C21D8B5DFBF5DE03FCAF0D69C00E9E2918280561E9898C556A1C743A566D47", + "ledger_index": 612, + "date": 1711133159000 +} diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts index 6b3c4900c..c83a8e173 100644 --- a/src/containers/shared/transactionUtils.ts +++ b/src/containers/shared/transactionUtils.ts @@ -119,6 +119,37 @@ export const TX_FLAGS: Record> = { }, } +// Dynamic MPT (XLS-94) MutableFlags fields. These are separate UInt32 fields +// from the standard `Flags` field, so they are decoded with their own maps. +// Values mirror rippled's tmfMPT* definitions in TxFlags.h. + +// MPTokenIssuanceCreate.MutableFlags: declares which fields/flags may be +// modified after creation. Boolean capabilities use the "CanEnable" form +// (they may only be turned on later); Metadata/TransferFee use "CanMutate". +export const MPT_CREATE_MUTABLE_FLAGS: Record = { + 0x00000002: 'tmfMPTCanEnableCanLock', + 0x00000004: 'tmfMPTCanEnableRequireAuth', + 0x00000008: 'tmfMPTCanEnableCanEscrow', + 0x00000010: 'tmfMPTCanEnableCanTrade', + 0x00000020: 'tmfMPTCanEnableCanTransfer', + 0x00000040: 'tmfMPTCanEnableCanClawback', + 0x00000080: 'tmfMPTCannotEnableCanHoldConfidentialBalance', + 0x00010000: 'tmfMPTCanMutateMetadata', + 0x00020000: 'tmfMPTCanMutateTransferFee', +} + +// MPTokenIssuanceSet.MutableFlags: enables a previously-declared mutable +// capability flag. Single "Set" bit per capability (no clear operation). +export const MPT_SET_MUTABLE_FLAGS: Record = { + 0x00000001: 'tmfMPTSetCanLock', + 0x00000002: 'tmfMPTSetRequireAuth', + 0x00000004: 'tmfMPTSetCanEscrow', + 0x00000008: 'tmfMPTSetCanTrade', + 0x00000010: 'tmfMPTSetCanTransfer', + 0x00000020: 'tmfMPTSetCanClawback', + 0x00000040: 'tmfMPTSetCanHoldConfidentialBalance', +} + export const ACCOUNT_FLAGS: Record = { 17: 'asfAllowTrustLineLocking', 16: 'asfAllowTrustLineClawback', diff --git a/src/rippled/lib/utils.ts b/src/rippled/lib/utils.ts index 823870688..cc7f63abc 100644 --- a/src/rippled/lib/utils.ts +++ b/src/rippled/lib/utils.ts @@ -47,6 +47,20 @@ const MPT_ISSUANCE_FLAGS: FlagMap = { 0x00000040: 'lsfMPTCanClawback', 0x00000080: 'lsfMPTCanConfidentialAmount', } +// MutableFlags on the MPTokenIssuance ledger object (Dynamic MPT, XLS-94). +// Declares which fields/flags the issuer may modify after creation. +// Values mirror rippled's lsmfMPT* definitions in LedgerFormats.h. +const MPT_ISSUANCE_MUTABLE_FLAGS: FlagMap = { + 0x00000002: 'lsmfMPTCanEnableCanLock', + 0x00000004: 'lsmfMPTCanEnableRequireAuth', + 0x00000008: 'lsmfMPTCanEnableCanEscrow', + 0x00000010: 'lsmfMPTCanEnableCanTrade', + 0x00000020: 'lsmfMPTCanEnableCanTransfer', + 0x00000040: 'lsmfMPTCanEnableCanClawback', + 0x00000080: 'lsmfMPTCannotEnableCanHoldConfidentialBalance', + 0x00010000: 'lsmfMPTCanMutateMetadata', + 0x00020000: 'lsmfMPTCanMutateTransferFee', +} const MPTOKEN_FLAGS: FlagMap = { 0x00000001: 'lsfMPTLocked', 0x00000002: 'lsfMPTAuthorized', @@ -267,6 +281,7 @@ interface MPTIssuanceInfo { Sequence: number MPTokenMetadata?: string Flags: number + MutableFlags?: number IssuerEncryptionKey?: string AuditorEncryptionKey?: string } @@ -281,6 +296,7 @@ interface FormattedMPTIssuance { sequence: number metadata?: any flags: string[] + mutableFlags: string[] rawMPTMetadata?: string parsedMPTMetadata?: Record isMPTMetadataCompliant: boolean @@ -312,6 +328,7 @@ const formatMPTIssuance = (info: MPTIssuanceInfo): FormattedMPTIssuance => { parsedMPTMetadata: parseMPTMetadata(rawMPTMetadataHex), isMPTMetadataCompliant: isMPTMetadataCompliant(rawMPTMetadataHex), flags: buildFlags(info.Flags, MPT_ISSUANCE_FLAGS), + mutableFlags: buildFlags(info.MutableFlags, MPT_ISSUANCE_MUTABLE_FLAGS), issuerEncryptionKey: info.IssuerEncryptionKey, auditorEncryptionKey: info.AuditorEncryptionKey, }