diff --git a/.ci-config/rippled.cfg b/.ci-config/rippled.cfg index 05d5753eea..361782cffa 100644 --- a/.ci-config/rippled.cfg +++ b/.ci-config/rippled.cfg @@ -193,6 +193,7 @@ Batch PermissionedDEX TokenEscrow SingleAssetVault +DynamicMPT # This section can be used to simulate various FeeSettings scenarios for rippled node in standalone mode [voting] diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8a2f528c1d..a6100b565b 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -122,8 +122,11 @@ jobs: fetch-depth: 0 - name: Run docker in background + id: run-docker run: | docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_info || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a" + CONTAINER_ID=$(docker ps -aqf "name=rippled-service") + echo "docker-container-id=$CONTAINER_ID" >> $GITHUB_OUTPUT - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 @@ -154,6 +157,17 @@ jobs: - run: npm run build + - name: Check if Docker container is running + id: check-docker-container + run: | + if ! docker ps | grep -q rippled-service; then + echo "INFO: Currently running docker containers:" + docker ps + echo "ERROR: rippled-service Docker container is not running" + exit 1 + fi + docker inspect ${{ steps.run-docker.outputs.docker-container-id }} + - name: Run integration test run: npm run test:integration @@ -181,8 +195,11 @@ jobs: node-version: ${{ matrix.node-version }} - name: Run docker in background + id: run-docker run: | docker run --detach --rm -p 6006:6006 --volume "${{ github.workspace }}/.ci-config/":"/etc/opt/ripple/" --name rippled-service --health-cmd="rippled server_info || exit 1" --health-interval=5s --health-retries=10 --health-timeout=2s --env GITHUB_ACTIONS=true --env CI=true --entrypoint bash ${{ env.RIPPLED_DOCKER_IMAGE }} -c "rippled -a" + CONTAINER_ID=$(docker ps -aqf "name=rippled-service") + echo "docker-container-id=$CONTAINER_ID" >> $GITHUB_OUTPUT - name: Setup npm version 10 run: | @@ -208,6 +225,17 @@ jobs: - run: npm run build + - name: Check if Docker container is running + id: check-docker-container + run: | + if ! docker ps | grep -q rippled-service; then + echo "INFO: Currently running docker containers:" + docker ps + echo "ERROR: rippled-service Docker container is not running" + exit 1 + fi + docker inspect ${{ steps.run-docker.outputs.docker-container-id }} + - name: Run integration test run: npm run test:browser diff --git a/packages/ripple-binary-codec/src/enums/definitions.json b/packages/ripple-binary-codec/src/enums/definitions.json index 4899a4df01..f7e5604bfc 100644 --- a/packages/ripple-binary-codec/src/enums/definitions.json +++ b/packages/ripple-binary-codec/src/enums/definitions.json @@ -680,6 +680,16 @@ "type": "UInt32" } ], + [ + "MutableFlags", + { + "isSerialized": true, + "isSigningField": true, + "isVLEncoded": false, + "nth": 53, + "type": "UInt32" + } + ], [ "IndexNext", { diff --git a/packages/xrpl/HISTORY.md b/packages/xrpl/HISTORY.md index 4b3d526306..1d3829b5b4 100644 --- a/packages/xrpl/HISTORY.md +++ b/packages/xrpl/HISTORY.md @@ -4,6 +4,9 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr ## Unreleased +### Added +* Support for `Dynamic MPT` (XLS-94D) + ### Fixed * Fix incorrect type checking in `validateVaultCreate` that prevented vault creation with MPT as an asset. diff --git a/packages/xrpl/src/models/ledger/LedgerEntry.ts b/packages/xrpl/src/models/ledger/LedgerEntry.ts index d106090fa3..44d0bb98f0 100644 --- a/packages/xrpl/src/models/ledger/LedgerEntry.ts +++ b/packages/xrpl/src/models/ledger/LedgerEntry.ts @@ -10,6 +10,7 @@ import DirectoryNode from './DirectoryNode' import Escrow from './Escrow' import FeeSettings from './FeeSettings' import LedgerHashes from './LedgerHashes' +import { MPTokenIssuance } from './MPTokenIssuance' import NegativeUNL from './NegativeUNL' import Offer from './Offer' import Oracle from './Oracle' @@ -46,6 +47,7 @@ type LedgerEntry = | Vault | XChainOwnedClaimID | XChainOwnedCreateAccountClaimID + | MPTokenIssuance type LedgerEntryFilter = | 'account' diff --git a/packages/xrpl/src/models/ledger/MPTokenIssuance.ts b/packages/xrpl/src/models/ledger/MPTokenIssuance.ts index b590071478..47a07d1192 100644 --- a/packages/xrpl/src/models/ledger/MPTokenIssuance.ts +++ b/packages/xrpl/src/models/ledger/MPTokenIssuance.ts @@ -11,4 +11,92 @@ export interface MPTokenIssuance extends BaseLedgerEntry, HasPreviousTxnID { MPTokenMetadata?: string OwnerNode?: string LockedAmount?: string + DomainID?: string + MutableFlags: number +} + +export interface MPTokenIssuanceFlagsInterface { + lsfMPTLocked?: boolean + lsfMPTCanLock?: boolean + lsfMPTRequireAuth?: boolean + lsfMPTCanEscrow?: boolean + lsfMPTCanTrade?: boolean + lsfMPTCanTransfer?: boolean + lsfMPTCanClawback?: boolean + + /** + * Indicates flag lsfMPTCanLock can be changed + */ + lsmfMPTCanMutateCanLock?: boolean + /** + * Indicates flag lsfMPTRequireAuth can be changed + */ + lsmfMPTCanMutateRequireAuth?: boolean + /** + * Indicates flag lsfMPTCanEscrow can be changed + */ + lsmfMPTCanMutateCanEscrow?: boolean + /** + * Indicates flag lsfMPTCanTrade can be changed + */ + lsmfMPTCanMutateCanTrade?: boolean + /** + * Indicates flag lsfMPTCanTransfer can be changed + */ + lsmfMPTCanMutateCanTransfer?: boolean + /** + * Indicates flag lsfMPTCanClawback can be changed + */ + lsmfMPTCanMutateCanClawback?: boolean + /** + * Allows field MPTokenMetadata to be modified + */ + lsmfMPTCanMutateMetadata?: boolean + /** + * Allows field TransferFee to be modified + */ + lsmfMPTCanMutateTransferFee?: boolean +} + +export enum MPTokenIssuanceFlags { + lsfMPTLocked = 0x00000001, + lsfMPTCanLock = 0x00000002, + lsfMPTRequireAuth = 0x00000004, + lsfMPTCanEscrow = 0x00000008, + lsfMPTCanTrade = 0x00000010, + lsfMPTCanTransfer = 0x00000020, + lsfMPTCanClawback = 0x00000040, + + /** + * Indicates flag lsfMPTCanLock can be changed + */ + lsmfMPTCanMutateCanLock = 0x00000002, + /** + * Indicates flag lsfMPTRequireAuth can be changed + */ + lsmfMPTCanMutateRequireAuth = 0x00000004, + /** + * Indicates flag lsfMPTCanEscrow can be changed + */ + lsmfMPTCanMutateCanEscrow = 0x00000008, + /** + * Indicates flag lsfMPTCanTrade can be changed + */ + lsmfMPTCanMutateCanTrade = 0x00000010, + /** + * Indicates flag lsfMPTCanTransfer can be changed + */ + lsmfMPTCanMutateCanTransfer = 0x00000020, + /** + * Indicates flag lsfMPTCanClawback can be changed + */ + lsmfMPTCanMutateCanClawback = 0x00000040, + /** + * Allows field MPTokenMetadata to be modified + */ + lsmfMPTCanMutateMetadata = 0x00010000, + /** + * Allows field TransferFee to be modified + */ + lsmfMPTCanMutateTransferFee = 0x00020000, } diff --git a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts index b9b753cc08..db2a60c83a 100644 --- a/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts +++ b/packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts @@ -1,5 +1,5 @@ import { ValidationError } from '../../errors' -import { isHex, INTEGER_SANITY_CHECK, isFlagEnabled } from '../utils' +import { isHex, INTEGER_SANITY_CHECK, isFlagEnabled, hasFlag } from '../utils' import { BaseTransaction, @@ -11,12 +11,13 @@ import { MAX_MPT_META_BYTE_LENGTH, MPT_META_WARNING_HEADER, validateMPTokenMetadata, + isDomainID, } from './common' import type { TransactionMetadataBase } from './metadata' // 2^63 - 1 const MAX_AMT = '9223372036854775807' -const MAX_TRANSFER_FEE = 50000 +export const MAX_TRANSFER_FEE = 50000 /** * Transaction Flags for an MPTokenIssuanceCreate Transaction. @@ -55,6 +56,54 @@ export enum MPTokenIssuanceCreateFlags { tfMPTCanClawback = 0x00000040, } +export enum MPTokenIssuanceCreateMutableFlags { + /** + * If set, Indicates flag lsfMPTCanLock can be changed. + */ + tmfMPTCanMutateCanLock = 0x00000002, + /** + * If set, Indicates flag lsfMPTRequireAuth can be changed + */ + tmfMPTCanMutateRequireAuth = 0x00000004, + /** + * If set, Indicates flag lsfMPTCanEscrow can be changed. + */ + tmfMPTCanMutateCanEscrow = 0x00000008, + /** + * If set, Indicates flag lsfMPTCanTrade can be changed. + */ + tmfMPTCanMutateCanTrade = 0x00000010, + /** + * If set, Indicates flag lsfMPTCanTransfer can be changed. + */ + tmfMPTCanMutateCanTransfer = 0x00000020, + /** + * If set, Indicates flag lsfMPTCanClawback can be changed. + */ + tmfMPTCanMutateCanClawback = 0x00000040, + /** + * If set, Allows field MPTokenMetadata to be modified. + */ + tmfMPTCanMutateMetadata = 0x00010000, + /** + * If set, Allows field TransferFee to be modified. + */ + tmfMPTCanMutateTransferFee = 0x00020000, +} + +/* eslint-disable no-bitwise -- Need bitwise operations to replicate rippled behavior */ +export const tmfMPTokenIssuanceCreateMutableMask = ~( + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanLock | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateRequireAuth | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanEscrow | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanTrade | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanTransfer | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanClawback | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateMetadata | + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateTransferFee +) +/* eslint-enable no-bitwise */ + /** * Map of flags to boolean values representing {@link MPTokenIssuanceCreate} transaction * flags. @@ -63,14 +112,72 @@ export enum MPTokenIssuanceCreateFlags { */ export interface MPTokenIssuanceCreateFlagsInterface extends GlobalFlagsInterface { + /** + * If set, indicates that the MPT can be locked both individually and globally. + * If not set, the MPT cannot be locked in any way. + */ tfMPTCanLock?: boolean + /** + * If set, indicates that individual holders must be authorized. + * This enables issuers to limit who can hold their assets. + */ tfMPTRequireAuth?: boolean + /** + * If set, indicates that individual holders can place their balances into an escrow. + */ tfMPTCanEscrow?: boolean + /** + * If set, indicates that individual holders can trade their balances + * using the XRP Ledger DEX or AMM. + */ tfMPTCanTrade?: boolean + /** + * If set, indicates that tokens may be transferred to other accounts + * that are not the issuer. + */ tfMPTCanTransfer?: boolean + /** + * If set, indicates that the issuer may use the Clawback transaction + * to clawback value from individual holders. + */ tfMPTCanClawback?: boolean } +export interface MPTokenIssuanceCreateMutableFlagsInterface { + /** + * If set, Indicates flag lsfMPTCanLock can be changed. + */ + tmfMPTCanMutateCanLock?: boolean + /** + * If set, Indicates flag lsfMPTRequireAuth can be changed. + */ + tmfMPTCanMutateRequireAuth?: boolean + /** + * If set, Indicates flag lsfMPTCanEscrow can be changed. + */ + tmfMPTCanMutateCanEscrow?: boolean + /** + * If set, Indicates flag lsfMPTCanTrade can be changed. + */ + tmfMPTCanMutateCanTrade?: boolean + /** + * If set, Indicates flag lsfMPTCanTransfer can be changed. + */ + tmfMPTCanMutateCanTransfer?: boolean + /** + * If set, Indicates flag lsfMPTCanClawback can be changed. + */ + tmfMPTCanMutateCanClawback?: boolean + /** + * If set, Allows field MPTokenMetadata to be modified. + */ + tmfMPTCanMutateMetadata?: boolean + /** + * If set, Allows field TransferFee to be modified. + */ + tmfMPTCanMutateTransferFee?: boolean +} + /** * The MPTokenIssuanceCreate transaction creates a MPTokenIssuance object * and adds it to the relevant directory node of the creator account. @@ -120,13 +227,15 @@ export interface MPTokenIssuanceCreate extends BaseTransaction { MPTokenMetadata?: string Flags?: number | MPTokenIssuanceCreateFlagsInterface + MutableFlags?: number + DomainID?: string } export interface MPTokenIssuanceCreateMetadata extends TransactionMetadataBase { mpt_issuance_id?: string } -/* eslint-disable max-lines-per-function -- Not needed to reduce function */ +/* eslint-disable max-lines-per-function, max-statements -- Not needed to reduce function */ /** * Verify the form and type of an MPTokenIssuanceCreate at runtime. * @@ -141,6 +250,31 @@ export function validateMPTokenIssuanceCreate( validateOptionalField(tx, 'MPTokenMetadata', isString) validateOptionalField(tx, 'TransferFee', isNumber) validateOptionalField(tx, 'AssetScale', isNumber) + validateOptionalField(tx, 'MutableFlags', isNumber) + validateOptionalField(tx, 'DomainID', isDomainID) + + if ( + tx.DomainID != null && + !hasFlag( + tx, + MPTokenIssuanceCreateFlags.tfMPTRequireAuth, + 'tfMPTRequireAuth', + ) + ) { + throw new ValidationError( + 'MPTokenIssuanceCreate: Cannot set DomainID unless tfMPTRequireAuth flag is set.', + ) + } + + if ( + tx.MutableFlags != null && + // eslint-disable-next-line no-bitwise -- Need bitwise operations to replicate rippled behavior + tx.MutableFlags & tmfMPTokenIssuanceCreateMutableMask + ) { + throw new ValidationError( + 'MPTokenIssuanceCreate: Invalid MutableFlags value', + ) + } if ( typeof tx.MPTokenMetadata === 'string' && @@ -157,7 +291,7 @@ export function validateMPTokenIssuanceCreate( throw new ValidationError('MPTokenIssuanceCreate: Invalid MaximumAmount') } else if ( BigInt(tx.MaximumAmount) > BigInt(MAX_AMT) || - BigInt(tx.MaximumAmount) < BigInt(`0`) + BigInt(tx.MaximumAmount) <= BigInt(`0`) ) { throw new ValidationError( 'MPTokenIssuanceCreate: MaximumAmount out of range', @@ -202,4 +336,4 @@ export function validateMPTokenIssuanceCreate( } } } -/* eslint-enable max-lines-per-function */ +/* eslint-enable max-lines-per-function, max-statements */ diff --git a/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts b/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts index 12d15260ff..bc124a2803 100644 --- a/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts +++ b/packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts @@ -1,5 +1,7 @@ import { ValidationError } from '../../errors' -import { isFlagEnabled } from '../utils' +import { isFlagEnabled, isHex } from '../utils' +// eslint-disable-next-line import/no-cycle -- this method is needed to convert txn flags to number +import { convertTxFlagsToNumber } from '../utils/flags' import { BaseTransaction, @@ -10,7 +12,13 @@ import { validateOptionalField, isAccount, GlobalFlagsInterface, + isNumber, + MAX_MPT_META_BYTE_LENGTH, + isDomainID, } from './common' +import { MAX_TRANSFER_FEE } from './MPTokenIssuanceCreate' + +import type { Transaction } from '.' /** * Transaction Flags for an MPTokenIssuanceSet Transaction. @@ -28,6 +36,50 @@ export enum MPTokenIssuanceSetFlags { tfMPTUnlock = 0x00000002, } +export enum MPTokenIssuanceSetMutableFlags { + /* Sets the lsfMPTCanLock flag. Enables the token to be locked both individually and globally. */ + tmfMPTSetCanLock = 0x00000001, + /* Clears the lsfMPTCanLock flag. Disables both individual and global locking of the token. */ + tmfMPTClearCanLock = 0x00000002, + /* Sets the lsfMPTRequireAuth flag. Requires individual holders to be authorized. */ + tmfMPTSetRequireAuth = 0x00000004, + /* Clears the lsfMPTRequireAuth flag. Holders are not required to be authorized. */ + tmfMPTClearRequireAuth = 0x00000008, + /* Sets the lsfMPTCanEscrow flag. Allows holders to place balances into escrow. */ + tmfMPTSetCanEscrow = 0x00000010, + /* Clears the lsfMPTCanEscrow flag. Disallows holders from placing balances into escrow. */ + tmfMPTClearCanEscrow = 0x00000020, + /* Sets the lsfMPTCanTrade flag. Allows holders to trade balances on the XRPL DEX. */ + tmfMPTSetCanTrade = 0x00000040, + /* Clears the lsfMPTCanTrade flag. Disallows holders from trading balances on the XRPL DEX. */ + tmfMPTClearCanTrade = 0x00000080, + /* Sets the lsfMPTCanTransfer flag. Allows tokens to be transferred to non-issuer accounts. */ + tmfMPTSetCanTransfer = 0x00000100, + /* Clears the lsfMPTCanTransfer flag. Disallows transfers to non-issuer accounts. */ + tmfMPTClearCanTransfer = 0x00000200, + /* Sets the lsfMPTCanClawback flag. Enables the issuer to claw back tokens via Clawback or AMMClawback transactions. */ + tmfMPTSetCanClawback = 0x00000400, + /* Clears the lsfMPTCanClawback flag. The token can not be clawed back. */ + tmfMPTClearCanClawback = 0x00000800, +} + +/* eslint-disable no-bitwise -- Need bitwise operations to replicate rippled behavior */ +export const tmfMPTokenIssuanceSetMutableMask = ~( + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanLock | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanLock | + MPTokenIssuanceSetMutableFlags.tmfMPTSetRequireAuth | + MPTokenIssuanceSetMutableFlags.tmfMPTClearRequireAuth | + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanEscrow | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanEscrow | + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTrade | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTrade | + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTransfer | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer | + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanClawback | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanClawback +) +/* eslint-enable no-bitwise */ + /** * Map of flags to boolean values representing {@link MPTokenIssuanceSet} transaction * flags. @@ -39,6 +91,33 @@ export interface MPTokenIssuanceSetFlagsInterface extends GlobalFlagsInterface { tfMPTUnlock?: boolean } +export interface MPTokenIssuanceSetMutableFlagsInterface { + /* Sets the lsfMPTCanLock flag. Enables the token to be locked both individually and globally. */ + tmfMPTSetCanLock?: boolean + /* Clears the lsfMPTCanLock flag. Disables both individual and global locking of the token. */ + tmfMPTClearCanLock?: boolean + /* Sets the lsfMPTRequireAuth flag. Requires individual holders to be authorized. */ + tmfMPTSetRequireAuth?: boolean + /* Clears the lsfMPTRequireAuth flag. Holders are not required to be authorized. */ + tmfMPTClearRequireAuth?: boolean + /* Sets the lsfMPTCanEscrow flag. Allows holders to place balances into escrow. */ + tmfMPTSetCanEscrow?: boolean + /* Clears the lsfMPTCanEscrow flag. Disallows holders from placing balances into escrow. */ + tmfMPTClearCanEscrow?: boolean + /* Sets the lsfMPTCanTrade flag. Allows holders to trade balances on the XRPL DEX. */ + tmfMPTSetCanTrade?: boolean + /* Clears the lsfMPTCanTrade flag. Disallows holders from trading balances on the XRPL DEX. */ + tmfMPTClearCanTrade?: boolean + /* Sets the lsfMPTCanTransfer flag. Allows tokens to be transferred to non-issuer accounts. */ + tmfMPTSetCanTransfer?: boolean + /* Clears the lsfMPTCanTransfer flag. Disallows transfers to non-issuer accounts. */ + tmfMPTClearCanTransfer?: boolean + /* Sets the lsfMPTCanClawback flag. Enables the issuer to claw back tokens via Clawback or AMMClawback transactions. */ + tmfMPTSetCanClawback?: boolean + /* Clears the lsfMPTCanClawback flag. The token can not be clawed back. */ + tmfMPTClearCanClawback?: boolean +} + /** * The MPTokenIssuanceSet transaction is used to globally lock/unlock a MPTokenIssuance, * or lock/unlock an individual's MPToken. @@ -55,8 +134,14 @@ export interface MPTokenIssuanceSet extends BaseTransaction { */ Holder?: Account Flags?: number | MPTokenIssuanceSetFlagsInterface + + MPTokenMetadata?: string + TransferFee?: number + MutableFlags?: number + DomainID?: string } +/* eslint-disable max-lines-per-function, max-statements -- All validation rules are needed */ /** * Verify the form and type of an MPTokenIssuanceSet at runtime. * @@ -67,6 +152,23 @@ export function validateMPTokenIssuanceSet(tx: Record): void { validateBaseTransaction(tx) validateRequiredField(tx, 'MPTokenIssuanceID', isString) validateOptionalField(tx, 'Holder', isAccount) + validateOptionalField(tx, 'MPTokenMetadata', isString) + validateOptionalField(tx, 'TransferFee', isNumber) + validateOptionalField(tx, 'MutableFlags', isNumber) + validateOptionalField(tx, 'DomainID', isDomainID) + + if (tx.DomainID != null && tx.Holder != null) { + throw new ValidationError( + 'MPTokenIssuanceSet: Cannot set both DomainID and Holder fields.', + ) + } + if ( + tx.MutableFlags != null && + // eslint-disable-next-line no-bitwise -- Need bitwise operations to replicate rippled behavior + tx.MutableFlags & tmfMPTokenIssuanceSetMutableMask + ) { + throw new ValidationError('MPTokenIssuanceSet: Invalid MutableFlags value') + } // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Not necessary const flags = (tx.Flags ?? 0) as number | MPTokenIssuanceSetFlagsInterface @@ -83,4 +185,97 @@ export function validateMPTokenIssuanceSet(tx: Record): void { if (isTfMPTLock && isTfMPTUnlock) { throw new ValidationError('MPTokenIssuanceSet: flag conflict') } + + if (tx.Holder != null && tx.Holder === tx.Account) { + throw new ValidationError( + 'MPTokenIssuanceSet: Holder cannot be the same as the Account.', + ) + } + + const isMutate = + tx.MutableFlags != null || + tx.MPTokenMetadata != null || + tx.TransferFee != null + if ( + (tx.Flags === 0 || tx.Flags === undefined) && + tx.DomainID == null && + !isMutate + ) { + throw new ValidationError( + 'MPTokenIssuanceSet: Transaction does not change the state of the MPTokenIssuance ledger object.', + ) + } + + if (isMutate && tx.Holder != null) { + throw new ValidationError( + 'MPTokenIssuanceSet: Holder field is not allowed when mutating MPTokenIssuance.', + ) + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Pseudo-Txn missing in BaseTransaction type. + if (isMutate && convertTxFlagsToNumber(tx as Transaction) !== 0) { + throw new ValidationError( + 'MPTokenIssuanceSet: Can not set flags when mutating MPTokenIssuance.', + ) + } + + const MPTMutabilityFlags: Array<{ setFlag: number; clearFlag: number }> = [ + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetCanLock, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanLock, + }, + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetRequireAuth, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearRequireAuth, + }, + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetCanEscrow, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanEscrow, + }, + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTrade, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTrade, + }, + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTransfer, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + }, + { + setFlag: MPTokenIssuanceSetMutableFlags.tmfMPTSetCanClawback, + clearFlag: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanClawback, + }, + ] + + // Can not set and clear the same flag + if (tx.MutableFlags != null) { + for (const flagPair of MPTMutabilityFlags) { + if ( + isFlagEnabled(tx.MutableFlags, flagPair.setFlag) && + isFlagEnabled(tx.MutableFlags, flagPair.clearFlag) + ) { + throw new ValidationError( + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + } + } + } + + if (typeof tx.TransferFee === 'number') { + if (tx.TransferFee < 0 || tx.TransferFee > MAX_TRANSFER_FEE) { + throw new ValidationError( + `MPTokenIssuanceSet: TransferFee must be between 0 and ${MAX_TRANSFER_FEE}`, + ) + } + } + + if ( + typeof tx.MPTokenMetadata === 'string' && + (!isHex(tx.MPTokenMetadata) || + tx.MPTokenMetadata.length / 2 > MAX_MPT_META_BYTE_LENGTH) + ) { + throw new ValidationError( + `MPTokenIssuanceSet: MPTokenMetadata (hex format) must be non-empty and no more than ${MAX_MPT_META_BYTE_LENGTH} bytes.`, + ) + } } +/* eslint-enable max-lines-per-function, max-statements */ diff --git a/packages/xrpl/src/models/transactions/common.ts b/packages/xrpl/src/models/transactions/common.ts index 660ae19460..b5645af3b5 100644 --- a/packages/xrpl/src/models/transactions/common.ts +++ b/packages/xrpl/src/models/transactions/common.ts @@ -774,7 +774,8 @@ export function isDomainID(domainID: unknown): domainID is string { return ( isString(domainID) && domainID.length === _DOMAIN_ID_LENGTH && - isHex(domainID) + isHex(domainID) && + domainID !== '0'.repeat(_DOMAIN_ID_LENGTH) ) } diff --git a/packages/xrpl/src/models/transactions/index.ts b/packages/xrpl/src/models/transactions/index.ts index fa8e73afed..e3e700e6a6 100644 --- a/packages/xrpl/src/models/transactions/index.ts +++ b/packages/xrpl/src/models/transactions/index.ts @@ -58,12 +58,16 @@ export { MPTokenIssuanceCreate, MPTokenIssuanceCreateFlags, MPTokenIssuanceCreateFlagsInterface, + MPTokenIssuanceCreateMutableFlags, + MPTokenIssuanceCreateMutableFlagsInterface, } from './MPTokenIssuanceCreate' export { MPTokenIssuanceDestroy } from './MPTokenIssuanceDestroy' export { MPTokenIssuanceSet, MPTokenIssuanceSetFlags, MPTokenIssuanceSetFlagsInterface, + MPTokenIssuanceSetMutableFlags, + MPTokenIssuanceSetMutableFlagsInterface, } from './MPTokenIssuanceSet' export { NFTokenAcceptOffer } from './NFTokenAcceptOffer' export { NFTokenBurn } from './NFTokenBurn' diff --git a/packages/xrpl/src/models/utils/flags.ts b/packages/xrpl/src/models/utils/flags.ts index ff04bf86f4..f044817638 100644 --- a/packages/xrpl/src/models/utils/flags.ts +++ b/packages/xrpl/src/models/utils/flags.ts @@ -12,6 +12,7 @@ import { BatchFlags } from '../transactions/batch' import { GlobalFlags } from '../transactions/common' import { MPTokenAuthorizeFlags } from '../transactions/MPTokenAuthorize' import { MPTokenIssuanceCreateFlags } from '../transactions/MPTokenIssuanceCreate' +// eslint-disable-next-line import/no-cycle -- this method is needed to map txn flags import { MPTokenIssuanceSetFlags } from '../transactions/MPTokenIssuanceSet' import { NFTokenCreateOfferFlags } from '../transactions/NFTokenCreateOffer' import { NFTokenMintFlags } from '../transactions/NFTokenMint' diff --git a/packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts b/packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts index 195fbe5949..fbe44f8331 100644 --- a/packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts +++ b/packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts @@ -6,6 +6,8 @@ import { MPTokenIssuanceCreateFlags, MPTokenIssuanceSetFlags, TransactionMetadata, + MPTokenIssuanceCreateMutableFlags, + MPTokenIssuanceSetMutableFlags, } from '../../../src' import serverUrl from '../serverUrl' import { @@ -75,4 +77,65 @@ describe('MPTokenIssuanceDestroy', function () { }, TIMEOUT, ) + + it( + 'Test Mutability of Flags as per Dynamic MPT (XLS-94D) amendment', + async () => { + const createTx: MPTokenIssuanceCreate = { + TransactionType: 'MPTokenIssuanceCreate', + Account: testContext.wallet.classicAddress, + Flags: MPTokenIssuanceCreateFlags.tfMPTCanTransfer, + MutableFlags: + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateTransferFee + + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateCanTransfer, + } + + const mptCreateRes = await testTransaction( + testContext.client, + createTx, + testContext.wallet, + ) + + const txHash = mptCreateRes.result.tx_json.hash + + const txResponse = await testContext.client.request({ + command: 'tx', + transaction: txHash, + }) + + const meta = txResponse.result + .meta as TransactionMetadata + + const mptID = meta.mpt_issuance_id + + const setTransferFeeTx: MPTokenIssuanceSet = { + TransactionType: 'MPTokenIssuanceSet', + Account: testContext.wallet.classicAddress, + MPTokenIssuanceID: mptID!, + // set the transfer fee to a non-zero value + TransferFee: 200, + } + + await testTransaction( + testContext.client, + setTransferFeeTx, + testContext.wallet, + ) + + // remove the ability to transfer the MPT + const clearTransferFlagTx: MPTokenIssuanceSet = { + TransactionType: 'MPTokenIssuanceSet', + Account: testContext.wallet.classicAddress, + MPTokenIssuanceID: mptID!, + MutableFlags: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + } + + await testTransaction( + testContext.client, + clearTransferFlagTx, + testContext.wallet, + ) + }, + TIMEOUT, + ) }) diff --git a/packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts b/packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts index 26a4d0496a..6dfc178587 100644 --- a/packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts +++ b/packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts @@ -1,11 +1,16 @@ import { stringToHex } from '@xrplf/isomorphic/src/utils' import { MPTokenIssuanceCreateFlags, MPTokenMetadata } from '../../src' +// import { MPTokenIssuanceCreateMutableFlags } from '../../src/models/transactions/MPTokenIssuanceCreate' import { MAX_MPT_META_BYTE_LENGTH, MPT_META_WARNING_HEADER, } from '../../src/models/transactions/common' -import { validateMPTokenIssuanceCreate } from '../../src/models/transactions/MPTokenIssuanceCreate' +import { + MPTokenIssuanceCreateMutableFlags, + tmfMPTokenIssuanceCreateMutableMask, + validateMPTokenIssuanceCreate, +} from '../../src/models/transactions/MPTokenIssuanceCreate' import { assertTxIsValid, assertTxValidationError } from '../testUtils' const assertValid = (tx: any): void => @@ -28,6 +33,8 @@ describe('MPTokenIssuanceCreate', function () { AssetScale: 2, TransferFee: 1, Flags: MPTokenIssuanceCreateFlags.tfMPTCanTransfer, + MutableFlags: + MPTokenIssuanceCreateMutableFlags.tmfMPTCanMutateTransferFee, MPTokenMetadata: stringToHex(`{ "ticker": "TBILL", "name": "T-Bill Yield Token", @@ -141,6 +148,60 @@ describe('MPTokenIssuanceCreate', function () { 'MPTokenIssuanceCreate: TransferFee cannot be provided without enabling tfMPTCanTransfer flag', ) }) + + it(`throws w/ invalid MutableFlags value`, async () => { + const invalid = { + TransactionType: 'MPTokenIssuanceCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MutableFlags: tmfMPTokenIssuanceCreateMutableMask, + } as any + + assertInvalid(invalid, 'MPTokenIssuanceCreate: Invalid MutableFlags value') + }) + + it(`throws with Zero MaximumAmount`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MaximumAmount: '0', + } as any + + assertInvalid(invalid, 'MPTokenIssuanceCreate: MaximumAmount out of range') + }) + + it(`throws with Zero DomainID`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + DomainID: '0'.repeat(64), + } as any + + assertInvalid(invalid, 'MPTokenIssuanceCreate: invalid field DomainID') + }) + + it(`throws with DomainID and tfMPTRequireAuth flag not set`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + DomainID: '1'.repeat(64), + Flags: 0, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceCreate: Cannot set DomainID unless tfMPTRequireAuth flag is set.', + ) + }) + + it(`throws with invalid type of DomainID`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceCreate', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + DomainID: 1, + } as any + + assertInvalid(invalid, 'MPTokenIssuanceCreate: invalid field DomainID') + }) }) /** diff --git a/packages/xrpl/test/models/MPTokenIssuanceSet.test.ts b/packages/xrpl/test/models/MPTokenIssuanceSet.test.ts index 2a05f2a71a..913f2bc1e6 100644 --- a/packages/xrpl/test/models/MPTokenIssuanceSet.test.ts +++ b/packages/xrpl/test/models/MPTokenIssuanceSet.test.ts @@ -1,5 +1,13 @@ +import { stringToHex } from '@xrplf/isomorphic/dist/utils' + import { MPTokenIssuanceSetFlags } from '../../src' -import { validateMPTokenIssuanceSet } from '../../src/models/transactions/MPTokenIssuanceSet' +import { MAX_MPT_META_BYTE_LENGTH } from '../../src/models/transactions/common' +import { MAX_TRANSFER_FEE } from '../../src/models/transactions/MPTokenIssuanceCreate' +import { + validateMPTokenIssuanceSet, + tmfMPTokenIssuanceSetMutableMask, + MPTokenIssuanceSetMutableFlags, +} from '../../src/models/transactions/MPTokenIssuanceSet' import { assertTxIsValid, assertTxValidationError } from '../testUtils' const assertValid = (tx: any): void => @@ -35,15 +43,12 @@ describe('MPTokenIssuanceSet', function () { assertValid(validMPTokenIssuanceSet) - // It's fine to not specify any flag, it means only tx fee is deducted - validMPTokenIssuanceSet = { + assertValid({ TransactionType: 'MPTokenIssuanceSet', Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', MPTokenIssuanceID: TOKEN_ID, - Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG', - } as any - - assertValid(validMPTokenIssuanceSet) + MutableFlags: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + } as any) }) it(`throws w/ missing MPTokenIssuanceID`, function () { @@ -75,4 +80,275 @@ describe('MPTokenIssuanceSet', function () { assertInvalid(invalid, 'MPTokenIssuanceSet: flag conflict') }) + + it(`Throws w/ invalid type of TransferFee`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + TransferFee: '100', + } as any + + assertInvalid(invalid, 'MPTokenIssuanceSet: invalid field TransferFee') + }) + + it(`Throws w/ invalid (too low) value of TransferFee`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + TransferFee: -1, + } as any + + assertInvalid( + invalid, + `MPTokenIssuanceSet: TransferFee must be between 0 and ${MAX_TRANSFER_FEE}`, + ) + }) + + it(`Throws w/ invalid (too high) value of TransferFee`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + TransferFee: MAX_TRANSFER_FEE + 1, + } as any + + assertInvalid( + invalid, + `MPTokenIssuanceSet: TransferFee must be between 0 and ${MAX_TRANSFER_FEE}`, + ) + }) + + it(`Throws w/ invalid type of MutableFlags`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: '100', + } as any + + assertInvalid(invalid, 'MPTokenIssuanceSet: invalid field MutableFlags') + }) + + it(`Throws w/ invalid MutableFlags value`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: tmfMPTokenIssuanceSetMutableMask, + } as any + + assertInvalid(invalid, 'MPTokenIssuanceSet: Invalid MutableFlags value') + }) + + it(`Throws w/ invalid type of MPTokenMetadata`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MPTokenMetadata: 1234, + } as any + + assertInvalid(invalid, 'MPTokenIssuanceSet: invalid field MPTokenMetadata') + }) + + it(`Throws w/ invalid (non-hex characters) MPTokenMetadata`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MPTokenMetadata: 'zznothex', + } as any + + assertInvalid( + invalid, + `MPTokenIssuanceSet: MPTokenMetadata (hex format) must be non-empty and no more than ${MAX_MPT_META_BYTE_LENGTH} bytes.`, + ) + }) + + it(`Throws w/ invalid (too large) MPTokenMetadata`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MPTokenMetadata: stringToHex('a'.repeat(MAX_MPT_META_BYTE_LENGTH + 1)), + } as any + + assertInvalid( + invalid, + `MPTokenIssuanceSet: MPTokenMetadata (hex format) must be non-empty and no more than ${MAX_MPT_META_BYTE_LENGTH} bytes.`, + ) + }) + + it(`Throws w/ invalid type of DomainID`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + DomainID: 1, + } as any + + assertInvalid(invalid, 'MPTokenIssuanceSet: invalid field DomainID') + }) + + it(`throws w/ identical holder and account ID`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + Holder: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Holder cannot be the same as the Account.', + ) + }) + + it(`Throws w/ no changes to the MPTokenIssuance ledger object`, function () { + const noOpMPTokenIssuanceSet = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG', + } as any + + assertInvalid( + noOpMPTokenIssuanceSet, + 'MPTokenIssuanceSet: Transaction does not change the state of the MPTokenIssuance ledger object.', + ) + }) + + it(`Throws w/ Holder field and mutating the MPTokenIssuance ledger object`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + Holder: 'rajgkBmMxmz161r8bWYH7CQAFZP5bA9oSG', + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Holder field is not allowed when mutating MPTokenIssuance.', + ) + }) + + it(`Throws w/ Flags field and mutating the MPTokenIssuance ledger object`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + Flags: MPTokenIssuanceSetFlags.tfMPTLock, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set flags when mutating MPTokenIssuance.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanLock flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanLock | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanLock, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanTransfer flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTransfer | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTransfer, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanClawback flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanClawback | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanClawback, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanEscrow flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanEscrow | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanEscrow, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanTrade flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetCanTrade | + MPTokenIssuanceSetMutableFlags.tmfMPTClearCanTrade, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) + + it(`Throws w/ setting and clearing the tmfMPTCanRequireAuth flag`, function () { + const invalid = { + TransactionType: 'MPTokenIssuanceSet', + Account: 'rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm', + MPTokenIssuanceID: TOKEN_ID, + MutableFlags: + // eslint-disable-next-line no-bitwise -- required to OR the flags + MPTokenIssuanceSetMutableFlags.tmfMPTSetRequireAuth | + MPTokenIssuanceSetMutableFlags.tmfMPTClearRequireAuth, + } as any + + assertInvalid( + invalid, + 'MPTokenIssuanceSet: Can not set and clear the same flag.', + ) + }) })