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 @@ -945,5 +945,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 @@ -946,5 +946,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 @@ -946,5 +946,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 @@ -946,5 +946,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 @@ -946,5 +946,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 @@ -946,5 +946,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 @@ -946,5 +946,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
}
72 changes: 67 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,28 @@ import { useTranslation } from 'react-i18next'

interface Props {
flags?: string[]
immutableFlags?: string[]
}

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.
immutableFlag?: string
}

export const Settings = ({ flags = [] }: Props): JSX.Element => {
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)

const flagItems: FlagItem[] = [
{
key: 'locked',
Expand All @@ -23,31 +34,37 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
key: 'canLock',
label: t('can_lock'),
enabled: flags.includes('lsfMPTCanLock'),
immutableFlag: 'lsifMPTCanLock',
},
{
key: 'requireAuth',
label: t('require_auth'),
enabled: flags.includes('lsfMPTRequireAuth'),
immutableFlag: 'lsifMPTRequireAuth',
},
{
key: 'canEscrow',
label: t('can_escrow'),
enabled: flags.includes('lsfMPTCanEscrow'),
immutableFlag: 'lsifMPTCanEscrow',
},
{
key: 'canTrade',
label: t('can_trade'),
enabled: flags.includes('lsfMPTCanTrade'),
immutableFlag: 'lsifMPTCanTrade',
},
{
key: 'canTransfer',
label: t('can_transfer'),
enabled: flags.includes('lsfMPTCanTransfer'),
immutableFlag: 'lsifMPTCanTransfer',
},
{
key: 'canClawback',
label: t('can_clawback'),
enabled: flags.includes('lsfMPTCanClawback'),
immutableFlag: 'lsifMPTCanClawback',
},
{
key: 'canConfidentialAmount',
Expand All @@ -56,17 +73,62 @@ export const Settings = ({ flags = [] }: Props): JSX.Element => {
},
]

// 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'),
immutableFlag: 'lsifMPTMetadata',
},
{
key: 'transferFee',
label: t('transfer_fee'),
immutableFlag: 'lsifMPTTransferFee',
},
].filter((item) => isStillMutable(item.immutableFlag))

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 "Mutable" pill while the flag is still
not locked (not in immutableFlags) and not yet enabled. */}
{isStillMutable(flag.immutableFlag) && !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,
immutableFlags,
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} immutableFlags={immutableFlags} />
{(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
61 changes: 58 additions & 3 deletions 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} immutableFlags={props.immutableFlags} />
</I18nextProvider>,
)

Expand All @@ -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', () => {
Expand Down Expand Up @@ -101,4 +103,57 @@ describe('Settings component', () => {
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. 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('hides the mutable badge for a capability once it is locked (in immutableFlags)', () => {
const { container, getAllByTestId } = renderComponent({
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)
})

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'],
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)
})

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('shows extra rows for mutable metadata and transfer fee when not locked', () => {
const { container, getAllByTestId } = renderComponent({
flags: [],
immutableFlags: [],
})
// 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)
})
})
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[]
immutableFlags?: 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,
immutableFlags,
} = data.instructions
const { t } = useTranslation()
const language = useLanguage()
const formattedFee =
Expand Down Expand Up @@ -64,6 +70,17 @@ export const Simple: TransactionSimpleComponent = ({
)}
</SimpleRow>
)}
{immutableFlags && immutableFlags.length > 0 && (
<SimpleRow
label={t('mutable_flags')}
className="flag"
data-testid="mpt-mutable-flags"
>
{immutableFlags.map((flag) => (
<div key={flag}>{flag}</div>
))}
</SimpleRow>
)}
</>
)
}
Loading
Loading