From 508352cc529216056d320fce22b8ee94a6605a75 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Wed, 1 Jul 2026 11:48:16 -0400 Subject: [PATCH 1/9] 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/9] 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/9] 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, } From 1870ae0de682b1dfd36b853587d6ce12832d4521 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Thu, 27 Aug 2026 06:52:27 -1000 Subject: [PATCH 4/9] more updates --- public/locales/ca-CA/translations.json | 1 + public/locales/en-US/translations.json | 1 + public/locales/es-ES/translations.json | 1 + public/locales/fr-FR/translations.json | 1 + public/locales/ja-JP/translations.json | 1 + public/locales/ko-KR/translations.json | 1 + public/locales/my-MM/translations.json | 1 + src/containers/Token/MPT/Header/Settings.tsx | 1 + .../Token/MPT/test/Header/Settings.test.tsx | 37 ++++++++++++------- .../MPTokenIssuanceCreate/Simple.tsx | 2 +- .../Transaction/MPTokenIssuanceSet/Simple.tsx | 2 +- .../test/MPTokenIssuanceSetSimple.test.jsx | 2 +- .../mock_data/MPTokenIssuanceSet_Dynamic.json | 2 +- 13 files changed, 36 insertions(+), 17 deletions(-) diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 3467d89c4..d8a3a99d3 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -946,6 +946,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 3dda681f4..ac97561fb 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": "Starts in", "mutable": "Mutable", "mutable_flags": "Mutable Flags", + "immutable_flags": "Immutable 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 8b7384fa2..98afaadd7 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 cb36a2f7d..275b1d8de 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 db73a2754..5097ab866 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 d230bea89..00f2deb4e 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 803924dba..0dcde6eff 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -947,6 +947,7 @@ "maintenance_banner.countdown_prefix": null, "mutable": null, "mutable_flags": null, + "immutable_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 582ae96f2..b8110d591 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -70,6 +70,7 @@ export const Settings = ({ key: 'canConfidentialAmount', label: t('can_confidential_amount'), enabled: flags.includes('lsfMPTCanConfidentialAmount'), + immutableFlag: 'lsifMPTCanHoldConfidentialBalance', }, ] diff --git a/src/containers/Token/MPT/test/Header/Settings.test.tsx b/src/containers/Token/MPT/test/Header/Settings.test.tsx index 5676ddfc4..8fd97282a 100644 --- a/src/containers/Token/MPT/test/Header/Settings.test.tsx +++ b/src/containers/Token/MPT/test/Header/Settings.test.tsx @@ -23,7 +23,7 @@ describe('Settings component', () => { 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. + // (both still mutable), giving 8 capability rows + 2 field rows = 10. const { container } = renderComponent() expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) }) @@ -106,10 +106,10 @@ describe('Settings component', () => { 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. + // rows (metadata, transferFee) are visible. 7 disabled cap badges + 2 field + // row badges = 9 total. (locked flag has no immutableFlag so never badges.) const { queryAllByTestId, container } = renderComponent({ flags: [] }) - expect(queryAllByTestId('mutable-badge')).toHaveLength(8) + expect(queryAllByTestId('mutable-badge')).toHaveLength(9) // 8 capability rows + metadata + transferFee field rows expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) }) @@ -119,10 +119,21 @@ describe('Settings component', () => { flags: [], immutableFlags: ['lsifMPTCanLock'], }) - // lsifMPTCanLock is locked → its badge is hidden; 5 other disabled caps + - // 2 field rows still show badges = 7 total - expect(getAllByTestId('mutable-badge')).toHaveLength(7) - expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(7) + // lsifMPTCanLock is locked → its badge is hidden; 6 other disabled caps + + // 2 field rows still show badges = 8 total + expect(getAllByTestId('mutable-badge')).toHaveLength(8) + expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(8) + }) + + it('hides the mutable badge for confidential capability once locked in immutableFlags', () => { + const { container, getAllByTestId } = renderComponent({ + flags: [], + immutableFlags: ['lsifMPTCanHoldConfidentialBalance'], + }) + // lsifMPTCanHoldConfidentialBalance locked → its badge hidden; + // 6 other disabled caps + 2 field rows still show badges = 8 total + expect(getAllByTestId('mutable-badge')).toHaveLength(8) + expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(8) }) it('hides the mutable badge once a capability is enabled', () => { @@ -132,9 +143,9 @@ describe('Settings component', () => { flags: ['lsfMPTCanLock'], immutableFlags: [], }) - // canLock is enabled → no badge for it; 5 other disabled caps + 2 field - // rows still show badges = 7 total - expect(queryAllByTestId('mutable-badge')).toHaveLength(7) + // canLock is enabled → no badge for it; 6 other disabled caps + 2 field + // rows still show badges = 8 total + expect(queryAllByTestId('mutable-badge')).toHaveLength(8) }) it('hides extra field rows for metadata and transferFee when locked in immutableFlags', () => { @@ -153,7 +164,7 @@ describe('Settings component', () => { }) // 8 capability rows + metadata + transfer fee (both still mutable) expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) - // badges: 6 disabled cap flags (not locked) + 2 field row badges = 8 - expect(getAllByTestId('mutable-badge')).toHaveLength(8) + // badges: 7 disabled cap flags (not locked) + 2 field row badges = 9 + expect(getAllByTestId('mutable-badge')).toHaveLength(9) }) }) diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx index 6caec6c41..ec9062c01 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/Simple.tsx @@ -72,7 +72,7 @@ export const Simple: TransactionSimpleComponent = ({ )} {immutableFlags && immutableFlags.length > 0 && ( diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx index 9bd4e94bf..5bcda5df6 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx @@ -81,7 +81,7 @@ export const Simple: TransactionSimpleComponent = ({ )} {flagChanges.length > 0 && ( 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 daca7bd26..056001a2c 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/test/MPTokenIssuanceSetSimple.test.jsx @@ -47,7 +47,7 @@ describe('MPTokenIssuanceSet', () => { ) expectSimpleRowText(container, 'mpt-fee', '0.500%') expectSimpleRowText(container, 'mpt-metadata', 'FOO') - // ImmutableFlags 17 (0x11) = 0x10 (tifMPTCanTrade) + 0x01 (not in MPT_IMMUTABLE_FLAGS map) + // ImmutableFlags 16 (0x10) = tifMPTCanTrade 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 f6ec14387..b84dca60e 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", - "ImmutableFlags": 17, + "ImmutableFlags": 16, "TransferFee": 500, "MPTokenMetadata": "464F4F", "Sequence": 609, From 8309412a5cab261d674db79a8443db0b090922be Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Fri, 28 Aug 2026 09:57:53 -1000 Subject: [PATCH 5/9] update setting page --- public/locales/ca-CA/translations.json | 9 +- public/locales/en-US/translations.json | 11 +- public/locales/es-ES/translations.json | 9 +- public/locales/fr-FR/translations.json | 9 +- public/locales/ja-JP/translations.json | 9 +- public/locales/ko-KR/translations.json | 9 +- public/locales/my-MM/translations.json | 9 +- scripts/showcase-dynamic-mpt.mjs | 253 ++++++++++++++++++ src/containers/Token/MPT/Header/Settings.tsx | 77 ++++-- src/containers/Token/MPT/Header/styles.scss | 11 +- .../Token/MPT/test/Header/Settings.test.tsx | 134 ++++++---- src/containers/shared/transactionUtils.ts | 1 + 12 files changed, 439 insertions(+), 102 deletions(-) create mode 100644 scripts/showcase-dynamic-mpt.mjs diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 85e4d2cba..81dc72bba 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -946,9 +946,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index b205d15df..b0f84b37a 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -947,9 +947,10 @@ "included": "Included", "maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).", "maintenance_banner.countdown_prefix": "Starts in", - "mutable": "Mutable", - "mutable_flags": "Mutable Flags", - "immutable_flags": "Immutable Flags", - "mutable_flag_tooltip": "The issuer can enable this flag later", - "mutable_field_tooltip": "The issuer can change this value" + "can_enable": "Can Enable", + "can_enable_flag_tooltip": "The issuer can still enable this capability — it has not been permanently locked", + "immutable": "Immutable", + "immutable_flag_tooltip": "This capability has been permanently locked by the issuer and cannot be enabled", + "immutable_field_tooltip": "This field has been permanently locked by the issuer and cannot be changed", + "immutable_flags": "Immutable Flags" } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 631eeaf5f..e30c9f535 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -947,9 +947,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index 776feac97..2a5978c1d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -947,9 +947,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index e4900cfc2..b392093d2 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -947,9 +947,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index 0f1e98155..5b974d392 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -947,9 +947,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/public/locales/my-MM/translations.json b/public/locales/my-MM/translations.json index 133919f6a..567be79be 100644 --- a/public/locales/my-MM/translations.json +++ b/public/locales/my-MM/translations.json @@ -947,9 +947,10 @@ "included": null, "maintenance_banner.notice": null, "maintenance_banner.countdown_prefix": null, - "mutable": null, - "mutable_flags": null, "immutable_flags": null, - "mutable_flag_tooltip": null, - "mutable_field_tooltip": null + "can_enable": null, + "can_enable_flag_tooltip": null, + "immutable": null, + "immutable_flag_tooltip": null, + "immutable_field_tooltip": null } diff --git a/scripts/showcase-dynamic-mpt.mjs b/scripts/showcase-dynamic-mpt.mjs new file mode 100644 index 000000000..f29892641 --- /dev/null +++ b/scripts/showcase-dynamic-mpt.mjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node +/** + * showcase-dynamic-mpt.mjs + * + * Submits four DynamicMPT transactions to a local rippled standalone and prints + * Explorer URLs to review each UI surface: + * 1. MPTokenIssuanceCreate — all mutable (no ImmutableFlags) + * 2. MPTokenIssuanceCreate — with ImmutableFlags set + * 3. MPTokenIssuanceSet — enable capabilities via tfMPTSetCan* Flags + * 4. MPTokenIssuanceSet — update TransferFee + lock flags via ImmutableFlags + * + * Usage: + * node scripts/showcase-dynamic-mpt.mjs + * EXPLORER=http://localhost:3001/localhost node scripts/showcase-dynamic-mpt.mjs + */ + +const ENDPOINT = process.env.RIPPLED ?? 'http://localhost:5005' +const GENESIS_ACCOUNT = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh' +const GENESIS_SECRET = 'snoPBrXtMeMyMHUVTgbuqAfg1SUTb' +const EXPLORER = process.env.EXPLORER ?? 'http://localhost:3001/localhost' + +// ── RPC helpers ─────────────────────────────────────────────────────────────── + +async function rpc(method, params = {}) { + const res = await fetch(ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method, params: [params] }), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const { result } = await res.json() + if (result.error) throw new Error(`${result.error}: ${result.error_message ?? ''}`) + return result +} + +async function submit(txJson) { + const result = await rpc('submit', { tx_json: txJson, secret: GENESIS_SECRET }) + const eng = result.engine_result + if (eng !== 'tesSUCCESS' && eng !== 'terQUEUED') { + throw new Error(`${eng}: ${result.engine_result_message}`) + } + return result.tx_json.hash +} + +async function advanceLedger() { + await rpc('ledger_accept') +} + +async function getValidatedTx(hash) { + // retry a few times in case the ledger hasn't closed yet + for (let i = 0; i < 5; i++) { + try { + const result = await rpc('tx', { transaction: hash }) + if (result.validated) return result + } catch (_) {} + await new Promise(r => setTimeout(r, 200)) + } + throw new Error(`tx ${hash} not validated after retries`) +} + +// ── XLS-0089 MPT metadata schemas ──────────────────────────────────────────── +// Short key names per spec: t=ticker, n=name, d=desc, i=icon, ac=asset_class, +// as=asset_subclass (required for ac=rwa), in=issuer_name, us=uris, ai=additional_info + +// Issuance A: DeFi token, all capabilities still mutable at creation. +const METADATA_A = { + t: 'DYNX', + n: 'DynToken Flex', + d: 'A DeFi token demonstrating DynamicMPT — all capabilities mutable at creation. The issuer can still enable escrow, trade, clawback, and more via MPTokenIssuanceSet.', + i: 'https://example.org/dynx/icon.png', + ac: 'defi', + in: 'XRPL Foundation', +} + +// Issuance B: RWA (treasury) token with CanLock, RequireAuth, and Metadata +// permanently locked at issuance via ImmutableFlags. +const METADATA_B = { + t: 'RTBIL', + n: 'T-Bill Yield Token', + d: 'Yield-bearing token backed by U.S. Treasuries. CanLock, RequireAuth, and Metadata are permanently immutable — set once at issuance and can never be changed.', + i: 'https://example.org/rtbil/icon.png', + ac: 'rwa', + as: 'treasury', + in: 'XRPL Foundation', + us: [ + { u: 'https://example.org/rtbil', c: 'website', t: 'Product Page' }, + ], + ai: { interest_rate: '5.00%', maturity_date: '2027-06-30' }, +} + +// TX4 update to Issuance A: new metadata after locking CanTrade, CanTransfer, +// and TransferFee — reflects the permanent change in the description. +const METADATA_A_UPDATED = { + t: 'DYNX', + n: 'DynToken Flex', + d: 'Updated via MPTokenIssuanceSet: TransferFee raised to 0.075%. CanTrade, CanTransfer, and TransferFee are now permanently immutable.', + i: 'https://example.org/dynx/icon.png', + ac: 'defi', + in: 'XRPL Foundation', + us: [ + { u: 'https://example.org/dynx', c: 'website', t: 'Product Page' }, + ], +} + +function toHex(obj) { + const json = typeof obj === 'string' ? obj : JSON.stringify(obj) + return Buffer.from(json, 'utf8').toString('hex').toUpperCase() +} + +// ── Main ────────────────────────────────────────────────────────────────────── + +async function main() { + console.log(`Connecting to ${ENDPOINT} …`) + const { info } = await rpc('server_info') + console.log(` rippled ${info.build_version} network_id=${info.network_id}\n`) + + const { account_data } = await rpc('account_info', { + account: GENESIS_ACCOUNT, + ledger_index: 'current', + }) + let seq = account_data.Sequence + console.log(` Genesis account: ${GENESIS_ACCOUNT} seq=${seq}\n`) + + const results = {} + + // ── TX 1: Create — all mutable (no ImmutableFlags) ────────────────────────── + // Flags: tfMPTCanTransfer (0x20) + // Metadata: XLS-0089 JSON (ac=defi), all capabilities mutable. + // Shows: Create Simple with NO "Immutable Flags" row. + // Settings: 7 "Can Enable" badges on unlocked disabled caps; + // Metadata + TransferFee field rows visible with no badge. + console.log('TX1 MPTokenIssuanceCreate — all mutable (DYNX / DeFi)') + const hash1 = await submit({ + TransactionType: 'MPTokenIssuanceCreate', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + Flags: 0x00000020, // tfMPTCanTransfer + AssetScale: 6, + MaximumAmount: '1000000000000', + TransferFee: 100, // 0.010% — still mutable + MPTokenMetadata: toHex(METADATA_A), + }) + await advanceLedger() + const tx1 = await getValidatedTx(hash1) + const issuanceIdA = tx1.meta?.mpt_issuance_id ?? '?' + results.tx1 = { hash: hash1, issuanceId: issuanceIdA } + console.log(` hash : ${hash1}`) + console.log(` issuance ID: ${issuanceIdA}\n`) + + // ── TX 2: Create — with ImmutableFlags ────────────────────────────────────── + // Flags: tfMPTCanTransfer (0x20) | tfMPTRequireAuth (0x04) + // ImmutableFlags: tifMPTCanLock(0x2) + tifMPTRequireAuth(0x4) + tifMPTMetadata(0x10000) + // = 0x10006 = 65542 + // Metadata: XLS-0089 JSON (ac=rwa, as=treasury), with URIs and ai fields. + // Shows: Create Simple "Immutable Flags" row listing tifMPTCanLock, + // tifMPTRequireAuth, tifMPTMetadata; metadata renders as JSON in Detail. + // Settings: CanLock + RequireAuth → "Immutable" badge; + // Metadata field row → "Immutable" badge; TransferFee row mutable. + console.log('TX2 MPTokenIssuanceCreate — with ImmutableFlags (RTBIL / RWA treasury)') + const hash2 = await submit({ + TransactionType: 'MPTokenIssuanceCreate', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + Flags: 0x00000024, // tfMPTCanTransfer | tfMPTRequireAuth + AssetScale: 2, + MaximumAmount: '9223372036854775807', + TransferFee: 500, // 0.500% + MPTokenMetadata: toHex(METADATA_B), + ImmutableFlags: 0x00010006, // tifMPTCanLock + tifMPTRequireAuth + tifMPTMetadata + }) + await advanceLedger() + const tx2 = await getValidatedTx(hash2) + const issuanceIdB = tx2.meta?.mpt_issuance_id ?? '?' + results.tx2 = { hash: hash2, issuanceId: issuanceIdB } + console.log(` hash : ${hash2}`) + console.log(` issuance ID: ${issuanceIdB}\n`) + + // ── TX 3: Set — enable capabilities via tfMPTSetCan* Flags ────────────────── + // Flags: tfMPTSetCanEscrow(0x10) | tfMPTSetCanTrade(0x20) = 0x30 + // Shows: Set Simple with NO ImmutableFlags row; the tfMPTSetCan* bits appear + // automatically in the Detail tab via TX_FLAGS.MPTokenIssuanceSet. + console.log('TX3 MPTokenIssuanceSet — enable CanEscrow + CanTrade on Issuance A') + const hash3 = await submit({ + TransactionType: 'MPTokenIssuanceSet', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + MPTokenIssuanceID: issuanceIdA, + Flags: 0x00000030, // tfMPTSetCanEscrow | tfMPTSetCanTrade + }) + await advanceLedger() + results.tx3 = { hash: hash3 } + console.log(` hash: ${hash3}\n`) + + // ── TX 4: Set — update TransferFee + metadata + lock via ImmutableFlags ────── + // ImmutableFlags: tifMPTCanTrade(0x10) + tifMPTCanTransfer(0x20) + tifMPTTransferFee(0x20000) + // = 0x20030 = 131120 + // Metadata: updated XLS-0089 JSON reflecting the new locked state. + // Shows: Set Simple with TransferFee row (0.075%), Metadata row (JSON rendered), + // and "Immutable Flags" row listing tifMPTCanTrade, tifMPTCanTransfer, + // tifMPTTransferFee. + console.log('TX4 MPTokenIssuanceSet — update metadata + TransferFee + lock via ImmutableFlags on Issuance A') + const hash4 = await submit({ + TransactionType: 'MPTokenIssuanceSet', + Account: GENESIS_ACCOUNT, + Sequence: seq++, + Fee: '200', + MPTokenIssuanceID: issuanceIdA, + TransferFee: 750, // update: 0.010% → 0.075% + MPTokenMetadata: toHex(METADATA_A_UPDATED), + ImmutableFlags: 0x00020030, // tifMPTCanTrade + tifMPTCanTransfer + tifMPTTransferFee + }) + await advanceLedger() + results.tx4 = { hash: hash4 } + console.log(` hash: ${hash4}\n`) + + // ── Summary ────────────────────────────────────────────────────────────────── + console.log('═══════════════════════════════════════════════════════════════════') + console.log(' EXPLORER URLS') + console.log('═══════════════════════════════════════════════════════════════════\n') + + console.log('── MPTokenIssuanceCreate / Simple view ─────────────────────────────') + console.log(' TX1 DYNX — no ImmutableFlags ("Immutable Flags" row absent)') + console.log(` ${EXPLORER}/transactions/${results.tx1.hash}/simple`) + console.log() + console.log(' TX2 RTBIL — ImmutableFlags=0x10006 (tifMPTCanLock, tifMPTRequireAuth, tifMPTMetadata)') + console.log(` ${EXPLORER}/transactions/${results.tx2.hash}/simple`) + console.log() + + console.log('── MPTokenIssuanceSet / Simple view ────────────────────────────────') + console.log(' TX3 tfMPTSetCanEscrow + tfMPTSetCanTrade (see Detail tab for flag decode)') + console.log(` ${EXPLORER}/transactions/${results.tx3.hash}/simple`) + console.log() + console.log(' TX4 TransferFee + Metadata (JSON) + ImmutableFlags rows') + console.log(` ${EXPLORER}/transactions/${results.tx4.hash}/simple`) + console.log() + + console.log('── MPT token Settings page ─────────────────────────────────────────') + console.log(' Issuance A DYNX — "Can Enable" on unlocked caps; CanTrade+CanTransfer+TransferFee → "Immutable"') + console.log(` ${EXPLORER}/token/mpt/${results.tx1.issuanceId}`) + console.log() + console.log(' Issuance B RTBIL — CanLock+RequireAuth+Metadata → "Immutable"; TransferFee still mutable') + console.log(` ${EXPLORER}/token/mpt/${results.tx2.issuanceId}`) + console.log() + console.log('═══════════════════════════════════════════════════════════════════') +} + +main().catch(err => { + console.error('\nFatal:', err.message) + process.exit(1) +}) diff --git a/src/containers/Token/MPT/Header/Settings.tsx b/src/containers/Token/MPT/Header/Settings.tsx index b8110d591..fa9fcca91 100644 --- a/src/containers/Token/MPT/Header/Settings.tsx +++ b/src/containers/Token/MPT/Header/Settings.tsx @@ -9,20 +9,31 @@ interface FlagItem { key: string label: string enabled: boolean - // The lsifMPT* flag name (Dynamic MPT, XLS-94) that permanently locks this - // capability; present only for settings that can be declared immutable. + // The lsifMPT* flag name that permanently locks this capability (XLS-94). immutableFlag?: string } +interface FieldItem { + key: string + label: string + // The lsifMPT* flag name that permanently locks this field (XLS-94). + immutableFlag: string +} + export const Settings = ({ flags = [], immutableFlags = [], }: Props): JSX.Element => { const { t } = useTranslation() - // Returns true when the flag is NOT locked in immutableFlags (i.e. still mutable). - const isStillMutable = (immutableFlag?: string): boolean => - !!immutableFlag && !immutableFlags.includes(immutableFlag) + // Whether the lsif bit is present — the capability/field is permanently frozen. + const isLocked = (immutableFlag?: string): boolean => + !!immutableFlag && immutableFlags.includes(immutableFlag) + + // Whether the capability can still be enabled: not yet locked and not yet on. + // Absent immutableFlag means it is not a lockable capability (e.g. lsfMPTLocked). + const isCanEnable = (immutableFlag?: string, enabled = false): boolean => + !!immutableFlag && !immutableFlags.includes(immutableFlag) && !enabled const flagItems: FlagItem[] = [ { @@ -74,9 +85,9 @@ export const Settings = ({ }, ] - // 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 = [ + // Mutable fields (not capability flags): always shown. An Immutable badge + // appears once the issuer has permanently locked the field via ImmutableFlags. + const fieldItems: FieldItem[] = [ { key: 'metadata', label: t('metadata'), @@ -87,7 +98,7 @@ export const Settings = ({ label: t('transfer_fee'), immutableFlag: 'lsifMPTTransferFee', }, - ].filter((item) => isStillMutable(item.immutableFlag)) + ] return (
@@ -97,16 +108,27 @@ export const Settings = ({
{flag.label}
- {/* Capabilities are one-directional (can only be enabled later), - so only surface the "Mutable" pill while the flag is still - not locked (not in immutableFlags) and not yet enabled. */} - {isStillMutable(flag.immutableFlag) && !flag.enabled && ( + {/* Capabilities are one-way (enable-only). Show "Can Enable" while + the flag is not yet locked and not yet on, so holders know the + issuer can still activate this capability. */} + {isCanEnable(flag.immutableFlag, flag.enabled) && (
- {t('mutable')} + {t('can_enable')} +
+ )} + {/* Show "Immutable" when the issuer has permanently locked this + capability via ImmutableFlags — it can never be enabled. */} + {isLocked(flag.immutableFlag) && !flag.enabled && ( +
+ {t('immutable')}
)}
))} - {mutableFieldItems.map((field) => ( + {fieldItems.map((field) => (
{field.label}
-
- {t('mutable')} -
+ {/* Show "Immutable" once the issuer has permanently locked this + field. No badge when still changeable — the row being visible + is enough to indicate the field exists and can be updated. */} + {isLocked(field.immutableFlag) && ( +
+ {t('immutable')} +
+ )}
))} diff --git a/src/containers/Token/MPT/Header/styles.scss b/src/containers/Token/MPT/Header/styles.scss index a6a378d9a..fb9fbedd2 100644 --- a/src/containers/Token/MPT/Header/styles.scss +++ b/src/containers/Token/MPT/Header/styles.scss @@ -163,12 +163,21 @@ color: $black-0; } - &.mutable { + // Capability not yet locked — issuer can still enable it. + &.can-enable { min-width: 0; border: 1px solid $green-50; background: transparent; color: $green-50; } + + // Capability/field permanently locked by the issuer via ImmutableFlags. + &.immutable { + min-width: 0; + border: 1px solid $orange-50; + background: transparent; + color: $orange-50; + } } } diff --git a/src/containers/Token/MPT/test/Header/Settings.test.tsx b/src/containers/Token/MPT/test/Header/Settings.test.tsx index 8fd97282a..c82d7a59f 100644 --- a/src/containers/Token/MPT/test/Header/Settings.test.tsx +++ b/src/containers/Token/MPT/test/Header/Settings.test.tsx @@ -21,13 +21,15 @@ describe('Settings component', () => { ) }) - it('renders 8 capability flag items plus 2 field rows by default', () => { - // With no immutableFlags, metadata and transferFee field rows are shown - // (both still mutable), giving 8 capability rows + 2 field rows = 10. + it('renders 8 capability rows plus 2 field rows always', () => { + // Field rows (Metadata, TransferFee) are always visible regardless of + // immutableFlags — they show an Immutable badge when locked. const { container } = renderComponent() expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) }) + // ── Enabled / disabled status ─────────────────────────────────────────────── + it('shows locked flag as disabled by default', () => { const { container } = renderComponent({ flags: [] }) expect(container).toHaveTextContent('locked') @@ -41,43 +43,43 @@ describe('Settings component', () => { expect(lockedItem.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_lock flag status', () => { + it('shows can_lock flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanLock'] }) expect(container).toHaveTextContent('can_lock') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows require_auth flag status', () => { + it('shows require_auth flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTRequireAuth'] }) expect(container).toHaveTextContent('require_auth') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_escrow flag status', () => { + it('shows can_escrow flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanEscrow'] }) expect(container).toHaveTextContent('can_escrow') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_trade flag status', () => { + it('shows can_trade flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanTrade'] }) expect(container).toHaveTextContent('can_trade') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_transfer flag status', () => { + it('shows can_transfer flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanTransfer'] }) expect(container).toHaveTextContent('can_transfer') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_clawback flag status', () => { + it('shows can_clawback flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanClawback'] }) expect(container).toHaveTextContent('can_clawback') expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(1) }) - it('shows can_confidential_amount flag status', () => { + it('shows can_confidential_amount flag as enabled when present', () => { const { container } = renderComponent({ flags: ['lsfMPTCanConfidentialAmount'], }) @@ -93,78 +95,116 @@ describe('Settings component', () => { expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(5) }) - it('handles empty flags array', () => { + it('handles empty flags array — all caps disabled', () => { const { container } = renderComponent({ flags: [] }) expect(container.querySelectorAll('.flag-status.enabled')).toHaveLength(0) expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8) }) - it('handles undefined flags', () => { + it('handles undefined flags — all caps disabled', () => { const { container } = renderComponent({ flags: undefined }) expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8) }) - it('shows mutable badges for all unlocked disabled caps and field rows when no immutableFlags set', () => { - // With no immutableFlags, all capabilities are still mutable and both field - // rows (metadata, transferFee) are visible. 7 disabled cap badges + 2 field - // row badges = 9 total. (locked flag has no immutableFlag so never badges.) + // ── Can Enable badge ──────────────────────────────────────────────────────── + // Appears on disabled capabilities that have not been locked via ImmutableFlags. + // Tells holders: "the issuer can still activate this capability." + + it('shows can-enable badges for all unlocked disabled caps when no immutableFlags set', () => { + // 7 lockable caps (canLock through canConfidentialAmount) × disabled + unlocked + // = 7 can-enable badges. lsfMPTLocked has no immutableFlag → no badge. + // Field rows show no badge when not locked. const { queryAllByTestId, container } = renderComponent({ flags: [] }) - expect(queryAllByTestId('mutable-badge')).toHaveLength(9) - // 8 capability rows + metadata + transferFee field rows + expect(queryAllByTestId('can-enable-badge')).toHaveLength(7) + expect(queryAllByTestId('immutable-badge')).toHaveLength(0) expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) }) - it('hides the mutable badge for a capability once it is locked (in immutableFlags)', () => { - const { container, getAllByTestId } = renderComponent({ + it('hides can-enable badge once a capability is enabled', () => { + // Capabilities are one-way (enable-only), so an enabled flag never needs + // the "Can Enable" indicator. + const { queryAllByTestId } = renderComponent({ + flags: ['lsfMPTCanLock'], + immutableFlags: [], + }) + // canLock is enabled → no can-enable badge for it; 6 other disabled unlocked + expect(queryAllByTestId('can-enable-badge')).toHaveLength(6) + expect(queryAllByTestId('immutable-badge')).toHaveLength(0) + }) + + it('hides can-enable badge once a capability is locked in immutableFlags', () => { + // Locked = permanently disabled; "Can Enable" would be misleading. + const { queryAllByTestId } = renderComponent({ flags: [], immutableFlags: ['lsifMPTCanLock'], }) - // lsifMPTCanLock is locked → its badge is hidden; 6 other disabled caps + - // 2 field rows still show badges = 8 total - expect(getAllByTestId('mutable-badge')).toHaveLength(8) - expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(8) + // canLock is locked → no can-enable badge; 6 other disabled unlocked caps + expect(queryAllByTestId('can-enable-badge')).toHaveLength(6) }) - it('hides the mutable badge for confidential capability once locked in immutableFlags', () => { - const { container, getAllByTestId } = renderComponent({ + // ── Immutable badge ───────────────────────────────────────────────────────── + // Appears on capabilities/fields permanently locked via ImmutableFlags. + // Tells everyone: "the issuer cannot change this — ever." + + it('shows immutable badge for a locked capability', () => { + const { container, getAllByTestId, queryAllByTestId } = renderComponent({ + flags: [], + immutableFlags: ['lsifMPTCanLock'], + }) + // canLock locked → 1 immutable badge; 6 other disabled unlocked → 6 can-enable + expect(getAllByTestId('immutable-badge')).toHaveLength(1) + expect(queryAllByTestId('can-enable-badge')).toHaveLength(6) + expect(container.querySelectorAll('.flag-status.immutable')).toHaveLength(1) + }) + + it('shows immutable badge for locked confidential capability', () => { + const { getAllByTestId, queryAllByTestId } = renderComponent({ flags: [], immutableFlags: ['lsifMPTCanHoldConfidentialBalance'], }) - // lsifMPTCanHoldConfidentialBalance locked → its badge hidden; - // 6 other disabled caps + 2 field rows still show badges = 8 total - expect(getAllByTestId('mutable-badge')).toHaveLength(8) - expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(8) + expect(getAllByTestId('immutable-badge')).toHaveLength(1) + expect(queryAllByTestId('can-enable-badge')).toHaveLength(6) }) - 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. + it('does not show immutable badge for an already-enabled capability', () => { + // Once enabled, the "Immutable" badge would be redundant — the enabled status + // is already permanent (capabilities are one-way). const { queryAllByTestId } = renderComponent({ flags: ['lsfMPTCanLock'], - immutableFlags: [], + immutableFlags: ['lsifMPTCanLock'], }) - // canLock is enabled → no badge for it; 6 other disabled caps + 2 field - // rows still show badges = 8 total - expect(queryAllByTestId('mutable-badge')).toHaveLength(8) + expect(queryAllByTestId('immutable-badge')).toHaveLength(0) }) - it('hides extra field rows for metadata and transferFee when locked in immutableFlags', () => { - const { container } = renderComponent({ + // ── Field rows (Metadata / TransferFee) ───────────────────────────────────── + // Always shown. An Immutable badge appears when locked; no badge when mutable. + + it('always shows metadata and transferFee rows regardless of immutableFlags', () => { + const { container: withLock } = renderComponent({ + immutableFlags: ['lsifMPTMetadata', 'lsifMPTTransferFee'], + }) + const { container: withoutLock } = renderComponent({ immutableFlags: [] }) + // Both states → 8 cap rows + 2 field rows = 10 + expect(withLock.querySelectorAll('.header-box-item')).toHaveLength(10) + expect(withoutLock.querySelectorAll('.header-box-item')).toHaveLength(10) + }) + + it('shows immutable badges on field rows when locked', () => { + const { getAllByTestId, queryAllByTestId } = renderComponent({ flags: [], immutableFlags: ['lsifMPTMetadata', 'lsifMPTTransferFee'], }) - // 8 capability rows only (field rows are hidden when locked) - expect(container.querySelectorAll('.header-box-item')).toHaveLength(8) + // 2 immutable badges on field rows; 7 can-enable on unlocked disabled caps + expect(getAllByTestId('immutable-badge')).toHaveLength(2) + expect(queryAllByTestId('can-enable-badge')).toHaveLength(7) }) - it('shows extra rows for mutable metadata and transfer fee when not locked', () => { - const { container, getAllByTestId } = renderComponent({ + it('shows no badges on field rows when not locked', () => { + const { queryAllByTestId } = renderComponent({ flags: [], immutableFlags: [], }) - // 8 capability rows + metadata + transfer fee (both still mutable) - expect(container.querySelectorAll('.header-box-item')).toHaveLength(10) - // badges: 7 disabled cap flags (not locked) + 2 field row badges = 9 - expect(getAllByTestId('mutable-badge')).toHaveLength(9) + expect(queryAllByTestId('immutable-badge')).toHaveLength(0) + expect(queryAllByTestId('can-enable-badge')).toHaveLength(7) }) }) diff --git a/src/containers/shared/transactionUtils.ts b/src/containers/shared/transactionUtils.ts index c86064a6b..5d0288bda 100644 --- a/src/containers/shared/transactionUtils.ts +++ b/src/containers/shared/transactionUtils.ts @@ -76,6 +76,7 @@ export const TX_FLAGS: Record> = { 0x00000002: 'tfMPTUnlock', // Capability-setting flags (XLS-94 DynamicMPT): one-way, moved into Flags 0x00000004: 'tfMPTSetCanLock', + 0x00000008: 'tfMPTSetRequireAuth', 0x00000010: 'tfMPTSetCanEscrow', 0x00000020: 'tfMPTSetCanTrade', 0x00000040: 'tfMPTSetCanTransfer', From 4c4759710dd019208de00ac1de5cf25ca3b89a34 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Fri, 28 Aug 2026 10:11:17 -1000 Subject: [PATCH 6/9] fix linter --- scripts/showcase-dynamic-mpt.mjs | 92 +++++++++++++++++++++----------- src/rippled/lib/utils.ts | 5 +- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/scripts/showcase-dynamic-mpt.mjs b/scripts/showcase-dynamic-mpt.mjs index f29892641..1550bb645 100644 --- a/scripts/showcase-dynamic-mpt.mjs +++ b/scripts/showcase-dynamic-mpt.mjs @@ -29,12 +29,16 @@ async function rpc(method, params = {}) { }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const { result } = await res.json() - if (result.error) throw new Error(`${result.error}: ${result.error_message ?? ''}`) + if (result.error) + throw new Error(`${result.error}: ${result.error_message ?? ''}`) return result } async function submit(txJson) { - const result = await rpc('submit', { tx_json: txJson, secret: GENESIS_SECRET }) + const result = await rpc('submit', { + tx_json: txJson, + secret: GENESIS_SECRET, + }) const eng = result.engine_result if (eng !== 'tesSUCCESS' && eng !== 'terQUEUED') { throw new Error(`${eng}: ${result.engine_result_message}`) @@ -53,7 +57,7 @@ async function getValidatedTx(hash) { const result = await rpc('tx', { transaction: hash }) if (result.validated) return result } catch (_) {} - await new Promise(r => setTimeout(r, 200)) + await new Promise((r) => setTimeout(r, 200)) } throw new Error(`tx ${hash} not validated after retries`) } @@ -82,9 +86,7 @@ const METADATA_B = { ac: 'rwa', as: 'treasury', in: 'XRPL Foundation', - us: [ - { u: 'https://example.org/rtbil', c: 'website', t: 'Product Page' }, - ], + us: [{ u: 'https://example.org/rtbil', c: 'website', t: 'Product Page' }], ai: { interest_rate: '5.00%', maturity_date: '2027-06-30' }, } @@ -97,9 +99,7 @@ const METADATA_A_UPDATED = { i: 'https://example.org/dynx/icon.png', ac: 'defi', in: 'XRPL Foundation', - us: [ - { u: 'https://example.org/dynx', c: 'website', t: 'Product Page' }, - ], + us: [{ u: 'https://example.org/dynx', c: 'website', t: 'Product Page' }], } function toHex(obj) { @@ -112,7 +112,9 @@ function toHex(obj) { async function main() { console.log(`Connecting to ${ENDPOINT} …`) const { info } = await rpc('server_info') - console.log(` rippled ${info.build_version} network_id=${info.network_id}\n`) + console.log( + ` rippled ${info.build_version} network_id=${info.network_id}\n`, + ) const { account_data } = await rpc('account_info', { account: GENESIS_ACCOUNT, @@ -135,10 +137,10 @@ async function main() { Account: GENESIS_ACCOUNT, Sequence: seq++, Fee: '200', - Flags: 0x00000020, // tfMPTCanTransfer + Flags: 0x00000020, // tfMPTCanTransfer AssetScale: 6, MaximumAmount: '1000000000000', - TransferFee: 100, // 0.010% — still mutable + TransferFee: 100, // 0.010% — still mutable MPTokenMetadata: toHex(METADATA_A), }) await advanceLedger() @@ -157,18 +159,20 @@ async function main() { // tifMPTRequireAuth, tifMPTMetadata; metadata renders as JSON in Detail. // Settings: CanLock + RequireAuth → "Immutable" badge; // Metadata field row → "Immutable" badge; TransferFee row mutable. - console.log('TX2 MPTokenIssuanceCreate — with ImmutableFlags (RTBIL / RWA treasury)') + console.log( + 'TX2 MPTokenIssuanceCreate — with ImmutableFlags (RTBIL / RWA treasury)', + ) const hash2 = await submit({ TransactionType: 'MPTokenIssuanceCreate', Account: GENESIS_ACCOUNT, Sequence: seq++, Fee: '200', - Flags: 0x00000024, // tfMPTCanTransfer | tfMPTRequireAuth + Flags: 0x00000024, // tfMPTCanTransfer | tfMPTRequireAuth AssetScale: 2, MaximumAmount: '9223372036854775807', - TransferFee: 500, // 0.500% + TransferFee: 500, // 0.500% MPTokenMetadata: toHex(METADATA_B), - ImmutableFlags: 0x00010006, // tifMPTCanLock + tifMPTRequireAuth + tifMPTMetadata + ImmutableFlags: 0x00010006, // tifMPTCanLock + tifMPTRequireAuth + tifMPTMetadata }) await advanceLedger() const tx2 = await getValidatedTx(hash2) @@ -181,14 +185,16 @@ async function main() { // Flags: tfMPTSetCanEscrow(0x10) | tfMPTSetCanTrade(0x20) = 0x30 // Shows: Set Simple with NO ImmutableFlags row; the tfMPTSetCan* bits appear // automatically in the Detail tab via TX_FLAGS.MPTokenIssuanceSet. - console.log('TX3 MPTokenIssuanceSet — enable CanEscrow + CanTrade on Issuance A') + console.log( + 'TX3 MPTokenIssuanceSet — enable CanEscrow + CanTrade on Issuance A', + ) const hash3 = await submit({ TransactionType: 'MPTokenIssuanceSet', Account: GENESIS_ACCOUNT, Sequence: seq++, Fee: '200', MPTokenIssuanceID: issuanceIdA, - Flags: 0x00000030, // tfMPTSetCanEscrow | tfMPTSetCanTrade + Flags: 0x00000030, // tfMPTSetCanEscrow | tfMPTSetCanTrade }) await advanceLedger() results.tx3 = { hash: hash3 } @@ -201,53 +207,75 @@ async function main() { // Shows: Set Simple with TransferFee row (0.075%), Metadata row (JSON rendered), // and "Immutable Flags" row listing tifMPTCanTrade, tifMPTCanTransfer, // tifMPTTransferFee. - console.log('TX4 MPTokenIssuanceSet — update metadata + TransferFee + lock via ImmutableFlags on Issuance A') + console.log( + 'TX4 MPTokenIssuanceSet — update metadata + TransferFee + lock via ImmutableFlags on Issuance A', + ) const hash4 = await submit({ TransactionType: 'MPTokenIssuanceSet', Account: GENESIS_ACCOUNT, Sequence: seq++, Fee: '200', MPTokenIssuanceID: issuanceIdA, - TransferFee: 750, // update: 0.010% → 0.075% + TransferFee: 750, // update: 0.010% → 0.075% MPTokenMetadata: toHex(METADATA_A_UPDATED), - ImmutableFlags: 0x00020030, // tifMPTCanTrade + tifMPTCanTransfer + tifMPTTransferFee + ImmutableFlags: 0x00020030, // tifMPTCanTrade + tifMPTCanTransfer + tifMPTTransferFee }) await advanceLedger() results.tx4 = { hash: hash4 } console.log(` hash: ${hash4}\n`) // ── Summary ────────────────────────────────────────────────────────────────── - console.log('═══════════════════════════════════════════════════════════════════') + console.log( + '═══════════════════════════════════════════════════════════════════', + ) console.log(' EXPLORER URLS') - console.log('═══════════════════════════════════════════════════════════════════\n') + console.log( + '═══════════════════════════════════════════════════════════════════\n', + ) - console.log('── MPTokenIssuanceCreate / Simple view ─────────────────────────────') + console.log( + '── MPTokenIssuanceCreate / Simple view ─────────────────────────────', + ) console.log(' TX1 DYNX — no ImmutableFlags ("Immutable Flags" row absent)') console.log(` ${EXPLORER}/transactions/${results.tx1.hash}/simple`) console.log() - console.log(' TX2 RTBIL — ImmutableFlags=0x10006 (tifMPTCanLock, tifMPTRequireAuth, tifMPTMetadata)') + console.log( + ' TX2 RTBIL — ImmutableFlags=0x10006 (tifMPTCanLock, tifMPTRequireAuth, tifMPTMetadata)', + ) console.log(` ${EXPLORER}/transactions/${results.tx2.hash}/simple`) console.log() - console.log('── MPTokenIssuanceSet / Simple view ────────────────────────────────') - console.log(' TX3 tfMPTSetCanEscrow + tfMPTSetCanTrade (see Detail tab for flag decode)') + console.log( + '── MPTokenIssuanceSet / Simple view ────────────────────────────────', + ) + console.log( + ' TX3 tfMPTSetCanEscrow + tfMPTSetCanTrade (see Detail tab for flag decode)', + ) console.log(` ${EXPLORER}/transactions/${results.tx3.hash}/simple`) console.log() console.log(' TX4 TransferFee + Metadata (JSON) + ImmutableFlags rows') console.log(` ${EXPLORER}/transactions/${results.tx4.hash}/simple`) console.log() - console.log('── MPT token Settings page ─────────────────────────────────────────') - console.log(' Issuance A DYNX — "Can Enable" on unlocked caps; CanTrade+CanTransfer+TransferFee → "Immutable"') + console.log( + '── MPT token Settings page ─────────────────────────────────────────', + ) + console.log( + ' Issuance A DYNX — "Can Enable" on unlocked caps; CanTrade+CanTransfer+TransferFee → "Immutable"', + ) console.log(` ${EXPLORER}/token/mpt/${results.tx1.issuanceId}`) console.log() - console.log(' Issuance B RTBIL — CanLock+RequireAuth+Metadata → "Immutable"; TransferFee still mutable') + console.log( + ' Issuance B RTBIL — CanLock+RequireAuth+Metadata → "Immutable"; TransferFee still mutable', + ) console.log(` ${EXPLORER}/token/mpt/${results.tx2.issuanceId}`) console.log() - console.log('═══════════════════════════════════════════════════════════════════') + console.log( + '═══════════════════════════════════════════════════════════════════', + ) } -main().catch(err => { +main().catch((err) => { console.error('\nFatal:', err.message) process.exit(1) }) diff --git a/src/rippled/lib/utils.ts b/src/rippled/lib/utils.ts index 9b3991e6c..2e1781d5c 100644 --- a/src/rippled/lib/utils.ts +++ b/src/rippled/lib/utils.ts @@ -329,7 +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), + immutableFlags: buildFlags( + info.ImmutableFlags, + MPT_ISSUANCE_IMMUTABLE_FLAGS, + ), issuerEncryptionKey: info.IssuerEncryptionKey, auditorEncryptionKey: info.AuditorEncryptionKey, } From d75f1b0b598744ba740814d83ed3033e7488eae4 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Fri, 28 Aug 2026 10:28:28 -1000 Subject: [PATCH 7/9] removed TODO --- .../components/Transaction/MPTokenIssuanceCreate/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts index 1bfb17585..2481383b7 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts @@ -7,7 +7,7 @@ import { } from '../../../../../rippled/lib/utils' import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils' -// TODO: use MPTokenIssuanceCreate when DynamicMPT is supported on xrpl.js +// Extends the base type until xrpl.js ships ImmutableFlags (PR #3417). interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate { ImmutableFlags?: number } From 3a41d615fed8b04abbc7cff9d6e754ae35e86364 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Fri, 28 Aug 2026 10:35:42 -1000 Subject: [PATCH 8/9] use xrpl.js models --- package-lock.json | 147 ++++++++---------- package.json | 2 +- .../MPTokenIssuanceCreate/parser.ts | 7 +- .../Transaction/MPTokenIssuanceSet/Simple.tsx | 10 +- 4 files changed, 65 insertions(+), 101 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ea2d4cdc..ce4e95ac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,7 +101,7 @@ "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "xrpl": "^4.5.0" + "xrpl": "^5.1.0" }, "engines": { "node": ">=22.0.0 <23", @@ -4561,29 +4561,28 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@noble/curves/-/curves-2.4.0.tgz", + "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@noble/hashes": "2.4.0" }, "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -5349,39 +5348,37 @@ "license": "MIT" }, "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "dev": true, + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/bip32/-/bip32-2.4.0.tgz", + "integrity": "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few==", "dev": true, "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "version": "2.4.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/hashes": "2.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -6931,16 +6928,14 @@ "node": ">=18.0.0" } }, - "node_modules/@xrplf/isomorphic/node_modules/@noble/hashes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", - "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", - "license": "MIT", + "node_modules/@xrplf/mpt-crypto": { + "version": "0.1.1", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@xrplf/mpt-crypto/-/mpt-crypto-0.1.1.tgz", + "integrity": "sha512-Wv9wbk60DRGWvI+B9DITwfWygCWDfEjv8yWxiLJyDf/G8UPkfhVlUKIflF9nMaBFMygPRkXrpO1jNqTj6/vJow==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">= 18" } }, "node_modules/@xrplf/prettier-config": { @@ -6951,14 +6946,14 @@ "license": "ISC" }, "node_modules/@xrplf/secret-numbers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@xrplf/secret-numbers/-/secret-numbers-2.0.0.tgz", - "integrity": "sha512-z3AOibRTE9E8MbjgzxqMpG1RNaBhQ1jnfhNCa1cGf2reZUJzPMYs4TggQTc7j8+0WyV3cr7y/U8Oz99SXIkN5Q==", + "version": "3.0.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/@xrplf/secret-numbers/-/secret-numbers-3.0.0.tgz", + "integrity": "sha512-qpGhAZXv5noMDjCtfzq5NK0y5rrdwTVjKhhPcAYSE+a/gogBOgqdpCKyieprVVPCnmVmJnGeRoZKBAqpCGegsA==", "dev": true, "license": "ISC", "dependencies": { - "@xrplf/isomorphic": "^1.0.1", - "ripple-keypairs": "^2.0.0" + "@xrplf/isomorphic": "^1.0.2", + "ripple-keypairs": "^3.0.0" } }, "node_modules/accepts": { @@ -7752,14 +7747,10 @@ } }, "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } + "version": "10.0.2", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/bignumber.js/-/bignumber.js-10.0.2.tgz", + "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -19676,19 +19667,10 @@ "node": ">= 18" } }, - "node_modules/ripple-address-codec/node_modules/@scure/base": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.3.0.tgz", - "integrity": "sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ripple-binary-codec": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-2.9.0.tgz", - "integrity": "sha512-DOv4CPwm2B5mjAJNHE4tfes7tIqefi85iun8BskS7/i1PvLIwVydHIbTV5hbQ7eT69Zg4K5dtO1CzfuvQVAYvQ==", + "version": "2.10.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/ripple-binary-codec/-/ripple-binary-codec-2.10.0.tgz", + "integrity": "sha512-H4pBm2WdOMmVnA0HGUdWAShmUFdmqavWAsi2WlMCzOknc1XjrZssa/RxEyziaPww8MXe1OpgGU7uCs8G2nYe/A==", "license": "ISC", "dependencies": { "@xrplf/isomorphic": "^1.0.2", @@ -19699,25 +19681,19 @@ "node": ">= 18" } }, - "node_modules/ripple-binary-codec/node_modules/bignumber.js": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-10.0.2.tgz", - "integrity": "sha512-E8Wp9O06QA6lneJ4aRUXKYf/1GIomqUEmUMwtIOMtDxf1U52ffJY+y7JBk/8wRafA8qOIqLnXQGqonYXZdBnFQ==", - "license": "MIT" - }, "node_modules/ripple-keypairs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ripple-keypairs/-/ripple-keypairs-2.0.0.tgz", - "integrity": "sha512-b5rfL2EZiffmklqZk1W+dvSy97v3V/C7936WxCCgDynaGPp7GE6R2XO7EU9O2LlM/z95rj870IylYnOQs+1Rag==", + "version": "3.0.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/ripple-keypairs/-/ripple-keypairs-3.0.0.tgz", + "integrity": "sha512-lE69pD0E8hFNCqZoVXRyY45Yi8Ku+Qw7Rf1qRwPj4nOi34vp9NAuwzfiJH1IwXGWNCfEkwVfctG99CPTEoUf+g==", "dev": true, "license": "ISC", "dependencies": { - "@noble/curves": "^1.0.0", - "@xrplf/isomorphic": "^1.0.0", - "ripple-address-codec": "^5.0.0" + "@noble/curves": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "ripple-address-codec": "^5.0.1" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/robust-predicates": { @@ -23639,25 +23615,26 @@ "license": "MIT" }, "node_modules/xrpl": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-4.6.0.tgz", - "integrity": "sha512-0nXZfqDHRJ6bsDv1WtA9MdCYalMtXuxVa9mtLdqT3xypRKf2LwT5DbuGL/kHcVfuqk3B+ly+SFARlrnX+LHtRQ==", + "version": "5.1.0", + "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/xrpl/-/xrpl-5.1.0.tgz", + "integrity": "sha512-wYTiyrFqV22kwSX2lrUCD10Ns8Zyv2zz8rZ2Pi0KOVm5Sc7/8m7rrca2dAZy1vGhOt36tkvWyX4aC2SQveyT9w==", "dev": true, "license": "ISC", "dependencies": { - "@scure/bip32": "^1.3.1", - "@scure/bip39": "^1.2.1", - "@xrplf/isomorphic": "^1.0.1", - "@xrplf/secret-numbers": "^2.0.0", - "bignumber.js": "^9.0.0", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@xrplf/isomorphic": "^1.0.2", + "@xrplf/mpt-crypto": "^0.1.1", + "@xrplf/secret-numbers": "^3.0.0", + "bignumber.js": "^10.0.2", "eventemitter3": "^5.0.1", "fast-json-stable-stringify": "^2.1.0", - "ripple-address-codec": "^5.0.0", - "ripple-binary-codec": "^2.7.0", - "ripple-keypairs": "^2.0.0" + "ripple-address-codec": "^5.0.1", + "ripple-binary-codec": "^2.10.0", + "ripple-keypairs": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.19.0" } }, "node_modules/xrpl-client": { diff --git a/package.json b/package.json index cc8d56238..3c11e9c5e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "ts-jest": "^29.4.1", "ts-node": "^10.9.2", "typescript": "^5.9.3", - "xrpl": "^4.5.0" + "xrpl": "^5.1.0" }, "resolutions": { "jest-environment-jsdom": "29.3.1", diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts index 2481383b7..8aa49165d 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceCreate/parser.ts @@ -7,13 +7,8 @@ import { } from '../../../../../rippled/lib/utils' import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils' -// Extends the base type until xrpl.js ships ImmutableFlags (PR #3417). -interface MPTokenIssuanceCreateExtended extends MPTokenIssuanceCreate { - ImmutableFlags?: number -} - export const parser: TransactionParser< - MPTokenIssuanceCreateExtended, + MPTokenIssuanceCreate, MPTokenIssuanceCreateInstructions > = (tx, meta) => { const immutableFlags = buildFlags(tx.ImmutableFlags, MPT_IMMUTABLE_FLAGS) diff --git a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx index 5bcda5df6..787cea2c3 100644 --- a/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx +++ b/src/containers/shared/components/Transaction/MPTokenIssuanceSet/Simple.tsx @@ -17,17 +17,9 @@ import { } from '../../../../../rippled/lib/utils' import { MPT_IMMUTABLE_FLAGS } from '../../../transactionUtils' -interface MPTokenIssuanceSetExtended extends MPTokenIssuanceSet { - MPTokenMetadata?: string - TransferFee?: number - ImmutableFlags?: number - IssuerEncryptionKey?: string - AuditorEncryptionKey?: string -} - export const Simple: TransactionSimpleComponent = ({ data, -}: TransactionSimpleProps) => { +}: TransactionSimpleProps) => { const { MPTokenIssuanceID, Holder, From 254293d2530d623870b32dc730a6b25ad5805c15 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Fri, 28 Aug 2026 10:42:35 -1000 Subject: [PATCH 9/9] use public registry --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ce4e95ac1..2743ac8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23616,7 +23616,7 @@ }, "node_modules/xrpl": { "version": "5.1.0", - "resolved": "https://artifactory.ops.ripple.com/artifactory/api/npm/ripple-npm/xrpl/-/xrpl-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-5.1.0.tgz", "integrity": "sha512-wYTiyrFqV22kwSX2lrUCD10Ns8Zyv2zz8rZ2Pi0KOVm5Sc7/8m7rrca2dAZy1vGhOt36tkvWyX4aC2SQveyT9w==", "dev": true, "license": "ISC",