diff --git a/xrpl/ledger-entry-types/mptoken_issuance.go b/xrpl/ledger-entry-types/mptoken_issuance.go index 0e21ffb1..65ac9dec 100644 --- a/xrpl/ledger-entry-types/mptoken_issuance.go +++ b/xrpl/ledger-entry-types/mptoken_issuance.go @@ -19,30 +19,37 @@ const ( LsfMPTCanTransfer uint32 = 0x00000020 // LsfMPTCanClawback if set, indicates that the issuer may use the Clawback transaction to claw back value from individual holders. LsfMPTCanClawback uint32 = 0x00000040 - // LsfMPTCanConfidentialAmount if set, indicates that confidential transfers are enabled for this token issuance. - LsfMPTCanConfidentialAmount uint32 = 0x00000080 + // LsfMPTCanHoldConfidentialBalance if set, indicates that holders may hold confidential balances for this token issuance. + LsfMPTCanHoldConfidentialBalance uint32 = 0x00000080 ) -// Ledger-state mutable flags for MPTokenIssuance (Lsmf prefix). +// Ledger-state mutable flags for MPTokenIssuance (Lsmf prefix) record future permissions and an opt-out. +// CanEnable permissions allow only a later transition from disabled to enabled; they never +// permit disabling. CanMutate permissions allow repeated metadata or transfer-fee updates. +// CannotEnableCanHoldConfidentialBalance permanently records the confidential opt-out. const ( - // LsmfMPTCanMutateCanLock indicates the CanLock property can be mutated. - LsmfMPTCanMutateCanLock uint32 = 0x00000002 - // LsmfMPTCanMutateRequireAuth indicates the RequireAuth property can be mutated. - LsmfMPTCanMutateRequireAuth uint32 = 0x00000004 - // LsmfMPTCanMutateCanEscrow indicates the CanEscrow property can be mutated. - LsmfMPTCanMutateCanEscrow uint32 = 0x00000008 - // LsmfMPTCanMutateCanTrade indicates the CanTrade property can be mutated. - LsmfMPTCanMutateCanTrade uint32 = 0x00000010 - // LsmfMPTCanMutateCanTransfer indicates the CanTransfer property can be mutated. - LsmfMPTCanMutateCanTransfer uint32 = 0x00000020 - // LsmfMPTCanMutateCanClawback indicates the CanClawback property can be mutated. - LsmfMPTCanMutateCanClawback uint32 = 0x00000040 - // LsmfMPTCanMutateMetadata indicates the MPTokenMetadata can be mutated. + // LsmfMPTCanEnableCanLock records one-way permission to enable CanLock after issuance. + LsmfMPTCanEnableCanLock uint32 = 0x00000002 + // LsmfMPTCanEnableRequireAuth records one-way permission to enable RequireAuth after issuance. + LsmfMPTCanEnableRequireAuth uint32 = 0x00000004 + // LsmfMPTCanEnableCanEscrow records one-way permission to enable CanEscrow after issuance. + LsmfMPTCanEnableCanEscrow uint32 = 0x00000008 + // LsmfMPTCanEnableCanTrade records one-way permission to enable CanTrade after issuance. + LsmfMPTCanEnableCanTrade uint32 = 0x00000010 + // LsmfMPTCanEnableCanTransfer records one-way permission to enable CanTransfer after issuance. + LsmfMPTCanEnableCanTransfer uint32 = 0x00000020 + // LsmfMPTCanEnableCanClawback records one-way permission to enable CanClawback after issuance. + LsmfMPTCanEnableCanClawback uint32 = 0x00000040 + // LsmfMPTCannotEnableCanHoldConfidentialBalance records a permanent opt-out from enabling confidential balances. + LsmfMPTCannotEnableCanHoldConfidentialBalance uint32 = 0x00000080 + // LsmfMPTCanMutateMetadata records repeatable permission to replace MPTokenMetadata. LsmfMPTCanMutateMetadata uint32 = 0x00010000 - // LsmfMPTCanMutateTransferFee indicates the TransferFee can be mutated. + // LsmfMPTCanMutateTransferFee records repeatable permission to replace TransferFee. LsmfMPTCanMutateTransferFee uint32 = 0x00020000 - // LsmfMPTCannotMutateCanConfidentialAmount if set, the lsfMPTCanConfidentialAmount flag can never be changed after the token is issued. - LsmfMPTCannotMutateCanConfidentialAmount uint32 = 0x00040000 + + // MPTokenIssuanceMutableFlagsMask contains every valid ledger MutableFlags bit. + // Bit 0x01 is reserved. + MPTokenIssuanceMutableFlagsMask uint32 = 0x000300FE ) // An MPTokenIssuance entry represents a single MPT issuance and holds data associated with the issuance itself. @@ -87,7 +94,9 @@ type MPTokenIssuance struct { LockedAmount uint64 `json:",omitempty"` // DomainID is the ledger entry ID of a permissioned domain that grants access to the MPT. DomainID string `json:",omitempty"` - // MutableFlags indicates which properties of this MPT can be mutated after creation. + // MutableFlags records one-way future-enable permissions for ordinary capabilities, + // repeatable mutation permission for metadata and transfer fees, and a permanent + // opt-out from enabling confidential balances. MutableFlags uint32 `json:",omitempty"` // The issuer's encryption key for confidential transfers. // Required if confidential transfers are enabled. @@ -139,3 +148,57 @@ func (c *MPTokenIssuance) SetLsfMPTCanTransfer() { func (c *MPTokenIssuance) SetLsfMPTCanClawback() { c.Flags |= LsfMPTCanClawback } + +// SetLsfMPTCanHoldConfidentialBalance sets the confidential balance capability. +func (c *MPTokenIssuance) SetLsfMPTCanHoldConfidentialBalance() { + c.Flags |= LsfMPTCanHoldConfidentialBalance +} + +func (c *MPTokenIssuance) setMutableFlag(flag uint32) { + c.MutableFlags |= flag +} + +// SetLsmfMPTCanEnableCanLock records one-way permission to enable CanLock after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableCanLock() { + c.setMutableFlag(LsmfMPTCanEnableCanLock) +} + +// SetLsmfMPTCanEnableRequireAuth records one-way permission to enable RequireAuth after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableRequireAuth() { + c.setMutableFlag(LsmfMPTCanEnableRequireAuth) +} + +// SetLsmfMPTCanEnableCanEscrow records one-way permission to enable CanEscrow after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableCanEscrow() { + c.setMutableFlag(LsmfMPTCanEnableCanEscrow) +} + +// SetLsmfMPTCanEnableCanTrade records one-way permission to enable CanTrade after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableCanTrade() { + c.setMutableFlag(LsmfMPTCanEnableCanTrade) +} + +// SetLsmfMPTCanEnableCanTransfer records one-way permission to enable CanTransfer after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableCanTransfer() { + c.setMutableFlag(LsmfMPTCanEnableCanTransfer) +} + +// SetLsmfMPTCanEnableCanClawback records one-way permission to enable CanClawback after issuance. +func (c *MPTokenIssuance) SetLsmfMPTCanEnableCanClawback() { + c.setMutableFlag(LsmfMPTCanEnableCanClawback) +} + +// SetLsmfMPTCannotEnableCanHoldConfidentialBalance records a permanent opt-out from enabling confidential balances. +func (c *MPTokenIssuance) SetLsmfMPTCannotEnableCanHoldConfidentialBalance() { + c.setMutableFlag(LsmfMPTCannotEnableCanHoldConfidentialBalance) +} + +// SetLsmfMPTCanMutateMetadata records repeatable permission to replace MPTokenMetadata. +func (c *MPTokenIssuance) SetLsmfMPTCanMutateMetadata() { + c.setMutableFlag(LsmfMPTCanMutateMetadata) +} + +// SetLsmfMPTCanMutateTransferFee records repeatable permission to replace TransferFee. +func (c *MPTokenIssuance) SetLsmfMPTCanMutateTransferFee() { + c.setMutableFlag(LsmfMPTCanMutateTransferFee) +} diff --git a/xrpl/ledger-entry-types/mptoken_issuance_test.go b/xrpl/ledger-entry-types/mptoken_issuance_test.go index 81b07ed4..ae94495a 100644 --- a/xrpl/ledger-entry-types/mptoken_issuance_test.go +++ b/xrpl/ledger-entry-types/mptoken_issuance_test.go @@ -344,7 +344,7 @@ func TestMPTokenIssuanceSerialization(t *testing.T) { PreviousTxnID: types.Hash256("8089451B193AAD110ACED3D62BE79BB523658545E6EE8B7BB0BE573FED9BCBFB"), PreviousTxnLgrSeq: 234644, Sequence: 1, - MutableFlags: LsmfMPTCanMutateCanLock | LsmfMPTCanMutateMetadata, + MutableFlags: LsmfMPTCanEnableCanLock | LsmfMPTCanMutateMetadata, }, expected: `{ "index": "A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9", @@ -364,11 +364,11 @@ func TestMPTokenIssuanceSerialization(t *testing.T) { }`, }, { - name: "pass - valid MPTokenIssuance with LsfMPTCanConfidentialAmount", + name: "pass - valid MPTokenIssuance with LsfMPTCanHoldConfidentialBalance", mpTokenIssuance: &MPTokenIssuance{ Index: types.Hash256("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9"), LedgerEntryType: MPTokenIssuanceEntry, - Flags: LsfMPTCanConfidentialAmount, + Flags: LsfMPTCanHoldConfidentialBalance, Issuer: types.Address("rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD"), AssetScale: 2, MaximumAmount: 1000, @@ -401,7 +401,7 @@ func TestMPTokenIssuanceSerialization(t *testing.T) { mpTokenIssuance: &MPTokenIssuance{ Index: types.Hash256("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9"), LedgerEntryType: MPTokenIssuanceEntry, - Flags: LsfMPTCanConfidentialAmount | LsfMPTCanTransfer, + Flags: LsfMPTCanHoldConfidentialBalance | LsfMPTCanTransfer, Issuer: types.Address("rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD"), AssetScale: 2, MaximumAmount: 1000, @@ -445,3 +445,70 @@ func TestMPTokenIssuanceSerialization(t *testing.T) { }) } } + +func TestMPTokenIssuanceFlagValuesAndSettersPreserveBits(t *testing.T) { + require.Equal(t, uint32(0x02), LsfMPTCanLock) + require.Equal(t, uint32(0x04), LsfMPTRequireAuth) + require.Equal(t, uint32(0x08), LsfMPTCanEscrow) + require.Equal(t, uint32(0x10), LsfMPTCanTrade) + require.Equal(t, uint32(0x20), LsfMPTCanTransfer) + require.Equal(t, uint32(0x40), LsfMPTCanClawback) + require.Equal(t, uint32(0x80), LsfMPTCanHoldConfidentialBalance) + + tests := []struct { + name string + set func(*MPTokenIssuance) + literal uint32 + }{ + {"CanLock", (*MPTokenIssuance).SetLsfMPTCanLock, 0x02}, + {"RequireAuth", (*MPTokenIssuance).SetLsfMPTRequireAuth, 0x04}, + {"CanEscrow", (*MPTokenIssuance).SetLsfMPTCanEscrow, 0x08}, + {"CanTrade", (*MPTokenIssuance).SetLsfMPTCanTrade, 0x10}, + {"CanTransfer", (*MPTokenIssuance).SetLsfMPTCanTransfer, 0x20}, + {"CanClawback", (*MPTokenIssuance).SetLsfMPTCanClawback, 0x40}, + {"CanHoldConfidentialBalance", (*MPTokenIssuance).SetLsfMPTCanHoldConfidentialBalance, 0x80}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + issuance := &MPTokenIssuance{Flags: 0x100} + test.set(issuance) + require.Equal(t, uint32(0x100)|test.literal, issuance.Flags) + }) + } +} + +func TestMPTokenIssuanceMutableFlagValuesAndSettersPreserveBits(t *testing.T) { + require.Equal(t, uint32(0x02), LsmfMPTCanEnableCanLock) + require.Equal(t, uint32(0x04), LsmfMPTCanEnableRequireAuth) + require.Equal(t, uint32(0x08), LsmfMPTCanEnableCanEscrow) + require.Equal(t, uint32(0x10), LsmfMPTCanEnableCanTrade) + require.Equal(t, uint32(0x20), LsmfMPTCanEnableCanTransfer) + require.Equal(t, uint32(0x40), LsmfMPTCanEnableCanClawback) + require.Equal(t, uint32(0x80), LsmfMPTCannotEnableCanHoldConfidentialBalance) + require.Equal(t, uint32(0x10000), LsmfMPTCanMutateMetadata) + require.Equal(t, uint32(0x20000), LsmfMPTCanMutateTransferFee) + require.Equal(t, uint32(0x000300FE), MPTokenIssuanceMutableFlagsMask) + + tests := []struct { + name string + set func(*MPTokenIssuance) + literal uint32 + }{ + {"CanEnableCanLock", (*MPTokenIssuance).SetLsmfMPTCanEnableCanLock, 0x02}, + {"CanEnableRequireAuth", (*MPTokenIssuance).SetLsmfMPTCanEnableRequireAuth, 0x04}, + {"CanEnableCanEscrow", (*MPTokenIssuance).SetLsmfMPTCanEnableCanEscrow, 0x08}, + {"CanEnableCanTrade", (*MPTokenIssuance).SetLsmfMPTCanEnableCanTrade, 0x10}, + {"CanEnableCanTransfer", (*MPTokenIssuance).SetLsmfMPTCanEnableCanTransfer, 0x20}, + {"CanEnableCanClawback", (*MPTokenIssuance).SetLsmfMPTCanEnableCanClawback, 0x40}, + {"CannotEnableCanHoldConfidentialBalance", (*MPTokenIssuance).SetLsmfMPTCannotEnableCanHoldConfidentialBalance, 0x80}, + {"CanMutateMetadata", (*MPTokenIssuance).SetLsmfMPTCanMutateMetadata, 0x10000}, + {"CanMutateTransferFee", (*MPTokenIssuance).SetLsmfMPTCanMutateTransferFee, 0x20000}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + issuance := &MPTokenIssuance{MutableFlags: 0x80000000} + test.set(issuance) + require.Equal(t, uint32(0x80000000)|test.literal, issuance.MutableFlags) + }) + } +} diff --git a/xrpl/transaction/errors.go b/xrpl/transaction/errors.go index dd0a6913..44afe620 100644 --- a/xrpl/transaction/errors.go +++ b/xrpl/transaction/errors.go @@ -220,8 +220,10 @@ var ( // ErrHolderAccountConflict is returned when the holder account is the same as the issuing account. ErrHolderAccountConflict = errors.New("holder must be different from the account") - // ErrMPTIssuanceCreateMutableFlagsZero is returned when MutableFlags is set to zero in MPTokenIssuanceCreate. - ErrMPTIssuanceCreateMutableFlagsZero = errors.New("mptoken issuance create: MutableFlags cannot be zero") + // ErrMPTIssuanceCreateInvalidMutableFlags is returned when MutableFlags is zero or contains an unsupported bit. + ErrMPTIssuanceCreateInvalidMutableFlags = errors.New("mptoken issuance create: MutableFlags must contain only supported non-zero flags") + // ErrMPTIssuanceCreateTransferFeeWithConfidentialBalance is returned when a non-zero TransferFee is used with confidential balances. + ErrMPTIssuanceCreateTransferFeeWithConfidentialBalance = errors.New("mptoken issuance create: non-zero TransferFee cannot be set with TfMPTCanHoldConfidentialBalance") // ErrMPTIssuanceCreateDomainIDInvalid is returned when DomainID is not a valid 64-character hexadecimal string. ErrMPTIssuanceCreateDomainIDInvalid = errors.New("mptoken issuance create: DomainID must be a valid 64-character hexadecimal string") // ErrMPTIssuanceCreateDomainIDRequiresRequireAuth is returned when DomainID is set without enabling TfMPTRequireAuth flag. @@ -232,20 +234,16 @@ var ( ErrMPTIssuanceSetHolderMutuallyExclusive = errors.New("mptoken issuance set: Holder is mutually exclusive with MutableFlags/MPTokenMetadata/TransferFee/DomainID") // ErrMPTIssuanceSetFlagsMutuallyExclusive is returned when non-zero Flags are set together with DynamicMPT fields. ErrMPTIssuanceSetFlagsMutuallyExclusive = errors.New("mptoken issuance set: Flags is mutually exclusive with MutableFlags/MPTokenMetadata/TransferFee") - // ErrMPTIssuanceSetMutableFlagsZero is returned when MutableFlags is set to zero. - ErrMPTIssuanceSetMutableFlagsZero = errors.New("mptoken issuance set: MutableFlags cannot be zero") - // ErrMPTIssuanceSetMutableFlagsConflict is returned when MutableFlags has both set and clear for the same flag. - ErrMPTIssuanceSetMutableFlagsConflict = errors.New("mptoken issuance set: cannot set and clear the same flag simultaneously") - // ErrMPTIssuanceSetTransferFeeWithClearCanTransfer is returned when a non-zero TransferFee is set together with tmfMPTClearCanTransfer. - ErrMPTIssuanceSetTransferFeeWithClearCanTransfer = errors.New("mptoken issuance set: non-zero TransferFee cannot be set together with tmfMPTClearCanTransfer") + // ErrMPTIssuanceSetInvalidMutableFlags is returned when MutableFlags is zero or contains an unsupported bit. + ErrMPTIssuanceSetInvalidMutableFlags = errors.New("mptoken issuance set: MutableFlags must contain only supported non-zero flags") + // ErrMPTIssuanceSetTransferFeeWithConfidentialBalance is returned when a non-zero TransferFee enables confidential balances. + ErrMPTIssuanceSetTransferFeeWithConfidentialBalance = errors.New("mptoken issuance set: non-zero TransferFee cannot be set with TmfMPTSetCanHoldConfidentialBalance") // ErrMPTIssuanceSetDomainIDInvalid is returned when DomainID is not a valid 64-character hexadecimal string (and not empty). ErrMPTIssuanceSetDomainIDInvalid = errors.New("mptoken issuance set: DomainID must be a valid 64-character hexadecimal string or empty") // ErrMPTIssuanceSetKeyConflict is returned when encryption keys are set together with Holder. ErrMPTIssuanceSetKeyConflict = errors.New("mptoken issuance set: encryption keys cannot be set together with Holder") // ErrMPTIssuanceSetAuditorRequiresIssuerKey is returned when AuditorEncryptionKey is set without IssuerEncryptionKey. ErrMPTIssuanceSetAuditorRequiresIssuerKey = errors.New("mptoken issuance set: AuditorEncryptionKey requires IssuerEncryptionKey to be set") - // ErrMPTIssuanceSetKeysWithClearCanConfidentialAmount is returned when encryption keys are set together with tmfMPTClearCanConfidentialAmount. - ErrMPTIssuanceSetKeysWithClearCanConfidentialAmount = errors.New("mptoken issuance set: encryption keys cannot be set together with tmfMPTClearCanConfidentialAmount") // ErrMPTIssuanceSetInvalidKeyLength is returned when an encryption key has an invalid length. ErrMPTIssuanceSetInvalidKeyLength = errors.New("mptoken issuance set: encryption key must be 66 hex characters (33-byte compressed EC point)") diff --git a/xrpl/transaction/mptoken_issuance_create.go b/xrpl/transaction/mptoken_issuance_create.go index 285afe11..cc5cf893 100644 --- a/xrpl/transaction/mptoken_issuance_create.go +++ b/xrpl/transaction/mptoken_issuance_create.go @@ -20,31 +20,35 @@ const ( TfMPTCanTransfer uint32 = 0x00000020 // TfMPTCanClawback if set, indicates that the issuer may use the Clawback transaction to claw back value from individual holders. TfMPTCanClawback uint32 = 0x00000040 - // TfMPTCanConfidentialAmount if set, indicates that confidential transfers are enabled for this token issuance. - TfMPTCanConfidentialAmount uint32 = 0x00000080 + // TfMPTCanHoldConfidentialBalance if set, indicates that confidential transfers are enabled for this token issuance. + TfMPTCanHoldConfidentialBalance uint32 = 0x00000080 ) -// MutableFlags constants for MPTokenIssuanceCreate. -// These declare which properties can be mutated after creation. +// MutableFlags constants for MPTokenIssuanceCreate declare future permissions and an opt-out. +// CanEnable permissions allow only a later transition from disabled to enabled; they never +// permit disabling. CanMutate permissions allow repeated metadata or transfer-fee updates. +// CannotEnableCanHoldConfidentialBalance permanently declines the confidential capability. const ( - // TmfMPTCanMutateCanLock allows the CanLock property to be changed after creation. - TmfMPTCanMutateCanLock uint32 = 0x00000002 - // TmfMPTCanMutateRequireAuth allows the RequireAuth property to be changed after creation. - TmfMPTCanMutateRequireAuth uint32 = 0x00000004 - // TmfMPTCanMutateCanEscrow allows the CanEscrow property to be changed after creation. - TmfMPTCanMutateCanEscrow uint32 = 0x00000008 - // TmfMPTCanMutateCanTrade allows the CanTrade property to be changed after creation. - TmfMPTCanMutateCanTrade uint32 = 0x00000010 - // TmfMPTCanMutateCanTransfer allows the CanTransfer property to be changed after creation. - TmfMPTCanMutateCanTransfer uint32 = 0x00000020 - // TmfMPTCanMutateCanClawback allows the CanClawback property to be changed after creation. - TmfMPTCanMutateCanClawback uint32 = 0x00000040 - // TmfMPTCanMutateMetadata allows the MPTokenMetadata to be changed after creation. + // TmfMPTCanEnableCanLock grants one-way permission to enable CanLock after creation. + TmfMPTCanEnableCanLock uint32 = 0x00000002 + // TmfMPTCanEnableRequireAuth grants one-way permission to enable RequireAuth after creation. + TmfMPTCanEnableRequireAuth uint32 = 0x00000004 + // TmfMPTCanEnableCanEscrow grants one-way permission to enable CanEscrow after creation. + TmfMPTCanEnableCanEscrow uint32 = 0x00000008 + // TmfMPTCanEnableCanTrade grants one-way permission to enable CanTrade after creation. + TmfMPTCanEnableCanTrade uint32 = 0x00000010 + // TmfMPTCanEnableCanTransfer grants one-way permission to enable CanTransfer after creation. + TmfMPTCanEnableCanTransfer uint32 = 0x00000020 + // TmfMPTCanEnableCanClawback grants one-way permission to enable CanClawback after creation. + TmfMPTCanEnableCanClawback uint32 = 0x00000040 + // TmfMPTCanMutateMetadata grants repeatable permission to replace MPTokenMetadata after creation. TmfMPTCanMutateMetadata uint32 = 0x00010000 - // TmfMPTCanMutateTransferFee allows the TransferFee to be changed after creation. + // TmfMPTCanMutateTransferFee grants repeatable permission to replace TransferFee after creation. TmfMPTCanMutateTransferFee uint32 = 0x00020000 - // TmfMPTCannotMutateCanConfidentialAmount prevents the CanConfidentialAmount property from being changed after creation. - TmfMPTCannotMutateCanConfidentialAmount uint32 = 0x00040000 + // TmfMPTCannotEnableCanHoldConfidentialBalance permanently opts out of enabling confidential balances after creation. + TmfMPTCannotEnableCanHoldConfidentialBalance uint32 = 0x00000080 + // MPTokenIssuanceCreateMutableFlagsMask contains every supported MutableFlags bit. Bit 0x01 is reserved. + MPTokenIssuanceCreateMutableFlagsMask uint32 = 0x000300FE ) // MPTokenIssuanceCreateMetadata represents the resulting metadata of a succeeded MPTokenIssuanceCreate transaction. @@ -101,7 +105,9 @@ type MPTokenIssuanceCreate struct { // DomainID is the ledger entry ID of a permissioned domain that grants access to the MPT. // Requires the TfMPTRequireAuth flag to be set. DomainID *string `json:",omitempty"` - // MutableFlags declares which properties of this MPT can be mutated after creation. + // MutableFlags grants one-way future-enable permissions for ordinary capabilities, + // repeatable mutation permission for metadata and transfer fees, and a permanent + // opt-out from enabling confidential balances. MutableFlags *uint32 `json:",omitempty"` } @@ -173,9 +179,9 @@ func (m *MPTokenIssuanceCreate) SetMPTCanClawbackFlag() { m.Flags |= TfMPTCanClawback } -// SetMPTCanConfidentialAmountFlag sets the TfMPTCanConfidentialAmount flag to enable confidential transfers for this token issuance. -func (m *MPTokenIssuanceCreate) SetMPTCanConfidentialAmountFlag() { - m.Flags |= TfMPTCanConfidentialAmount +// SetMPTCanHoldConfidentialBalanceFlag sets the TfMPTCanHoldConfidentialBalance flag to enable confidential transfers for this token issuance. +func (m *MPTokenIssuanceCreate) SetMPTCanHoldConfidentialBalanceFlag() { + m.Flags |= TfMPTCanHoldConfidentialBalance } // setMutableFlag is a helper that initialises MutableFlags if nil and applies the given flag. @@ -187,49 +193,49 @@ func (m *MPTokenIssuanceCreate) setMutableFlag(f uint32) { *m.MutableFlags |= f } -// SetMPTCanMutateCanLockFlag allows the CanLock property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateCanLockFlag() { - m.setMutableFlag(TmfMPTCanMutateCanLock) +// SetMPTCanEnableCanLockFlag grants one-way permission to enable CanLock after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableCanLockFlag() { + m.setMutableFlag(TmfMPTCanEnableCanLock) } -// SetMPTCanMutateRequireAuthFlag allows the RequireAuth property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateRequireAuthFlag() { - m.setMutableFlag(TmfMPTCanMutateRequireAuth) +// SetMPTCanEnableRequireAuthFlag grants one-way permission to enable RequireAuth after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableRequireAuthFlag() { + m.setMutableFlag(TmfMPTCanEnableRequireAuth) } -// SetMPTCanMutateCanEscrowFlag allows the CanEscrow property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateCanEscrowFlag() { - m.setMutableFlag(TmfMPTCanMutateCanEscrow) +// SetMPTCanEnableCanEscrowFlag grants one-way permission to enable CanEscrow after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableCanEscrowFlag() { + m.setMutableFlag(TmfMPTCanEnableCanEscrow) } -// SetMPTCanMutateCanTradeFlag allows the CanTrade property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateCanTradeFlag() { - m.setMutableFlag(TmfMPTCanMutateCanTrade) +// SetMPTCanEnableCanTradeFlag grants one-way permission to enable CanTrade after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableCanTradeFlag() { + m.setMutableFlag(TmfMPTCanEnableCanTrade) } -// SetMPTCanMutateCanTransferFlag allows the CanTransfer property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateCanTransferFlag() { - m.setMutableFlag(TmfMPTCanMutateCanTransfer) +// SetMPTCanEnableCanTransferFlag grants one-way permission to enable CanTransfer after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableCanTransferFlag() { + m.setMutableFlag(TmfMPTCanEnableCanTransfer) } -// SetMPTCanMutateCanClawbackFlag allows the CanClawback property to be changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCanMutateCanClawbackFlag() { - m.setMutableFlag(TmfMPTCanMutateCanClawback) +// SetMPTCanEnableCanClawbackFlag grants one-way permission to enable CanClawback after creation. +func (m *MPTokenIssuanceCreate) SetMPTCanEnableCanClawbackFlag() { + m.setMutableFlag(TmfMPTCanEnableCanClawback) } -// SetMPTCanMutateMetadataFlag allows the MPTokenMetadata to be changed after creation. +// SetMPTCanMutateMetadataFlag grants repeatable permission to replace MPTokenMetadata after creation. func (m *MPTokenIssuanceCreate) SetMPTCanMutateMetadataFlag() { m.setMutableFlag(TmfMPTCanMutateMetadata) } -// SetMPTCanMutateTransferFeeFlag allows the TransferFee to be changed after creation. +// SetMPTCanMutateTransferFeeFlag grants repeatable permission to replace TransferFee after creation. func (m *MPTokenIssuanceCreate) SetMPTCanMutateTransferFeeFlag() { m.setMutableFlag(TmfMPTCanMutateTransferFee) } -// SetMPTCannotMutateCanConfidentialAmountFlag prevents the CanConfidentialAmount property from being changed after creation. -func (m *MPTokenIssuanceCreate) SetMPTCannotMutateCanConfidentialAmountFlag() { - m.setMutableFlag(TmfMPTCannotMutateCanConfidentialAmount) +// SetMPTCannotEnableCanHoldConfidentialBalanceFlag permanently opts out of enabling confidential balances after creation. +func (m *MPTokenIssuanceCreate) SetMPTCannotEnableCanHoldConfidentialBalanceFlag() { + m.setMutableFlag(TmfMPTCannotEnableCanHoldConfidentialBalance) } // Validate validates the MPTokenIssuanceCreate transaction ensuring all fields are correct. @@ -239,6 +245,10 @@ func (m *MPTokenIssuanceCreate) Validate() (bool, error) { return false, err } + if m.MutableFlags != nil && (*m.MutableFlags == 0 || *m.MutableFlags&^MPTokenIssuanceCreateMutableFlagsMask != 0) { + return false, ErrMPTIssuanceCreateInvalidMutableFlags + } + // Validate TransferFee: must not exceed MAX_TRANSFER_FEE and requires TfMPTCanTransfer flag. if m.TransferFee != nil && *m.TransferFee > 0 { if *m.TransferFee > MaxTransferFee { @@ -247,6 +257,9 @@ func (m *MPTokenIssuanceCreate) Validate() (bool, error) { if !flag.Contains(m.Flags, TfMPTCanTransfer) { return false, ErrTransferFeeRequiresCanTransfer } + if flag.Contains(m.Flags, TfMPTCanHoldConfidentialBalance) { + return false, ErrMPTIssuanceCreateTransferFeeWithConfidentialBalance + } } if m.MaximumAmount != nil { @@ -271,10 +284,5 @@ func (m *MPTokenIssuanceCreate) Validate() (bool, error) { } } - // MutableFlags cannot be zero when present. - if m.MutableFlags != nil && *m.MutableFlags == 0 { - return false, ErrMPTIssuanceCreateMutableFlagsZero - } - return true, nil } diff --git a/xrpl/transaction/mptoken_issuance_create_test.go b/xrpl/transaction/mptoken_issuance_create_test.go index a3468a7e..c1d2b862 100644 --- a/xrpl/transaction/mptoken_issuance_create_test.go +++ b/xrpl/transaction/mptoken_issuance_create_test.go @@ -62,7 +62,7 @@ func TestMPTokenIssuanceCreate_Flatten(t *testing.T) { BaseTx: BaseTx{ Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", }, - MutableFlags: types.MutableFlags(TmfMPTCanMutateCanLock | TmfMPTCanMutateMetadata), + MutableFlags: types.MutableFlags(TmfMPTCanEnableCanLock | TmfMPTCanMutateMetadata), }, expected: `{ "Account": "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", @@ -206,7 +206,7 @@ func TestMPTokenIssuanceCreate_Validate(t *testing.T) { Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", TransactionType: MPTokenIssuanceCreateTx, }, - MutableFlags: types.MutableFlags(TmfMPTCanMutateCanLock | TmfMPTCanMutateMetadata), + MutableFlags: types.MutableFlags(TmfMPTCanEnableCanLock | TmfMPTCanMutateMetadata), }, wantValid: true, wantErr: false, @@ -222,7 +222,7 @@ func TestMPTokenIssuanceCreate_Validate(t *testing.T) { }, wantValid: false, wantErr: true, - errMessage: ErrMPTIssuanceCreateMutableFlagsZero, + errMessage: ErrMPTIssuanceCreateInvalidMutableFlags, }, { name: "pass - valid with DomainID and TfMPTRequireAuth", @@ -295,151 +295,144 @@ func TestMPTokenIssuanceCreate_Validate(t *testing.T) { } } -func TestMPTokenIssuanceCreate_Flags(t *testing.T) { +func TestMPTokenIssuanceCreateFlagValuesAndSettersPreserveBits(t *testing.T) { + require.Equal(t, uint32(0x02), TfMPTCanLock) + require.Equal(t, uint32(0x04), TfMPTRequireAuth) + require.Equal(t, uint32(0x08), TfMPTCanEscrow) + require.Equal(t, uint32(0x10), TfMPTCanTrade) + require.Equal(t, uint32(0x20), TfMPTCanTransfer) + require.Equal(t, uint32(0x40), TfMPTCanClawback) + require.Equal(t, uint32(0x80), TfMPTCanHoldConfidentialBalance) + tests := []struct { - name string - setFlag func(*MPTokenIssuanceCreate) - flagMask uint32 + name string + setFlag func(*MPTokenIssuanceCreate) + literal uint32 }{ - { - name: "MPTCanLock", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanLockFlag, - flagMask: TfMPTCanLock, - }, - { - name: "MPTRequireAuth", - setFlag: (*MPTokenIssuanceCreate).SetMPTRequireAuthFlag, - flagMask: TfMPTRequireAuth, - }, - { - name: "MPTCanEscrow", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanEscrowFlag, - flagMask: TfMPTCanEscrow, - }, - { - name: "MPTCanTrade", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanTradeFlag, - flagMask: TfMPTCanTrade, - }, - { - name: "MPTCanTransfer", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanTransferFlag, - flagMask: TfMPTCanTransfer, - }, - { - name: "MPTCanClawback", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanClawbackFlag, - flagMask: TfMPTCanClawback, - }, - { - name: "MPTCanConfidentialAmount", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanConfidentialAmountFlag, - flagMask: TfMPTCanConfidentialAmount, - }, + {"MPTCanLock", (*MPTokenIssuanceCreate).SetMPTCanLockFlag, 0x02}, + {"MPTRequireAuth", (*MPTokenIssuanceCreate).SetMPTRequireAuthFlag, 0x04}, + {"MPTCanEscrow", (*MPTokenIssuanceCreate).SetMPTCanEscrowFlag, 0x08}, + {"MPTCanTrade", (*MPTokenIssuanceCreate).SetMPTCanTradeFlag, 0x10}, + {"MPTCanTransfer", (*MPTokenIssuanceCreate).SetMPTCanTransferFlag, 0x20}, + {"MPTCanClawback", (*MPTokenIssuanceCreate).SetMPTCanClawbackFlag, 0x40}, + {"MPTCanHoldConfidentialBalance", (*MPTokenIssuanceCreate).SetMPTCanHoldConfidentialBalanceFlag, 0x80}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx := &MPTokenIssuanceCreate{ - BaseTx: BaseTx{ - Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", - TransactionType: MPTokenIssuanceCreateTx, - }, - MPTokenMetadata: types.MPTokenMetadata("464f4f"), - } - - tt.setFlag(tx) - require.Equal(t, uint32(tt.flagMask), tx.Flags&tt.flagMask) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceCreate{BaseTx: BaseTx{Flags: 0x100}} + test.setFlag(tx) + require.Equal(t, uint32(0x100)|test.literal, tx.Flags) }) } +} - // Test all flags together - tx := &MPTokenIssuanceCreate{ - BaseTx: BaseTx{ - Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", - TransactionType: MPTokenIssuanceCreateTx, - }, - MPTokenMetadata: types.MPTokenMetadata("464f4f"), +func TestMPTokenIssuanceCreateMutableFlagSettersPreserveBits(t *testing.T) { + tests := []struct { + name string + setFlag func(*MPTokenIssuanceCreate) + literal uint32 + }{ + {"MPTCanEnableCanLock", (*MPTokenIssuanceCreate).SetMPTCanEnableCanLockFlag, 0x02}, + {"MPTCanEnableRequireAuth", (*MPTokenIssuanceCreate).SetMPTCanEnableRequireAuthFlag, 0x04}, + {"MPTCanEnableCanEscrow", (*MPTokenIssuanceCreate).SetMPTCanEnableCanEscrowFlag, 0x08}, + {"MPTCanEnableCanTrade", (*MPTokenIssuanceCreate).SetMPTCanEnableCanTradeFlag, 0x10}, + {"MPTCanEnableCanTransfer", (*MPTokenIssuanceCreate).SetMPTCanEnableCanTransferFlag, 0x20}, + {"MPTCanEnableCanClawback", (*MPTokenIssuanceCreate).SetMPTCanEnableCanClawbackFlag, 0x40}, + {"MPTCanMutateMetadata", (*MPTokenIssuanceCreate).SetMPTCanMutateMetadataFlag, 0x10000}, + {"MPTCanMutateTransferFee", (*MPTokenIssuanceCreate).SetMPTCanMutateTransferFeeFlag, 0x20000}, + {"MPTCannotEnableCanHoldConfidentialBalance", (*MPTokenIssuanceCreate).SetMPTCannotEnableCanHoldConfidentialBalanceFlag, 0x80}, } - for _, tt := range tests { - tt.setFlag(tx) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceCreate{MutableFlags: types.MutableFlags(0x80000000)} + test.setFlag(tx) + require.Equal(t, uint32(0x80000000)|test.literal, *tx.MutableFlags) + }) } +} - expectedFlags := TfMPTCanLock | TfMPTRequireAuth | TfMPTCanEscrow | TfMPTCanTrade | TfMPTCanTransfer | TfMPTCanClawback | TfMPTCanConfidentialAmount - require.Equal(t, uint32(expectedFlags), tx.Flags) +func TestMPTokenIssuanceCreateMutableFlagValuesAndMask(t *testing.T) { + require.Equal(t, uint32(0x02), TmfMPTCanEnableCanLock) + require.Equal(t, uint32(0x04), TmfMPTCanEnableRequireAuth) + require.Equal(t, uint32(0x08), TmfMPTCanEnableCanEscrow) + require.Equal(t, uint32(0x10), TmfMPTCanEnableCanTrade) + require.Equal(t, uint32(0x20), TmfMPTCanEnableCanTransfer) + require.Equal(t, uint32(0x40), TmfMPTCanEnableCanClawback) + require.Equal(t, uint32(0x80), TmfMPTCannotEnableCanHoldConfidentialBalance) + require.Equal(t, uint32(0x10000), TmfMPTCanMutateMetadata) + require.Equal(t, uint32(0x20000), TmfMPTCanMutateTransferFee) + require.Equal(t, uint32(0x000300FE), MPTokenIssuanceCreateMutableFlagsMask) } -func TestMPTokenIssuanceCreate_MutableFlags(t *testing.T) { +func TestMPTokenIssuanceCreate_ValidateMutableFlagsMask(t *testing.T) { tests := []struct { - name string - setFlag func(*MPTokenIssuanceCreate) - flagMask uint32 + name string + flags uint32 + ok bool }{ - { - name: "MPTCanMutateCanLock", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateCanLockFlag, - flagMask: TmfMPTCanMutateCanLock, - }, - { - name: "MPTCanMutateRequireAuth", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateRequireAuthFlag, - flagMask: TmfMPTCanMutateRequireAuth, - }, - { - name: "MPTCanMutateCanEscrow", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateCanEscrowFlag, - flagMask: TmfMPTCanMutateCanEscrow, - }, - { - name: "MPTCanMutateCanTrade", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateCanTradeFlag, - flagMask: TmfMPTCanMutateCanTrade, - }, - { - name: "MPTCanMutateCanTransfer", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateCanTransferFlag, - flagMask: TmfMPTCanMutateCanTransfer, - }, - { - name: "MPTCanMutateCanClawback", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateCanClawbackFlag, - flagMask: TmfMPTCanMutateCanClawback, - }, - { - name: "MPTCanMutateMetadata", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateMetadataFlag, - flagMask: TmfMPTCanMutateMetadata, - }, - { - name: "MPTCanMutateTransferFee", - setFlag: (*MPTokenIssuanceCreate).SetMPTCanMutateTransferFeeFlag, - flagMask: TmfMPTCanMutateTransferFee, - }, - { - name: "MPTCannotMutateCanConfidentialAmount", - setFlag: (*MPTokenIssuanceCreate).SetMPTCannotMutateCanConfidentialAmountFlag, - flagMask: TmfMPTCannotMutateCanConfidentialAmount, - }, + {"one valid bit", TmfMPTCanEnableCanLock, true}, + {"all valid bits", MPTokenIssuanceCreateMutableFlagsMask, true}, + {"present zero", 0, false}, + {"reserved bit 0x01", 0x01, false}, + {"removed bit 0x40000", 0x40000, false}, + {"unknown high bit", 0x80000000, false}, + {"valid and unknown", TmfMPTCanMutateMetadata | 0x100, false}, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx := &MPTokenIssuanceCreate{} - tt.setFlag(tx) - require.NotNil(t, tx.MutableFlags) - require.Equal(t, tt.flagMask, *tx.MutableFlags) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceCreate{ + BaseTx: BaseTx{Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", TransactionType: MPTokenIssuanceCreateTx}, + MutableFlags: types.MutableFlags(test.flags), + } + ok, err := tx.Validate() + require.Equal(t, test.ok, ok) + if test.ok { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, ErrMPTIssuanceCreateInvalidMutableFlags) + } }) } +} - // Test all mutable flags together - tx := &MPTokenIssuanceCreate{} - for _, tt := range tests { - tt.setFlag(tx) +func TestMPTokenIssuanceCreate_TransferFeeConfidentialBalance(t *testing.T) { + tests := []struct { + name string + fee *uint16 + flags uint32 + ok bool + }{ + {"non-zero fee with confidential", types.TransferFee(1), TfMPTCanTransfer | TfMPTCanHoldConfidentialBalance, false}, + {"zero fee with confidential", types.TransferFee(0), TfMPTCanHoldConfidentialBalance, true}, + {"absent fee with confidential", nil, TfMPTCanHoldConfidentialBalance, true}, + {"non-zero fee transferable", types.TransferFee(1), TfMPTCanTransfer, true}, } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceCreate{ + BaseTx: BaseTx{Account: "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", TransactionType: MPTokenIssuanceCreateTx, Flags: test.flags}, + TransferFee: test.fee, + } + ok, err := tx.Validate() + require.Equal(t, test.ok, ok) + if test.ok { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, ErrMPTIssuanceCreateTransferFeeWithConfidentialBalance) + } + }) + } +} - expectedMutableFlags := TmfMPTCanMutateCanLock | TmfMPTCanMutateRequireAuth | TmfMPTCanMutateCanEscrow | - TmfMPTCanMutateCanTrade | TmfMPTCanMutateCanTransfer | TmfMPTCanMutateCanClawback | - TmfMPTCanMutateMetadata | TmfMPTCanMutateTransferFee | TmfMPTCannotMutateCanConfidentialAmount - require.Equal(t, expectedMutableFlags, *tx.MutableFlags) +func TestMPTokenIssuanceCreate_SettersPreserveExistingBits(t *testing.T) { + tx := &MPTokenIssuanceCreate{ + BaseTx: BaseTx{Flags: 0x100}, + MutableFlags: types.MutableFlags(0x80000000), + } + tx.SetMPTCanHoldConfidentialBalanceFlag() + tx.SetMPTCanEnableCanLockFlag() + require.Equal(t, uint32(0x180), tx.Flags) + require.Equal(t, uint32(0x80000002), *tx.MutableFlags) } diff --git a/xrpl/transaction/mptoken_issuance_set.go b/xrpl/transaction/mptoken_issuance_set.go index 8aaedcc9..1f275712 100644 --- a/xrpl/transaction/mptoken_issuance_set.go +++ b/xrpl/transaction/mptoken_issuance_set.go @@ -2,7 +2,6 @@ package transaction import ( addresscodec "github.com/Peersyst/xrpl-go/address-codec" - "github.com/Peersyst/xrpl-go/pkg/typecheck" "github.com/Peersyst/xrpl-go/xrpl/flag" "github.com/Peersyst/xrpl-go/xrpl/transaction/types" ) @@ -15,36 +14,24 @@ const ( TfMPTUnlock uint32 = 0x00000002 ) -// MutableFlags constants for MPTokenIssuanceSet (Set/Clear pairs). +// MutableFlags constants for MPTokenIssuanceSet. Each operation enables a capability and is one-way. const ( - // TmfMPTSetCanLock sets the CanLock flag. + // TmfMPTSetCanLock enables CanLock. TmfMPTSetCanLock uint32 = 0x00000001 - // TmfMPTClearCanLock clears the CanLock flag. - TmfMPTClearCanLock uint32 = 0x00000002 - // TmfMPTSetRequireAuth sets the RequireAuth flag. - TmfMPTSetRequireAuth uint32 = 0x00000004 - // TmfMPTClearRequireAuth clears the RequireAuth flag. - TmfMPTClearRequireAuth uint32 = 0x00000008 - // TmfMPTSetCanEscrow sets the CanEscrow flag. - TmfMPTSetCanEscrow uint32 = 0x00000010 - // TmfMPTClearCanEscrow clears the CanEscrow flag. - TmfMPTClearCanEscrow uint32 = 0x00000020 - // TmfMPTSetCanTrade sets the CanTrade flag. - TmfMPTSetCanTrade uint32 = 0x00000040 - // TmfMPTClearCanTrade clears the CanTrade flag. - TmfMPTClearCanTrade uint32 = 0x00000080 - // TmfMPTSetCanTransfer sets the CanTransfer flag. - TmfMPTSetCanTransfer uint32 = 0x00000100 - // TmfMPTClearCanTransfer clears the CanTransfer flag. - TmfMPTClearCanTransfer uint32 = 0x00000200 - // TmfMPTSetCanClawback sets the CanClawback flag. - TmfMPTSetCanClawback uint32 = 0x00000400 - // TmfMPTClearCanClawback clears the CanClawback flag. - TmfMPTClearCanClawback uint32 = 0x00000800 - // TmfMPTSetCanConfidentialAmount sets the CanConfidentialAmount flag. - TmfMPTSetCanConfidentialAmount uint32 = 0x00001000 - // TmfMPTClearCanConfidentialAmount clears the CanConfidentialAmount flag. - TmfMPTClearCanConfidentialAmount uint32 = 0x00002000 + // TmfMPTSetRequireAuth enables RequireAuth. + TmfMPTSetRequireAuth uint32 = 0x00000002 + // TmfMPTSetCanEscrow enables CanEscrow. + TmfMPTSetCanEscrow uint32 = 0x00000004 + // TmfMPTSetCanTrade enables CanTrade. + TmfMPTSetCanTrade uint32 = 0x00000008 + // TmfMPTSetCanTransfer enables CanTransfer. + TmfMPTSetCanTransfer uint32 = 0x00000010 + // TmfMPTSetCanClawback enables CanClawback. + TmfMPTSetCanClawback uint32 = 0x00000020 + // TmfMPTSetCanHoldConfidentialBalance enables confidential balances. + TmfMPTSetCanHoldConfidentialBalance uint32 = 0x00000040 + // MPTokenIssuanceSetMutableFlagsMask contains every supported MutableFlags bit. + MPTokenIssuanceSetMutableFlagsMask uint32 = 0x0000007F ) // MPTokenIssuanceSet transaction is used to globally lock/unlock a MPTokenIssuance, @@ -56,14 +43,14 @@ const ( // { // "TransactionType": "MPTokenIssuanceSet", // "Fee": "10", -// "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", +// "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE2", // "Flags": 1 // } // // ``` type MPTokenIssuanceSet struct { BaseTx - // The MPTokenIssuance identifier. + // The MPTokenIssuance identifier as exactly 48 hexadecimal characters. MPTokenIssuanceID string // (Optional) XRPL Address of an individual token holder balance to lock/unlock. If omitted, this transaction applies to all any accounts holding MPTs. Holder *types.Address @@ -74,7 +61,8 @@ type MPTokenIssuanceSet struct { MPTokenMetadata *string `json:",omitempty"` // (Optional) New transfer fee value between 0 and 50,000. TransferFee *uint16 `json:",omitempty"` - // (Optional) Set or clear the flags which were marked as mutable. + // (Optional) One-way enable operations for capabilities permitted by the issuance's MutableFlags. + // Capabilities enabled here cannot be disabled. MutableFlags *uint32 `json:",omitempty"` // (Optional) A 33-byte compressed ElGamal public key for the issuer. // Required to use the confidential transfer feature. Must be 66 hex characters. @@ -154,69 +142,34 @@ func (m *MPTokenIssuanceSet) SetMPTSetCanLockMutableFlag() { m.setMutableFlag(TmfMPTSetCanLock) } -// SetMPTClearCanLockMutableFlag clears the CanLock mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanLockMutableFlag() { - m.setMutableFlag(TmfMPTClearCanLock) -} - // SetMPTSetRequireAuthMutableFlag sets the RequireAuth mutable flag. func (m *MPTokenIssuanceSet) SetMPTSetRequireAuthMutableFlag() { m.setMutableFlag(TmfMPTSetRequireAuth) } -// SetMPTClearRequireAuthMutableFlag clears the RequireAuth mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearRequireAuthMutableFlag() { - m.setMutableFlag(TmfMPTClearRequireAuth) -} - // SetMPTSetCanEscrowMutableFlag sets the CanEscrow mutable flag. func (m *MPTokenIssuanceSet) SetMPTSetCanEscrowMutableFlag() { m.setMutableFlag(TmfMPTSetCanEscrow) } -// SetMPTClearCanEscrowMutableFlag clears the CanEscrow mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanEscrowMutableFlag() { - m.setMutableFlag(TmfMPTClearCanEscrow) -} - // SetMPTSetCanTradeMutableFlag sets the CanTrade mutable flag. func (m *MPTokenIssuanceSet) SetMPTSetCanTradeMutableFlag() { m.setMutableFlag(TmfMPTSetCanTrade) } -// SetMPTClearCanTradeMutableFlag clears the CanTrade mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanTradeMutableFlag() { - m.setMutableFlag(TmfMPTClearCanTrade) -} - // SetMPTSetCanTransferMutableFlag sets the CanTransfer mutable flag. func (m *MPTokenIssuanceSet) SetMPTSetCanTransferMutableFlag() { m.setMutableFlag(TmfMPTSetCanTransfer) } -// SetMPTClearCanTransferMutableFlag clears the CanTransfer mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanTransferMutableFlag() { - m.setMutableFlag(TmfMPTClearCanTransfer) -} - // SetMPTSetCanClawbackMutableFlag sets the CanClawback mutable flag. func (m *MPTokenIssuanceSet) SetMPTSetCanClawbackMutableFlag() { m.setMutableFlag(TmfMPTSetCanClawback) } -// SetMPTClearCanClawbackMutableFlag clears the CanClawback mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanClawbackMutableFlag() { - m.setMutableFlag(TmfMPTClearCanClawback) -} - -// SetMPTSetCanConfidentialAmountMutableFlag sets the CanConfidentialAmount mutable flag. -func (m *MPTokenIssuanceSet) SetMPTSetCanConfidentialAmountMutableFlag() { - m.setMutableFlag(TmfMPTSetCanConfidentialAmount) -} - -// SetMPTClearCanConfidentialAmountMutableFlag clears the CanConfidentialAmount mutable flag. -func (m *MPTokenIssuanceSet) SetMPTClearCanConfidentialAmountMutableFlag() { - m.setMutableFlag(TmfMPTClearCanConfidentialAmount) +// SetMPTSetCanHoldConfidentialBalanceMutableFlag sets the CanHoldConfidentialBalance mutable flag. +func (m *MPTokenIssuanceSet) SetMPTSetCanHoldConfidentialBalanceMutableFlag() { + m.setMutableFlag(TmfMPTSetCanHoldConfidentialBalance) } // Validate validates the MPTokenIssuanceSet transaction ensuring all fields are correct. @@ -226,8 +179,8 @@ func (m *MPTokenIssuanceSet) Validate() (bool, error) { return false, err } - // MPTokenIssuanceID is required and must be valid hex. - if m.MPTokenIssuanceID == "" || !typecheck.IsHex(m.MPTokenIssuanceID) { + // MPTokenIssuanceID is required and must be exactly 24 bytes of hexadecimal. + if !IsMPTIssuanceID(m.MPTokenIssuanceID) { return false, ErrInvalidMPTokenIssuanceIDSet } @@ -272,16 +225,9 @@ func (m *MPTokenIssuanceSet) Validate() (bool, error) { return false, ErrMPTIssuanceSetFlagsMutuallyExclusive } - // MutableFlags cannot be zero when set. - if m.MutableFlags != nil && *m.MutableFlags == 0 { - return false, ErrMPTIssuanceSetMutableFlagsZero - } - - // Validate MutableFlags: cannot set and clear the same flag simultaneously. - if m.MutableFlags != nil { - if ok, err := validateMutableFlagsNoConflict(*m.MutableFlags); !ok { - return false, err - } + // MutableFlags must contain at least one supported enable operation. + if m.MutableFlags != nil && (*m.MutableFlags == 0 || *m.MutableFlags&^MPTokenIssuanceSetMutableFlagsMask != 0) { + return false, ErrMPTIssuanceSetInvalidMutableFlags } // TransferFee must not exceed MaxTransferFee. @@ -300,9 +246,9 @@ func (m *MPTokenIssuanceSet) Validate() (bool, error) { return false, ErrMPTIssuanceSetDomainIDInvalid } - // Non-zero TransferFee cannot be set together with tmfMPTClearCanTransfer (XLS-94). - if m.TransferFee != nil && *m.TransferFee != 0 && m.MutableFlags != nil && flag.Contains(*m.MutableFlags, TmfMPTClearCanTransfer) { - return false, ErrMPTIssuanceSetTransferFeeWithClearCanTransfer + // Confidential balances are encrypted, so a transfer fee cannot be enabled with them. + if m.TransferFee != nil && *m.TransferFee != 0 && m.MutableFlags != nil && flag.Contains(*m.MutableFlags, TmfMPTSetCanHoldConfidentialBalance) { + return false, ErrMPTIssuanceSetTransferFeeWithConfidentialBalance } // AuditorEncryptionKey requires IssuerEncryptionKey. @@ -310,11 +256,6 @@ func (m *MPTokenIssuanceSet) Validate() (bool, error) { return false, ErrMPTIssuanceSetAuditorRequiresIssuerKey } - // Encryption keys cannot be uploaded while clearing the confidential amount flag. - if hasEncryptionKeys && m.MutableFlags != nil && flag.Contains(*m.MutableFlags, TmfMPTClearCanConfidentialAmount) { - return false, ErrMPTIssuanceSetKeysWithClearCanConfidentialAmount - } - // Validate encryption key lengths (issuer and auditor keys must be 33-byte compressed). if m.IssuerEncryptionKey != nil && !IsValidCompressedEncryptionKey(*m.IssuerEncryptionKey) { return false, ErrMPTIssuanceSetInvalidKeyLength @@ -325,22 +266,3 @@ func (m *MPTokenIssuanceSet) Validate() (bool, error) { return true, nil } - -// validateMutableFlagsNoConflict checks that no set/clear pair is active simultaneously. -func validateMutableFlagsNoConflict(mf uint32) (bool, error) { - pairs := [7][2]uint32{ - {TmfMPTSetCanLock, TmfMPTClearCanLock}, - {TmfMPTSetRequireAuth, TmfMPTClearRequireAuth}, - {TmfMPTSetCanEscrow, TmfMPTClearCanEscrow}, - {TmfMPTSetCanTrade, TmfMPTClearCanTrade}, - {TmfMPTSetCanTransfer, TmfMPTClearCanTransfer}, - {TmfMPTSetCanClawback, TmfMPTClearCanClawback}, - {TmfMPTSetCanConfidentialAmount, TmfMPTClearCanConfidentialAmount}, - } - for _, p := range pairs { - if flag.Contains(mf, p[0]) && flag.Contains(mf, p[1]) { - return false, ErrMPTIssuanceSetMutableFlagsConflict - } - } - return true, nil -} diff --git a/xrpl/transaction/mptoken_issuance_set_test.go b/xrpl/transaction/mptoken_issuance_set_test.go index 40c585af..c291df8c 100644 --- a/xrpl/transaction/mptoken_issuance_set_test.go +++ b/xrpl/transaction/mptoken_issuance_set_test.go @@ -1,6 +1,7 @@ package transaction import ( + "encoding/json" "strings" "testing" @@ -8,902 +9,376 @@ import ( "github.com/stretchr/testify/require" ) +const validMPTIssuanceID = "00070C4495F14B0E44F78A264E41713C64B5F89242540EE2" + +// validCompressedKey is the compressed secp256k1 generator point. +const validCompressedKey = "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + +func validMPTokenIssuanceSet() *MPTokenIssuanceSet { + return &MPTokenIssuanceSet{ + BaseTx: BaseTx{ + Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", + TransactionType: MPTokenIssuanceSetTx, + }, + MPTokenIssuanceID: validMPTIssuanceID, + MutableFlags: types.MutableFlags(TmfMPTSetCanLock), + } +} + func TestMPTokenIssuanceSet_TxType(t *testing.T) { - tx := &MPTokenIssuanceSet{} - require.Equal(t, MPTokenIssuanceSetTx, tx.TxType()) + require.Equal(t, MPTokenIssuanceSetTx, (&MPTokenIssuanceSet{}).TxType()) } -// validCompressedKey is a 66-char hex string representing a valid compressed EC public key for testing. -var validCompressedKey = strings.Repeat("AB", 33) +func TestMPTokenIssuanceSet_FlattenAndJSON(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = types.MutableFlags(TmfMPTSetCanLock | TmfMPTSetCanHoldConfidentialBalance) + tx.MPTokenMetadata = types.MPTokenMetadata("464f4f") + tx.TransferFee = types.TransferFee(0) + tx.DomainID = types.DomainID("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9") + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + tx.AuditorEncryptionKey = types.EncryptionKey(validCompressedKey) + + require.Equal(t, FlatTransaction{ + "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", + "TransactionType": "MPTokenIssuanceSet", + "MPTokenIssuanceID": validMPTIssuanceID, + "DomainID": "A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9", + "MPTokenMetadata": "464f4f", + "TransferFee": uint16(0), + "MutableFlags": uint32(0x41), + "IssuerEncryptionKey": validCompressedKey, + "AuditorEncryptionKey": validCompressedKey, + }, tx.Flatten()) + + holderTx := &MPTokenIssuanceSet{ + BaseTx: BaseTx{ + Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", + TransactionType: MPTokenIssuanceSetTx, + Flags: TfMPTLock, + }, + MPTokenIssuanceID: validMPTIssuanceID, + Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), + } + require.Equal(t, FlatTransaction{ + "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", + "TransactionType": "MPTokenIssuanceSet", + "Flags": uint32(0x01), + "MPTokenIssuanceID": validMPTIssuanceID, + "Holder": "rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2", + }, holderTx.Flatten()) + + encoded, err := json.Marshal(tx) + require.NoError(t, err) + require.JSONEq(t, `{ + "Account":"rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", + "TransactionType":"MPTokenIssuanceSet", + "MPTokenIssuanceID":"00070C4495F14B0E44F78A264E41713C64B5F89242540EE2", + "Holder":null, + "DomainID":"A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9", + "MPTokenMetadata":"464f4f", + "TransferFee":0, + "MutableFlags":65, + "IssuerEncryptionKey":"`+validCompressedKey+`", + "AuditorEncryptionKey":"`+validCompressedKey+`" + }`, string(encoded)) +} -// validUncompressedKey is a 128-char hex string representing a valid uncompressed EC public key for testing. -var validUncompressedKey = strings.Repeat("CD", 64) +func TestMPTokenIssuanceSetMutableFlagValues(t *testing.T) { + require.Equal(t, uint32(0x01), TmfMPTSetCanLock) + require.Equal(t, uint32(0x02), TmfMPTSetRequireAuth) + require.Equal(t, uint32(0x04), TmfMPTSetCanEscrow) + require.Equal(t, uint32(0x08), TmfMPTSetCanTrade) + require.Equal(t, uint32(0x10), TmfMPTSetCanTransfer) + require.Equal(t, uint32(0x20), TmfMPTSetCanClawback) + require.Equal(t, uint32(0x40), TmfMPTSetCanHoldConfidentialBalance) + require.Equal(t, uint32(0x7F), MPTokenIssuanceSetMutableFlagsMask) +} -func TestMPTokenIssuanceSet_Flatten(t *testing.T) { +func TestMPTokenIssuanceSet_MutableFlagSettersPreserveBits(t *testing.T) { tests := []struct { - name string - tx *MPTokenIssuanceSet - expected FlatTransaction + name string + set func(*MPTokenIssuanceSet) + literal uint32 }{ - { - name: "pass - with holder", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - Flags: 1, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD"), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "Flags": uint32(1), - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "Holder": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - }, - { - name: "pass - without holder", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - Flags: 1, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "Flags": uint32(1), - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - }, - }, - { - name: "pass - with MPTokenMetadata", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata("464f4f"), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "MPTokenMetadata": "464f4f", - }, - }, - { - name: "pass - with TransferFee", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - TransferFee: types.TransferFee(314), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "TransferFee": uint16(314), - }, - }, - { - name: "pass - with MutableFlags", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanLock), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "MutableFlags": uint32(1), - }, - }, - { - name: "pass - with all DynamicMPT fields", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata("464f4f"), - TransferFee: types.TransferFee(314), - MutableFlags: types.MutableFlags(TmfMPTSetCanLock), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "MPTokenMetadata": "464f4f", - "TransferFee": uint16(314), - "MutableFlags": uint32(1), - }, - }, - { - name: "pass - with DomainID", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - DomainID: types.DomainID("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9"), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "DomainID": "A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9", - }, - }, - { - name: "pass - with IssuerEncryptionKey", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "IssuerEncryptionKey": validCompressedKey, - }, - }, - { - name: "pass - with both encryption keys", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - AuditorEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - expected: FlatTransaction{ - "TransactionType": "MPTokenIssuanceSet", - "Account": "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - "MPTokenIssuanceID": "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - "IssuerEncryptionKey": validCompressedKey, - "AuditorEncryptionKey": validCompressedKey, - }, - }, + {"SetCanLock", (*MPTokenIssuanceSet).SetMPTSetCanLockMutableFlag, 0x01}, + {"SetRequireAuth", (*MPTokenIssuanceSet).SetMPTSetRequireAuthMutableFlag, 0x02}, + {"SetCanEscrow", (*MPTokenIssuanceSet).SetMPTSetCanEscrowMutableFlag, 0x04}, + {"SetCanTrade", (*MPTokenIssuanceSet).SetMPTSetCanTradeMutableFlag, 0x08}, + {"SetCanTransfer", (*MPTokenIssuanceSet).SetMPTSetCanTransferMutableFlag, 0x10}, + {"SetCanClawback", (*MPTokenIssuanceSet).SetMPTSetCanClawbackMutableFlag, 0x20}, + {"SetCanHoldConfidentialBalance", (*MPTokenIssuanceSet).SetMPTSetCanHoldConfidentialBalanceMutableFlag, 0x40}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceSet{MutableFlags: types.MutableFlags(0x80000000)} + test.set(tx) + require.Equal(t, uint32(0x80000000)|test.literal, *tx.MutableFlags) + }) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - flattened := tt.tx.Flatten() - require.Equal(t, tt.expected, flattened) + tx := &MPTokenIssuanceSet{} + for _, test := range tests { + test.set(tx) + } + require.Equal(t, uint32(0x7F), *tx.MutableFlags) +} + +func TestMPTokenIssuanceSet_ValidateMutableFlags(t *testing.T) { + tests := []struct { + name string + flags uint32 + ok bool + }{ + {"one valid bit", TmfMPTSetCanTrade, true}, + {"all valid bits", MPTokenIssuanceSetMutableFlagsMask, true}, + {"present zero", 0, false}, + {"removed bit 0x80", 0x80, false}, + {"removed legacy clear bit", 0x2000, false}, + {"unknown high bit", 0x80000000, false}, + {"valid and unknown", TmfMPTSetCanLock | 0x100, false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = types.MutableFlags(test.flags) + ok, err := tx.Validate() + require.Equal(t, test.ok, ok) + if test.ok { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, ErrMPTIssuanceSetInvalidMutableFlags) + } }) } } -func TestMPTokenIssuanceSet_Validate(t *testing.T) { +func TestMPTokenIssuanceSet_TransferFeeConfidentialBalance(t *testing.T) { tests := []struct { - name string - tx *MPTokenIssuanceSet - wantOk bool - wantErr error + name string + fee *uint16 + flags uint32 + ok bool }{ - { - name: "pass - valid transaction with holder", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "fail - empty MPTokenIssuanceID", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock, - }, - MPTokenIssuanceID: "", - }, - wantOk: false, - wantErr: ErrInvalidMPTokenIssuanceIDSet, - }, - { - name: "fail - non-hex MPTokenIssuanceID", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock, - }, - MPTokenIssuanceID: "not-a-hex-value!", - }, - wantOk: false, - wantErr: ErrInvalidMPTokenIssuanceIDSet, - }, - { - name: "fail - no operation specified (no-op)", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetEmpty, - }, - { - name: "fail - invalid holder address", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("invalid"), - }, - wantOk: false, - wantErr: ErrInvalidAccount, - }, - { - name: "fail - holder same as account", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD"), - }, - wantOk: false, - wantErr: ErrHolderAccountConflict, - }, - { - name: "fail - conflicting lock/unlock flags", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock | TfMPTUnlock, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - }, - wantOk: false, - wantErr: ErrMPTokenIssuanceSetFlags, - }, - { - name: "fail - holder mutually exclusive with MutableFlags", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), - MutableFlags: types.MutableFlags(TmfMPTSetCanLock), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetHolderMutuallyExclusive, - }, - { - name: "fail - holder mutually exclusive with MPTokenMetadata", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), - MPTokenMetadata: types.MPTokenMetadata("464f4f"), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetHolderMutuallyExclusive, - }, - { - name: "fail - flags mutually exclusive with DynamicMPT fields", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanLock), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetFlagsMutuallyExclusive, - }, - { - name: "fail - MutableFlags cannot be zero", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(0), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsZero, - }, - { - name: "fail - MutableFlags set/clear conflict", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanLock | TmfMPTClearCanLock), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - TransferFee exceeds maximum", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - TransferFee: types.TransferFee(50001), - }, - wantOk: false, - wantErr: ErrInvalidTransferFee, - }, - { - name: "fail - invalid hex MPTokenMetadata", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata("not-hex!"), - }, - wantOk: false, - wantErr: ErrInvalidMPTokenMetadata, - }, - { - name: "fail - MPTokenMetadata exceeds 1024 bytes", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata(strings.Repeat("AB", 1025)), - }, - wantOk: false, - wantErr: ErrInvalidMPTokenMetadata, - }, - { - name: "pass - MPTokenMetadata exactly 1024 bytes", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata(strings.Repeat("AB", 1024)), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - empty MPTokenMetadata removes field", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MPTokenMetadata: types.MPTokenMetadata(""), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - valid DynamicMPT usage with all fields", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanLock | TmfMPTClearCanEscrow), - TransferFee: types.TransferFee(500), - MPTokenMetadata: types.MPTokenMetadata("464f4f"), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - valid DomainID", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - DomainID: types.DomainID("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9"), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - empty DomainID removes domain", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - DomainID: types.DomainID(""), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "fail - DomainID invalid hex", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - DomainID: types.DomainID("not-valid"), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetDomainIDInvalid, - }, - { - name: "fail - DomainID mutually exclusive with Holder", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), - DomainID: types.DomainID("A738A1E6E8505E1FC77BBB9FEF84FF9A9C609F2739E0F9573CDD6367100A0AA9"), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetHolderMutuallyExclusive, - }, - { - name: "pass - valid with IssuerEncryptionKey compressed", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "fail - IssuerEncryptionKey uncompressed not allowed", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validUncompressedKey), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetInvalidKeyLength, - }, - { - name: "pass - valid with both encryption keys", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - AuditorEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - enable confidential with both encryption keys", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanConfidentialAmount), - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - AuditorEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "fail - AuditorEncryptionKey without IssuerEncryptionKey", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - AuditorEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetAuditorRequiresIssuerKey, - }, - { - name: "fail - encryption keys with clear confidential amount", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTClearCanConfidentialAmount), - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetKeysWithClearCanConfidentialAmount, - }, - { - name: "fail - IssuerEncryptionKey invalid length", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey("AABB"), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetInvalidKeyLength, - }, - { - name: "fail - IssuerEncryptionKey invalid hex", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(strings.Repeat("GG", 33)), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetInvalidKeyLength, - }, - { - name: "fail - AuditorEncryptionKey invalid length", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - AuditorEncryptionKey: types.EncryptionKey("AABB"), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetInvalidKeyLength, - }, - { - name: "fail - encryption keys with Holder", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - Holder: types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2"), - IssuerEncryptionKey: types.EncryptionKey(validCompressedKey), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetKeyConflict, - }, - { - name: "fail - non-zero TransferFee with tmfMPTClearCanTransfer", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - TransferFee: types.TransferFee(200), - MutableFlags: types.MutableFlags(TmfMPTClearCanTransfer), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetTransferFeeWithClearCanTransfer, - }, - { - name: "pass - zero TransferFee with tmfMPTClearCanTransfer", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - TransferFee: types.TransferFee(0), - MutableFlags: types.MutableFlags(TmfMPTClearCanTransfer), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "pass - zero TransferFee alone is valid DynamicMPT operation", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - TransferFee: types.TransferFee(0), - }, - wantOk: true, - wantErr: nil, - }, - { - name: "fail - MutableFlags with Flags returns mutual exclusivity error", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - Flags: TfMPTLock, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(0), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetFlagsMutuallyExclusive, - }, - { - name: "fail - MutableFlags set/clear conflict RequireAuth", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetRequireAuth | TmfMPTClearRequireAuth), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - MutableFlags set/clear conflict CanEscrow", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanEscrow | TmfMPTClearCanEscrow), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - MutableFlags set/clear conflict CanTrade", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanTrade | TmfMPTClearCanTrade), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - MutableFlags set/clear conflict CanTransfer", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanTransfer | TmfMPTClearCanTransfer), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - MutableFlags set/clear conflict CanClawback", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanClawback | TmfMPTClearCanClawback), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, - { - name: "fail - MutableFlags set/clear conflict CanConfidentialAmount", - tx: &MPTokenIssuanceSet{ - BaseTx: BaseTx{ - Account: "rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD", - TransactionType: MPTokenIssuanceSetTx, - }, - MPTokenIssuanceID: "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000", - MutableFlags: types.MutableFlags(TmfMPTSetCanConfidentialAmount | TmfMPTClearCanConfidentialAmount), - }, - wantOk: false, - wantErr: ErrMPTIssuanceSetMutableFlagsConflict, - }, + {"non-zero fee with confidential", types.TransferFee(1), TmfMPTSetCanHoldConfidentialBalance, false}, + {"non-zero fee with confidential and another bit", types.TransferFee(1), TmfMPTSetCanLock | TmfMPTSetCanHoldConfidentialBalance, false}, + {"zero fee with confidential", types.TransferFee(0), TmfMPTSetCanHoldConfidentialBalance, true}, + {"absent fee with confidential", nil, TmfMPTSetCanHoldConfidentialBalance, true}, + {"non-zero fee without confidential", types.TransferFee(1), TmfMPTSetCanTransfer, true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.TransferFee = test.fee + tx.MutableFlags = types.MutableFlags(test.flags) + ok, err := tx.Validate() + require.Equal(t, test.ok, ok) + if test.ok { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, ErrMPTIssuanceSetTransferFeeWithConfidentialBalance) + } + }) } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ok, err := tt.tx.Validate() - require.Equal(t, tt.wantOk, ok) - require.Equal(t, tt.wantErr, err) +func TestMPTokenIssuanceSet_ValidateIssuanceIDAndHolder(t *testing.T) { + tests := []struct { + name string + mutate func(*MPTokenIssuanceSet) + err error + }{ + {"empty issuance ID", func(tx *MPTokenIssuanceSet) { tx.MPTokenIssuanceID = "" }, ErrInvalidMPTokenIssuanceIDSet}, + {"non-hex 48-character issuance ID", func(tx *MPTokenIssuanceSet) { + tx.MPTokenIssuanceID = "00070C4495F14B0E44F78A264E41713C64B5F89242540EG2" + }, ErrInvalidMPTokenIssuanceIDSet}, + {"46-character issuance ID", func(tx *MPTokenIssuanceSet) { tx.MPTokenIssuanceID = "00070C4495F14B0E44F78A264E41713C64B5F89242540E" }, ErrInvalidMPTokenIssuanceIDSet}, + {"50-character issuance ID", func(tx *MPTokenIssuanceSet) { + tx.MPTokenIssuanceID = "00070C4495F14B0E44F78A264E41713C64B5F89242540EE200" + }, ErrInvalidMPTokenIssuanceIDSet}, + {"oversized issuance ID", func(tx *MPTokenIssuanceSet) { + tx.MPTokenIssuanceID = "00070C4495F14B0E44F78A264E41713C64B5F89242540EE255534400000000000000" + }, ErrInvalidMPTokenIssuanceIDSet}, + {"invalid holder", func(tx *MPTokenIssuanceSet) { + tx.MutableFlags = nil + tx.Holder = types.Holder("invalid") + }, ErrInvalidAccount}, + {"holder equals account", func(tx *MPTokenIssuanceSet) { + tx.MutableFlags = nil + tx.Holder = types.Holder("rLUEXYuLiQptky37CqLcm9USQpPiz5rkpD") + }, ErrHolderAccountConflict}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + test.mutate(tx) + ok, err := tx.Validate() + require.False(t, ok) + require.ErrorIs(t, err, test.err) }) } + + t.Run("valid standalone holder", func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = nil + tx.Holder = types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2") + ok, err := tx.Validate() + require.True(t, ok) + require.NoError(t, err) + }) } -func TestMPTokenIssuanceSet_MutableFlags(t *testing.T) { +func TestMPTokenIssuanceSet_ValidateMetadata(t *testing.T) { tests := []struct { name string - setFlag func(*MPTokenIssuanceSet) - flagMask uint32 + metadata string + ok bool }{ - { - name: "MPTSetCanLock", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanLockMutableFlag, - flagMask: TmfMPTSetCanLock, - }, - { - name: "MPTClearCanLock", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanLockMutableFlag, - flagMask: TmfMPTClearCanLock, - }, - { - name: "MPTSetRequireAuth", - setFlag: (*MPTokenIssuanceSet).SetMPTSetRequireAuthMutableFlag, - flagMask: TmfMPTSetRequireAuth, - }, - { - name: "MPTClearRequireAuth", - setFlag: (*MPTokenIssuanceSet).SetMPTClearRequireAuthMutableFlag, - flagMask: TmfMPTClearRequireAuth, - }, - { - name: "MPTSetCanEscrow", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanEscrowMutableFlag, - flagMask: TmfMPTSetCanEscrow, - }, - { - name: "MPTClearCanEscrow", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanEscrowMutableFlag, - flagMask: TmfMPTClearCanEscrow, - }, - { - name: "MPTSetCanTrade", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanTradeMutableFlag, - flagMask: TmfMPTSetCanTrade, - }, - { - name: "MPTClearCanTrade", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanTradeMutableFlag, - flagMask: TmfMPTClearCanTrade, - }, - { - name: "MPTSetCanTransfer", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanTransferMutableFlag, - flagMask: TmfMPTSetCanTransfer, - }, - { - name: "MPTClearCanTransfer", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanTransferMutableFlag, - flagMask: TmfMPTClearCanTransfer, - }, - { - name: "MPTSetCanClawback", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanClawbackMutableFlag, - flagMask: TmfMPTSetCanClawback, - }, - { - name: "MPTClearCanClawback", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanClawbackMutableFlag, - flagMask: TmfMPTClearCanClawback, - }, - { - name: "MPTSetCanConfidentialAmount", - setFlag: (*MPTokenIssuanceSet).SetMPTSetCanConfidentialAmountMutableFlag, - flagMask: TmfMPTSetCanConfidentialAmount, - }, - { - name: "MPTClearCanConfidentialAmount", - setFlag: (*MPTokenIssuanceSet).SetMPTClearCanConfidentialAmountMutableFlag, - flagMask: TmfMPTClearCanConfidentialAmount, - }, + {"exactly 1024 bytes", strings.Repeat("AB", 1024), true}, + {"over 1024 bytes", strings.Repeat("AB", 1025), false}, + {"invalid hex", "not-hex", false}, + {"empty removes metadata", "", true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = nil + tx.MPTokenMetadata = types.MPTokenMetadata(test.metadata) + ok, err := tx.Validate() + require.Equal(t, test.ok, ok) + if test.ok { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, ErrInvalidMPTokenMetadata) + } + }) } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx := &MPTokenIssuanceSet{} - tt.setFlag(tx) - require.NotNil(t, tx.MutableFlags) - require.Equal(t, tt.flagMask, *tx.MutableFlags) +func TestMPTokenIssuanceSet_ValidateEncryptionKeys(t *testing.T) { + invalidSyntax := "02" + strings.Repeat("GG", 32) + tests := []struct { + name string + mutate func(*MPTokenIssuanceSet) + err error + }{ + {"issuer invalid syntax", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey(invalidSyntax) + }, ErrMPTIssuanceSetInvalidKeyLength}, + {"issuer invalid length", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey("02AABB") + }, ErrMPTIssuanceSetInvalidKeyLength}, + {"auditor invalid syntax", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + tx.AuditorEncryptionKey = types.EncryptionKey(invalidSyntax) + }, ErrMPTIssuanceSetInvalidKeyLength}, + {"auditor invalid length", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + tx.AuditorEncryptionKey = types.EncryptionKey("03AABB") + }, ErrMPTIssuanceSetInvalidKeyLength}, + {"auditor without issuer", func(tx *MPTokenIssuanceSet) { + tx.AuditorEncryptionKey = types.EncryptionKey(validCompressedKey) + }, ErrMPTIssuanceSetAuditorRequiresIssuerKey}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = nil + test.mutate(tx) + ok, err := tx.Validate() + require.False(t, ok) + require.ErrorIs(t, err, test.err) }) } - // Test all mutable flags together - tx := &MPTokenIssuanceSet{} - for _, tt := range tests { - tt.setFlag(tx) + for _, test := range []struct { + name string + mutate func(*MPTokenIssuanceSet) + }{ + {"issuer key", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + }}, + {"paired issuer and auditor keys", func(tx *MPTokenIssuanceSet) { + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + tx.AuditorEncryptionKey = types.EncryptionKey(validCompressedKey) + }}, + } { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = nil + test.mutate(tx) + ok, err := tx.Validate() + require.True(t, ok) + require.NoError(t, err) + }) } +} - expectedMutableFlags := TmfMPTSetCanLock | TmfMPTClearCanLock | - TmfMPTSetRequireAuth | TmfMPTClearRequireAuth | - TmfMPTSetCanEscrow | TmfMPTClearCanEscrow | - TmfMPTSetCanTrade | TmfMPTClearCanTrade | - TmfMPTSetCanTransfer | TmfMPTClearCanTransfer | - TmfMPTSetCanClawback | TmfMPTClearCanClawback | - TmfMPTSetCanConfidentialAmount | TmfMPTClearCanConfidentialAmount - require.Equal(t, expectedMutableFlags, *tx.MutableFlags) +func TestMPTokenIssuanceSet_ValidateOperationConflictsAndBounds(t *testing.T) { + tests := []struct { + name string + mutate func(*MPTokenIssuanceSet) + err error + }{ + {"no operation", func(tx *MPTokenIssuanceSet) { tx.MutableFlags = nil }, ErrMPTIssuanceSetEmpty}, + {"conflicting lock flags", func(tx *MPTokenIssuanceSet) { tx.MutableFlags = nil; tx.Flags = TfMPTLock | TfMPTUnlock }, ErrMPTokenIssuanceSetFlags}, + {"flags with mutation", func(tx *MPTokenIssuanceSet) { tx.Flags = TfMPTLock }, ErrMPTIssuanceSetFlagsMutuallyExclusive}, + {"holder with mutation", func(tx *MPTokenIssuanceSet) { tx.Holder = types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2") }, ErrMPTIssuanceSetHolderMutuallyExclusive}, + {"holder with key", func(tx *MPTokenIssuanceSet) { + tx.MutableFlags = nil + tx.Holder = types.Holder("rNCFjv8Ek5oDrNiMJ3pw6eLLFtMjZLJnf2") + tx.IssuerEncryptionKey = types.EncryptionKey(validCompressedKey) + }, ErrMPTIssuanceSetKeyConflict}, + {"fee too high", func(tx *MPTokenIssuanceSet) { tx.TransferFee = types.TransferFee(50001) }, ErrInvalidTransferFee}, + {"invalid domain", func(tx *MPTokenIssuanceSet) { tx.DomainID = types.DomainID("not-hex") }, ErrMPTIssuanceSetDomainIDInvalid}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + test.mutate(tx) + ok, err := tx.Validate() + require.False(t, ok) + require.ErrorIs(t, err, test.err) + }) + } } -func TestMPTokenIssuanceSet_Flags(t *testing.T) { +func TestMPTokenIssuanceSet_ZeroValuesAllowed(t *testing.T) { tests := []struct { - name string - setFlags func(*MPTokenIssuanceSet) - want uint32 + name string + mutate func(*MPTokenIssuanceSet) }{ - { - name: "pass - set MPTLock flag", - setFlags: func(tx *MPTokenIssuanceSet) { - tx.SetMPTLockFlag() - }, - want: TfMPTLock, - }, - { - name: "pass - set MPTUnlock flag", - setFlags: func(tx *MPTokenIssuanceSet) { - tx.SetMPTUnlockFlag() - }, - want: TfMPTUnlock, - }, - { - name: "pass - set both flags", - setFlags: func(tx *MPTokenIssuanceSet) { - tx.SetMPTLockFlag() - tx.SetMPTUnlockFlag() - }, - want: TfMPTLock | TfMPTUnlock, - }, + {"empty domain removes", func(tx *MPTokenIssuanceSet) { tx.DomainID = types.DomainID("") }}, + {"zero transfer fee", func(tx *MPTokenIssuanceSet) { tx.TransferFee = types.TransferFee(0) }}, } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := validMPTokenIssuanceSet() + tx.MutableFlags = nil + test.mutate(tx) + ok, err := tx.Validate() + require.True(t, ok) + require.NoError(t, err) + }) + } +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tx := &MPTokenIssuanceSet{} - tt.setFlags(tx) - require.Equal(t, tt.want, tx.Flags) +func TestMPTokenIssuanceSet_FlagValuesAndSettersPreserveBits(t *testing.T) { + require.Equal(t, uint32(0x01), TfMPTLock) + require.Equal(t, uint32(0x02), TfMPTUnlock) + + tests := []struct { + name string + set func(*MPTokenIssuanceSet) + literal uint32 + }{ + {"lock", (*MPTokenIssuanceSet).SetMPTLockFlag, 0x01}, + {"unlock", (*MPTokenIssuanceSet).SetMPTUnlockFlag, 0x02}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tx := &MPTokenIssuanceSet{BaseTx: BaseTx{Flags: 0x100}} + test.set(tx) + require.Equal(t, uint32(0x100)|test.literal, tx.Flags) }) } }