Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion public/locales/ca-CA/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -943,5 +943,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
6 changes: 5 additions & 1 deletion public/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": "Registered",
"included": "Included",
"maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).",
"maintenance_banner.countdown_prefix": "Starts in"
"maintenance_banner.countdown_prefix": "Starts in",
"mutable": "Mutable",
"mutable_flags": "Mutable Flags",
"mutable_flag_tooltip": "The issuer can enable this flag later",
"mutable_field_tooltip": "The issuer can change this value"
}
6 changes: 5 additions & 1 deletion public/locales/es-ES/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
6 changes: 5 additions & 1 deletion public/locales/fr-FR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
6 changes: 5 additions & 1 deletion public/locales/ja-JP/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
6 changes: 5 additions & 1 deletion public/locales/ko-KR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
6 changes: 5 additions & 1 deletion public/locales/my-MM/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -944,5 +944,9 @@
"registered": null,
"included": null,
"maintenance_banner.notice": null,
"maintenance_banner.countdown_prefix": null
"maintenance_banner.countdown_prefix": null,
"mutable": null,
"mutable_flags": null,
"mutable_flag_tooltip": null,
"mutable_field_tooltip": null
}
70 changes: 65 additions & 5 deletions src/containers/Token/MPT/Header/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -56,17 +72,61 @@ 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 (
<div className="header-box settings-box">
<div className="header-box-title">{t('settings')}</div>
<div className="header-box-contents">
{flagItems.map((flag) => (
<div className="header-box-item" key={flag.key}>
<div className="item-name">{flag.label}</div>
<div
className={`flag-status ${flag.enabled ? 'enabled' : 'disabled'}`}
>
{flag.enabled ? t('enabled') : t('disabled')}
<div className="flag-status-group">
{/* 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 && (
<div
className="flag-status mutable"
data-testid="mutable-badge"
title={t('mutable_flag_tooltip')}
>
{t('mutable')}
</div>
)}
<div
className={`flag-status ${
flag.enabled ? 'enabled' : 'disabled'
}`}
>
{flag.enabled ? t('enabled') : t('disabled')}
</div>
</div>
</div>
))}
{mutableFieldItems.map((field) => (
<div className="header-box-item" key={field.key}>
<div className="item-name">{field.label}</div>
<div className="flag-status-group">
<div
className="flag-status mutable"
data-testid="mutable-badge"
title={t('mutable_field_tooltip')}
>
{t('mutable')}
</div>
</div>
</div>
))}
Expand Down
3 changes: 2 additions & 1 deletion src/containers/Token/MPT/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt,
transferFee,
flags,
mutableFlags,
rawMPTMetadata,
parsedMPTMetadata,
isMPTMetadataCompliant,
Expand Down Expand Up @@ -235,7 +236,7 @@ export const Header = (props: Props) => {
confidentialOutstandingAmt={confidentialOutstandingAmt}
assetScale={assetScale}
/>
<Settings flags={flags} />
<Settings flags={flags} mutableFlags={mutableFlags} />
{(parsedMPTMetadata || rawMPTMetadata) && (
<Metadata
decodedMPTMetadata={(parsedMPTMetadata || rawMPTMetadata)!}
Expand Down
13 changes: 13 additions & 0 deletions src/containers/Token/MPT/Header/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@
align-items: center;
}

.flag-status-group {
display: flex;
align-items: center;
gap: 6px;
}

.flag-status {
min-width: 85px;
padding: 4px 10px;
Expand All @@ -156,6 +162,13 @@
background: $black-50;
color: $black-0;
}

&.mutable {
min-width: 0;
border: 1px solid $green-50;
background: transparent;
color: $green-50;
}
}
}

Expand Down
36 changes: 35 additions & 1 deletion src/containers/Token/MPT/test/Header/Settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('Settings component', () => {
const renderComponent = (props: any = {}) =>
render(
<I18nextProvider i18n={i18n}>
<Settings flags={props.flags} />
<Settings flags={props.flags} mutableFlags={props.mutableFlags} />
</I18nextProvider>,
)

Expand Down Expand Up @@ -101,4 +101,38 @@ describe('Settings component', () => {
const { container } = renderComponent({ flags: undefined })
expect(container.querySelectorAll('.flag-status.disabled')).toHaveLength(8)
})

it('does not render mutable badges when no mutable flags are set', () => {
const { queryAllByTestId } = renderComponent({ flags: [] })
expect(queryAllByTestId('mutable-badge')).toHaveLength(0)
})

it('marks a disabled capability flag as mutable', () => {
const { container, getAllByTestId } = renderComponent({
flags: [],
mutableFlags: ['lsmfMPTCanEnableCanLock'],
})
expect(getAllByTestId('mutable-badge')).toHaveLength(1)
expect(container.querySelectorAll('.flag-status.mutable')).toHaveLength(1)
})

it('hides the mutable badge once a capability is enabled', () => {
// Capabilities are one-directional (enable-only), so a mutable badge on an
// already-enabled flag would be misleading.
const { queryAllByTestId } = renderComponent({
flags: ['lsfMPTCanLock'],
mutableFlags: ['lsmfMPTCanEnableCanLock'],
})
expect(queryAllByTestId('mutable-badge')).toHaveLength(0)
})

it('renders extra rows for mutable metadata and transfer fee', () => {
const { container, getAllByTestId } = renderComponent({
flags: [],
mutableFlags: ['lsmfMPTCanMutateMetadata', 'lsmfMPTCanMutateTransferFee'],
})
// 8 capability rows + metadata + transfer fee
expect(container.querySelectorAll('.header-box-item')).toHaveLength(10)
expect(getAllByTestId('mutable-badge')).toHaveLength(2)
})
})
1 change: 1 addition & 0 deletions src/containers/shared/Interfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface FormattedMPTIssuance {
outstandingAmt?: string
confidentialOutstandingAmt?: string
flags?: string[]
mutableFlags?: string[]
transferFee?: number
rawMPTMetadata?: string
parsedMPTMetadata?: Record<string, unknown>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import './styles.scss'
export const Simple: TransactionSimpleComponent = ({
data,
}: TransactionSimpleProps<MPTokenIssuanceCreateInstructions>) => {
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 =
Expand Down Expand Up @@ -64,6 +70,17 @@ export const Simple: TransactionSimpleComponent = ({
)}
</SimpleRow>
)}
{mutableFlags && mutableFlags.length > 0 && (
<SimpleRow
label={t('mutable_flags')}
className="flag"
data-testid="mpt-mutable-flags"
>
{mutableFlags.map((flag) => (
<div key={flag}>{flag}</div>
))}
</SimpleRow>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -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,
}
}
Loading
Loading