diff --git a/Cargo.lock b/Cargo.lock index 4228f6cc03..845f1477d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,7 +509,7 @@ dependencies = [ "prefix-hex", "rustyline", "serde_json", - "strum", + "strum 0.25.0", "thiserror", "tokio", "winapi", @@ -1645,7 +1645,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "strum", + "strum 0.26.1", "thiserror", "time", "tokio", @@ -1671,7 +1671,7 @@ dependencies = [ "primitive-types", "serde", "serde_json", - "strum", + "strum 0.25.0", "thiserror", "tokio", "url", @@ -3148,7 +3148,16 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.3", +] + +[[package]] +name = "strum" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +dependencies = [ + "strum_macros 0.26.1", ] [[package]] @@ -3164,6 +3173,19 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/bindings/core/src/method_handler/secret_manager.rs b/bindings/core/src/method_handler/secret_manager.rs index 19f4806d31..15fb53f1cc 100644 --- a/bindings/core/src/method_handler/secret_manager.rs +++ b/bindings/core/src/method_handler/secret_manager.rs @@ -67,11 +67,14 @@ where } #[cfg(feature = "ledger_nano")] SecretManagerMethod::GetLedgerNanoStatus => { - if let Some(secret_manager) = secret_manager.downcast::() { - Response::LedgerNanoStatus(secret_manager.get_ledger_nano_status().await) + let secret_manager = if let Some(secret_manager) = secret_manager.downcast::() { + secret_manager + } else if let Some(SecretManager::LedgerNano(secret_manager)) = secret_manager.downcast::() { + secret_manager } else { return Err(iota_sdk::client::Error::SecretManagerMismatch.into()); - } + }; + Response::LedgerNanoStatus(secret_manager.get_ledger_nano_status().await) } SecretManagerMethod::SignTransaction { prepared_transaction_data, diff --git a/bindings/nodejs/lib/types/models/transaction-failure-reason.ts b/bindings/nodejs/lib/types/models/transaction-failure-reason.ts index d080b68eaf..fa464043af 100644 --- a/bindings/nodejs/lib/types/models/transaction-failure-reason.ts +++ b/bindings/nodejs/lib/types/models/transaction-failure-reason.ts @@ -5,141 +5,71 @@ * Reason for transaction failure. */ export enum TransactionFailureReason { - /** - * The referenced UTXO was already spent. - */ - InputUTXOAlreadySpent = 1, - - /** - * The transaction is conflicting with another transaction. - * Conflicting specifically means a double spend situation that both transactions pass all validation rules, - * eventually losing one(s) should have this reason. - */ - ConflictingWithAnotherTx = 2, - - /** - * The referenced UTXO is invalid. - */ - InvalidReferencedUtxo = 3, - - /** - * The transaction is invalid. - */ - InvalidTransaction = 4, - - /** - * The sum of the inputs and output base token amount does not match. - */ - SumInputsOutputsAmountMismatch = 5, - - /** - * The unlock block signature is invalid. - */ - InvalidUnlockBlockSignature = 6, - - /** - * The configured timelock is not yet expired. - */ - TimelockNotExpired = 7, - - /** - * The given native tokens are invalid. - */ - InvalidNativeTokens = 8, - - /** - * The return amount in a transaction is not fulfilled by the output side. - */ - StorageDepositReturnUnfulfilled = 9, - - /** - * An input unlock was invalid. - */ - InvalidInputUnlock = 10, - - /** - * The output contains a Sender with an ident (address) which is not unlocked. - */ - SenderNotUnlocked = 11, - - /** - * The chain state transition is invalid. - */ - InvalidChainStateTransition = 12, - - /** - * The referenced input is created after transaction issuing time. - */ - InvalidTransactionIssuingTime = 13, - - /** - * The mana amount is invalid. - */ - InvalidManaAmount = 14, - - /** - * The Block Issuance Credits amount is invalid. - */ - InvalidBlockIssuanceCreditsAmount = 15, - - /** - * Reward Context Input is invalid. - */ - InvalidRewardContextInput = 16, - - /** - * Commitment Context Input is invalid. - */ - InvalidCommitmentContextInput = 17, - - /** - * Staking Feature is not provided in account output when claiming rewards. - */ - MissingStakingFeature = 18, - - /** - * Failed to claim staking reward. - */ - FailedToClaimStakingReward = 19, - - /** - * Failed to claim delegation reward. - */ - FailedToClaimDelegationReward = 20, - - /** - * Burning of native tokens is not allowed in the transaction capabilities. - */ - TransactionCapabilityNativeTokenBurningNotAllowed = 21, - - /** - * Burning of mana is not allowed in the transaction capabilities. - */ - TransactionCapabilityManaBurningNotAllowed = 22, - - /** - * Destruction of accounts is not allowed in the transaction capabilities. - */ - TransactionCapabilityAccountDestructionNotAllowed = 23, - - /** - * Destruction of anchors is not allowed in the transaction capabilities. - */ - TransactionCapabilityAnchorDestructionNotAllowed = 24, - - /** - * Destruction of foundries is not allowed in the transaction capabilities. - */ - TransactionCapabilityFoundryDestructionNotAllowed = 25, - - /** - * Destruction of nfts is not allowed in the transaction capabilities. - */ - TransactionCapabilityNftDestructionNotAllowed = 26, - - /** - * The semantic validation failed for a reason not covered by the previous variants. - */ + None = 0, + TypeInvalid = 1, + Conflicting = 2, + InputAlreadySpent = 3, + InputCreationAfterTxCreation = 4, + UnlockSignatureInvalid = 5, + CommitmentInputMissing = 6, + CommitmentInputReferenceInvalid = 7, + BicInputReferenceInvalid = 8, + RewardInputReferenceInvalid = 9, + StakingRewardCalculationFailure = 10, + DelegationRewardCalculationFailure = 11, + InputOutputBaseTokenMismatch = 12, + ManaOverflow = 13, + InputOutputManaMismatch = 14, + ManaDecayCreationIndexExceedsTargetIndex = 15, + NativeTokenAmountLessThanZero = 16, + NativeTokenSumExceedsUint256 = 17, + NativeTokenSumUnbalanced = 18, + MultiAddressLengthUnlockLengthMismatch = 19, + MultiAddressUnlockThresholdNotReached = 20, + NestedMultiUnlock = 21, + SenderFeatureNotUnlocked = 22, + IssuerFeatureNotUnlocked = 23, + StakingRewardInputMissing = 24, + StakingBlockIssuerFeatureMissing = 25, + StakingCommitmentInputMissing = 26, + StakingRewardClaimingInvalid = 27, + StakingFeatureRemovedBeforeUnbonding = 28, + StakingFeatureModifiedBeforeUnbonding = 29, + StakingStartEpochInvalid = 30, + StakingEndEpochTooEarly = 31, + BlockIssuerCommitmentInputMissing = 32, + BlockIssuanceCreditInputMissing = 33, + BlockIssuerNotExpired = 34, + BlockIssuerExpiryTooEarly = 35, + ManaMovedOffBlockIssuerAccount = 36, + AccountLocked = 37, + TimelockCommitmentInputMissing = 38, + TimelockNotExpired = 39, + ExpirationCommitmentInputMissing = 40, + ExpirationNotUnlockable = 41, + ReturnAmountNotFulFilled = 42, + NewChainOutputHasNonZeroedId = 43, + ChainOutputImmutableFeaturesChanged = 44, + ImplicitAccountDestructionDisallowed = 45, + MultipleImplicitAccountCreationAddresses = 46, + AccountInvalidFoundryCounter = 47, + FoundryTransitionWithoutAccount = 48, + FoundrySerialInvalid = 49, + DelegationCommitmentInputMissing = 50, + DelegationRewardInputMissing = 51, + DelegationRewardsClaimingInvalid = 52, + DelegationOutputTransitionedTwice = 53, + DelegationModified = 54, + DelegationStartEpochInvalid = 55, + DelegationAmountMismatch = 56, + DelegationEndEpochNotZero = 57, + DelegationEndEpochInvalid = 58, + CapabilitiesNativeTokenBurningNotAllowed = 59, + CapabilitiesManaBurningNotAllowed = 60, + CapabilitiesAccountDestructionNotAllowed = 61, + CapabilitiesAnchorDestructionNotAllowed = 62, + CapabilitiesFoundryDestructionNotAllowed = 63, + CapabilitiesNftDestructionNotAllowed = 64, SemanticValidationFailed = 255, } @@ -149,57 +79,131 @@ export enum TransactionFailureReason { export const TRANSACTION_FAILURE_REASON_STRINGS: { [key in TransactionFailureReason]: string; } = { - [TransactionFailureReason.InputUTXOAlreadySpent]: - 'The referenced UTXO was already spent.', - [TransactionFailureReason.ConflictingWithAnotherTx]: - 'The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason.', - [TransactionFailureReason.InvalidReferencedUtxo]: - 'The referenced UTXO is invalid.', - [TransactionFailureReason.InvalidTransaction]: - 'The transaction is invalid.', - [TransactionFailureReason.SumInputsOutputsAmountMismatch]: - 'The sum of the inputs and output base token amount does not match.', - [TransactionFailureReason.InvalidUnlockBlockSignature]: - 'The unlock block signature is invalid.', - [TransactionFailureReason.TimelockNotExpired]: - 'The configured timelock is not yet expired.', - [TransactionFailureReason.InvalidNativeTokens]: - 'The given native tokens are invalid.', - [TransactionFailureReason.StorageDepositReturnUnfulfilled]: - 'The return amount in a transaction is not fulfilled by the output side.', - [TransactionFailureReason.InvalidInputUnlock]: - 'An input unlock was invalid.', - [TransactionFailureReason.SenderNotUnlocked]: - 'The output contains a Sender with an ident (address) which is not unlocked.', - [TransactionFailureReason.InvalidChainStateTransition]: - 'The chain state transition is invalid.', - [TransactionFailureReason.InvalidTransactionIssuingTime]: - 'The referenced input is created after transaction issuing time.', - [TransactionFailureReason.InvalidManaAmount]: 'The mana amount is invalid.', - [TransactionFailureReason.InvalidBlockIssuanceCreditsAmount]: - 'The Block Issuance Credits amount is invalid.', - [TransactionFailureReason.InvalidRewardContextInput]: - 'Reward Context Input is invalid.', - [TransactionFailureReason.InvalidCommitmentContextInput]: - 'Commitment Context Input is invalid.', - [TransactionFailureReason.MissingStakingFeature]: - 'Staking Feature is not provided in account output when claiming rewards.', - [TransactionFailureReason.FailedToClaimStakingReward]: - 'Failed to claim staking reward.', - [TransactionFailureReason.FailedToClaimDelegationReward]: - 'Failed to claim delegation reward.', - [TransactionFailureReason.TransactionCapabilityNativeTokenBurningNotAllowed]: - 'Burning of native tokens is not allowed in the transaction capabilities.', - [TransactionFailureReason.TransactionCapabilityManaBurningNotAllowed]: - 'Burning of mana is not allowed in the transaction capabilities.', - [TransactionFailureReason.TransactionCapabilityAccountDestructionNotAllowed]: - 'Destruction of accounts is not allowed in the transaction capabilities.', - [TransactionFailureReason.TransactionCapabilityAnchorDestructionNotAllowed]: - 'Destruction of anchors is not allowed in the transaction capabilities.', - [TransactionFailureReason.TransactionCapabilityFoundryDestructionNotAllowed]: - 'Destruction of foundries is not allowed in the transaction capabilities.', - [TransactionFailureReason.TransactionCapabilityNftDestructionNotAllowed]: - 'Destruction of nfts is not allowed in the transaction capabilities.', + [TransactionFailureReason.None]: 'None.', + [TransactionFailureReason.TypeInvalid]: 'Transaction type is invalid.', + [TransactionFailureReason.Conflicting]: 'Transaction is conflicting.', + [TransactionFailureReason.InputAlreadySpent]: 'Input already spent.', + [TransactionFailureReason.InputCreationAfterTxCreation]: + 'Input creation slot after tx creation slot.', + [TransactionFailureReason.UnlockSignatureInvalid]: + 'Signature in unlock is invalid.', + [TransactionFailureReason.CommitmentInputMissing]: + 'Commitment input required with reward or BIC input.', + [TransactionFailureReason.CommitmentInputReferenceInvalid]: + 'Commitment input references an invalid or non-existent commitment.', + [TransactionFailureReason.BicInputReferenceInvalid]: + 'BIC input reference cannot be loaded.', + [TransactionFailureReason.RewardInputReferenceInvalid]: + 'Reward input does not reference a staking account or a delegation output.', + [TransactionFailureReason.StakingRewardCalculationFailure]: + 'Staking rewards could not be calculated due to storage issues or overflow.', + [TransactionFailureReason.DelegationRewardCalculationFailure]: + 'Delegation rewards could not be calculated due to storage issues or overflow.', + [TransactionFailureReason.InputOutputBaseTokenMismatch]: + 'Inputs and outputs do not spend/deposit the same amount of base tokens.', + [TransactionFailureReason.ManaOverflow]: + 'Under- or overflow in Mana calculations.', + [TransactionFailureReason.InputOutputManaMismatch]: + 'Inputs and outputs do not contain the same amount of Mana.', + [TransactionFailureReason.ManaDecayCreationIndexExceedsTargetIndex]: + 'Mana decay creation slot/epoch index exceeds target slot/epoch index.', + [TransactionFailureReason.NativeTokenAmountLessThanZero]: + 'Native token amount must be greater than zero.', + [TransactionFailureReason.NativeTokenSumExceedsUint256]: + 'Native token sum exceeds max value of a uint256.', + [TransactionFailureReason.NativeTokenSumUnbalanced]: + 'Native token sums are unbalanced.', + [TransactionFailureReason.MultiAddressLengthUnlockLengthMismatch]: + 'Multi address length and multi unlock length do not match.', + [TransactionFailureReason.MultiAddressUnlockThresholdNotReached]: + 'Multi address unlock threshold not reached.', + [TransactionFailureReason.NestedMultiUnlock]: + "Multi unlocks can't be nested.", + [TransactionFailureReason.SenderFeatureNotUnlocked]: + 'Sender feature is not unlocked.', + [TransactionFailureReason.IssuerFeatureNotUnlocked]: + 'Issuer feature is not unlocked.', + [TransactionFailureReason.StakingRewardInputMissing]: + 'Staking feature removal or resetting requires a reward input.', + [TransactionFailureReason.StakingBlockIssuerFeatureMissing]: + 'Block issuer feature missing for account with staking feature.', + [TransactionFailureReason.StakingCommitmentInputMissing]: + 'Staking feature validation requires a commitment input.', + [TransactionFailureReason.StakingRewardClaimingInvalid]: + 'Staking feature must be removed or reset in order to claim rewards.', + [TransactionFailureReason.StakingFeatureRemovedBeforeUnbonding]: + 'Staking feature can only be removed after the unbonding period.', + [TransactionFailureReason.StakingFeatureModifiedBeforeUnbonding]: + 'Staking start epoch, fixed cost and staked amount cannot be modified while bonded.', + [TransactionFailureReason.StakingStartEpochInvalid]: + 'Staking start epoch must be the epoch of the transaction.', + [TransactionFailureReason.StakingEndEpochTooEarly]: + 'Staking end epoch must be set to the transaction epoch plus the unbonding period.', + [TransactionFailureReason.BlockIssuerCommitmentInputMissing]: + 'Commitment input missing for block issuer feature.', + [TransactionFailureReason.BlockIssuanceCreditInputMissing]: + 'Block issuance credit input missing for account with block issuer feature.', + [TransactionFailureReason.BlockIssuerNotExpired]: + 'Block issuer feature has not expired.', + [TransactionFailureReason.BlockIssuerExpiryTooEarly]: + 'Block issuer feature expiry set too early.', + [TransactionFailureReason.ManaMovedOffBlockIssuerAccount]: + 'Mana cannot be moved off block issuer accounts except with manalocks.', + [TransactionFailureReason.AccountLocked]: + 'Account is locked due to negative block issuance credits.', + [TransactionFailureReason.TimelockCommitmentInputMissing]: + "Transaction's containing a timelock condition require a commitment input.", + [TransactionFailureReason.TimelockNotExpired]: 'Timelock not expired.', + [TransactionFailureReason.ExpirationCommitmentInputMissing]: + "Transaction's containing an expiration condition require a commitment input.", + [TransactionFailureReason.ExpirationNotUnlockable]: + 'Expiration unlock condition cannot be unlocked.', + [TransactionFailureReason.ReturnAmountNotFulFilled]: + 'Return amount not fulfilled.', + [TransactionFailureReason.NewChainOutputHasNonZeroedId]: + 'New chain output has non-zeroed ID.', + [TransactionFailureReason.ChainOutputImmutableFeaturesChanged]: + 'Immutable features in chain output modified during transition.', + [TransactionFailureReason.ImplicitAccountDestructionDisallowed]: + 'Cannot destroy implicit account; must be transitioned to account.', + [TransactionFailureReason.MultipleImplicitAccountCreationAddresses]: + 'Multiple implicit account creation addresses on the input side.', + [TransactionFailureReason.AccountInvalidFoundryCounter]: + 'Foundry counter in account decreased or did not increase by the number of new foundries.', + [TransactionFailureReason.FoundryTransitionWithoutAccount]: + 'Foundry output transitioned without accompanying account on input or output side.', + [TransactionFailureReason.FoundrySerialInvalid]: + 'Foundry output serial number is invalid.', + [TransactionFailureReason.DelegationCommitmentInputMissing]: + 'Delegation output validation requires a commitment input.', + [TransactionFailureReason.DelegationRewardInputMissing]: + 'Delegation output cannot be destroyed without a reward input.', + [TransactionFailureReason.DelegationRewardsClaimingInvalid]: + 'Invalid delegation mana rewards claiming.', + [TransactionFailureReason.DelegationOutputTransitionedTwice]: + 'Delegation output attempted to be transitioned twice.', + [TransactionFailureReason.DelegationModified]: + 'Delegated amount, validator ID and start epoch cannot be modified.', + [TransactionFailureReason.DelegationStartEpochInvalid]: + 'Invalid start epoch.', + [TransactionFailureReason.DelegationAmountMismatch]: + 'Delegated amount does not match amount.', + [TransactionFailureReason.DelegationEndEpochNotZero]: + 'End epoch must be set to zero at output genesis.', + [TransactionFailureReason.DelegationEndEpochInvalid]: + 'Delegation end epoch does not match current epoch.', + [TransactionFailureReason.CapabilitiesNativeTokenBurningNotAllowed]: + 'Native token burning is not allowed by the transaction capabilities.', + [TransactionFailureReason.CapabilitiesManaBurningNotAllowed]: + 'Mana burning is not allowed by the transaction capabilities.', + [TransactionFailureReason.CapabilitiesAccountDestructionNotAllowed]: + 'Account destruction is not allowed by the transaction capabilities.', + [TransactionFailureReason.CapabilitiesAnchorDestructionNotAllowed]: + 'Anchor destruction is not allowed by the transaction capabilities.', + [TransactionFailureReason.CapabilitiesFoundryDestructionNotAllowed]: + 'Foundry destruction is not allowed by the transaction capabilities.', + [TransactionFailureReason.CapabilitiesNftDestructionNotAllowed]: + 'NFT destruction is not allowed by the transaction capabilities.', [TransactionFailureReason.SemanticValidationFailed]: - 'The semantic validation failed for a reason not covered by the previous variants.', + 'Semantic validation failed.', }; diff --git a/bindings/nodejs/lib/types/wallet/wallet.ts b/bindings/nodejs/lib/types/wallet/wallet.ts index 83032273a2..af3953ee98 100644 --- a/bindings/nodejs/lib/types/wallet/wallet.ts +++ b/bindings/nodejs/lib/types/wallet/wallet.ts @@ -107,7 +107,7 @@ export interface SyncOptions { /// Try to sync transactions from incoming outputs with their inputs. Some data may not be obtained if it has been /// pruned. syncIncomingTransactions?: boolean; - /** Checks pending transactions and reissues them if necessary. Default: true. */ + /** Checks pending transactions. Default: true. */ syncPendingTransactions?: boolean; /** Specifies what outputs should be synced for the ed25519 address from the wallet. */ wallet?: WalletSyncOptions; diff --git a/bindings/python/iota_sdk/types/block/metadata.py b/bindings/python/iota_sdk/types/block/metadata.py index 99b0a2bab5..b511392e62 100644 --- a/bindings/python/iota_sdk/types/block/metadata.py +++ b/bindings/python/iota_sdk/types/block/metadata.py @@ -115,94 +115,143 @@ def __str__(self): class TransactionFailureReason(Enum): - """Represents the possible reasons for a conflicting transaction. - - Attributes: - InputUtxoAlreadySpent: The referenced UTXO was already spent. - ConflictingWithAnotherTx: The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason. - InvalidReferencedUtxo: The referenced UTXO is invalid. - InvalidTransaction: The transaction is invalid. - SumInputsOutputsAmountMismatch: The sum of the inputs and output base token amount does not match. - InvalidUnlockBlockSignature: The unlock block signature is invalid. - TimelockNotExpired: The configured timelock is not yet expired. - InvalidNativeTokens: The given native tokens are invalid. - StorageDepositReturnUnfulfilled: The return amount in a transaction is not fulfilled by the output side. - InvalidInputUnlock: An input unlock was invalid. - SenderNotUnlocked: The output contains a Sender with an ident (address) which is not unlocked. - InvalidChainStateTransition: The chain state transition is invalid. - InvalidTransactionIssuingTime: The referenced input is created after the transaction issuing time. - InvalidManaAmount: The mana amount is invalid. - InvalidBlockIssuanceCreditsAmount: The Block Issuance Credits amount is invalid. - InvalidRewardContextInput: Reward Context Input is invalid. - InvalidCommitmentContextInput: Commitment Context Input is invalid. - MissingStakingFeature: Staking Feature is not provided in account output when claiming rewards. - FailedToClaimStakingReward: Failed to claim staking reward. - FailedToClaimDelegationReward: Failed to claim delegation reward. - TransactionCapabilityNativeTokenBurningNotAllowed: Burning of native tokens is not allowed in the transaction capabilities. - TransactionCapabilityManaBurningNotAllowed: Burning of mana is not allowed in the transaction capabilities. - TransactionCapabilityAccountDestructionNotAllowed: Destruction of accounts is not allowed in the transaction capabilities. - TransactionCapabilityAnchorDestructionNotAllowed: Destruction of anchors is not allowed in the transaction capabilities. - TransactionCapabilityFoundryDestructionNotAllowed: Destruction of foundries is not allowed in the transaction capabilities. - TransactionCapabilityNftDestructionNotAllowed: Destruction of nfts is not allowed in the transaction capabilities. - SemanticValidationFailed: The semantic validation failed for a reason not covered by the previous variants. + """Represents the possible reasons for a failing transaction. """ - InputUtxoAlreadySpent = 1 - ConflictingWithAnotherTx = 2 - InvalidReferencedUtxo = 3 - InvalidTransaction = 4 - SumInputsOutputsAmountMismatch = 5 - InvalidUnlockBlockSignature = 6 - TimelockNotExpired = 7 - InvalidNativeTokens = 8 - StorageDepositReturnUnfulfilled = 9 - InvalidInputUnlock = 10 - SenderNotUnlocked = 11 - InvalidChainStateTransition = 12 - InvalidTransactionIssuingTime = 13 - InvalidManaAmount = 14 - InvalidBlockIssuanceCreditsAmount = 15 - InvalidRewardContextInput = 16 - InvalidCommitmentContextInput = 17 - MissingStakingFeature = 18 - FailedToClaimStakingReward = 19 - FailedToClaimDelegationReward = 20 - TransactionCapabilityNativeTokenBurningNotAllowed = 21 - TransactionCapabilityManaBurningNotAllowed = 22 - TransactionCapabilityAccountDestructionNotAllowed = 23 - TransactionCapabilityAnchorDestructionNotAllowed = 24 - TransactionCapabilityFoundryDestructionNotAllowed = 25 - TransactionCapabilityNftDestructionNotAllowed = 26 + Null = 0 + TypeInvalid = 1 + Conflicting = 2 + InputAlreadySpent = 3 + InputCreationAfterTxCreation = 4 + UnlockSignatureInvalid = 5 + CommitmentInputMissing = 6 + CommitmentInputReferenceInvalid = 7 + BicInputReferenceInvalid = 8 + RewardInputReferenceInvalid = 9 + StakingRewardCalculationFailure = 10 + DelegationRewardCalculationFailure = 11 + InputOutputBaseTokenMismatch = 12 + ManaOverflow = 13 + InputOutputManaMismatch = 14 + ManaDecayCreationIndexExceedsTargetIndex = 15 + NativeTokenAmountLessThanZero = 16 + NativeTokenSumExceedsUint256 = 17 + NativeTokenSumUnbalanced = 18 + MultiAddressLengthUnlockLengthMismatch = 19 + MultiAddressUnlockThresholdNotReached = 20 + NestedMultiUnlock = 21 + SenderFeatureNotUnlocked = 22 + IssuerFeatureNotUnlocked = 23 + StakingRewardInputMissing = 24 + StakingBlockIssuerFeatureMissing = 25 + StakingCommitmentInputMissing = 26 + StakingRewardClaimingInvalid = 27 + StakingFeatureRemovedBeforeUnbonding = 28 + StakingFeatureModifiedBeforeUnbonding = 29 + StakingStartEpochInvalid = 30 + StakingEndEpochTooEarly = 31 + BlockIssuerCommitmentInputMissing = 32 + BlockIssuanceCreditInputMissing = 33 + BlockIssuerNotExpired = 34 + BlockIssuerExpiryTooEarly = 35 + ManaMovedOffBlockIssuerAccount = 36 + AccountLocked = 37 + TimelockCommitmentInputMissing = 38 + TimelockNotExpired = 39 + ExpirationCommitmentInputMissing = 40 + ExpirationNotUnlockable = 41 + ReturnAmountNotFulFilled = 42 + NewChainOutputHasNonZeroedId = 43 + ChainOutputImmutableFeaturesChanged = 44 + ImplicitAccountDestructionDisallowed = 45 + MultipleImplicitAccountCreationAddresses = 46 + AccountInvalidFoundryCounter = 47 + FoundryTransitionWithoutAccount = 48 + FoundrySerialInvalid = 49 + DelegationCommitmentInputMissing = 50 + DelegationRewardInputMissing = 51 + DelegationRewardsClaimingInvalid = 52 + DelegationOutputTransitionedTwice = 53 + DelegationModified = 54 + DelegationStartEpochInvalid = 55 + DelegationAmountMismatch = 56 + DelegationEndEpochNotZero = 57 + DelegationEndEpochInvalid = 58 + CapabilitiesNativeTokenBurningNotAllowed = 59 + CapabilitiesManaBurningNotAllowed = 60 + CapabilitiesAccountDestructionNotAllowed = 61 + CapabilitiesAnchorDestructionNotAllowed = 62 + CapabilitiesFoundryDestructionNotAllowed = 63 + CapabilitiesNftDestructionNotAllowed = 64 SemanticValidationFailed = 255 def __str__(self): return { - 1: "The referenced UTXO was already spent.", - 2: "The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason.", - 3: "The referenced UTXO is invalid.", - 4: "The transaction is invalid.", - 5: "The sum of the inputs and output base token amount does not match.", - 6: "The unlock block signature is invalid.", - 7: "The configured timelock is not yet expired.", - 8: "The given native tokens are invalid.", - 9: "The return amount in a transaction is not fulfilled by the output side.", - 10: "An input unlock was invalid.", - 11: "The output contains a Sender with an ident (address) which is not unlocked.", - 12: "The chain state transition is invalid.", - 13: "The referenced input is created after the transaction issuing time.", - 14: "The mana amount is invalid.", - 15: "The Block Issuance Credits amount is invalid.", - 16: "Reward Context Input is invalid.", - 17: "Commitment Context Input is invalid.", - 18: "Staking Feature is not provided in account output when claiming rewards.", - 19: "Failed to claim staking reward.", - 20: "Failed to claim delegation reward.", - 21: "Burning of native tokens is not allowed in the transaction capabilities.", - 22: "Burning of mana is not allowed in the transaction capabilities.", - 23: "Destruction of accounts is not allowed in the transaction capabilities.", - 24: "Destruction of anchors is not allowed in the transaction capabilities.", - 25: "Destruction of foundries is not allowed in the transaction capabilities.", - 26: "Destruction of nfts is not allowed in the transaction capabilities.", - 255: "The semantic validation failed for a reason not covered by the previous variants." + 0: "Null.", + 1: "Transaction type is invalid.", + 2: "Transaction is conflicting.", + 3: "Input already spent.", + 4: "Input creation slot after tx creation slot.", + 5: "Signature in unlock is invalid.", + 6: "Commitment input required with reward or BIC input.", + 7: "Commitment input references an invalid or non-existent commitment.", + 8: "BIC input reference cannot be loaded.", + 9: "Reward input does not reference a staking account or a delegation output.", + 10: "Staking rewards could not be calculated due to storage issues or overflow.", + 11: "Delegation rewards could not be calculated due to storage issues or overflow.", + 12: "Inputs and outputs do not spend/deposit the same amount of base tokens.", + 13: "Under- or overflow in Mana calculations.", + 14: "Inputs and outputs do not contain the same amount of Mana.", + 15: "Mana decay creation slot/epoch index exceeds target slot/epoch index.", + 16: "Native token amount must be greater than zero.", + 17: "Native token sum exceeds max value of a uint256.", + 18: "Native token sums are unbalanced.", + 19: "Multi address length and multi unlock length do not match.", + 20: "Multi address unlock threshold not reached.", + 21: "Multi unlocks can't be nested.", + 22: "Sender feature is not unlocked.", + 23: "Issuer feature is not unlocked.", + 24: "Staking feature removal or resetting requires a reward input.", + 25: "Block issuer feature missing for account with staking feature.", + 26: "Staking feature validation requires a commitment input.", + 27: "Staking feature must be removed or reset in order to claim rewards.", + 28: "Staking feature can only be removed after the unbonding period.", + 29: "Staking start epoch, fixed cost and staked amount cannot be modified while bonded.", + 30: "Staking start epoch must be the epoch of the transaction.", + 31: "Staking end epoch must be set to the transaction epoch plus the unbonding period.", + 32: "Commitment input missing for block issuer feature.", + 33: "Block issuance credit input missing for account with block issuer feature.", + 34: "Block issuer feature has not expired.", + 35: "Block issuer feature expiry set too early.", + 36: "Mana cannot be moved off block issuer accounts except with manalocks.", + 37: "Account is locked due to negative block issuance credits.", + 38: "Transaction's containing a timelock condition require a commitment input.", + 39: "Timelock not expired.", + 40: "Transaction's containing an expiration condition require a commitment input.", + 41: "Expiration unlock condition cannot be unlocked.", + 42: "Return amount not fulfilled.", + 43: "New chain output has non-zeroed ID.", + 44: "Immutable features in chain output modified during transition.", + 45: "Cannot destroy implicit account; must be transitioned to account.", + 46: "Multiple implicit account creation addresses on the input side.", + 47: "Foundry counter in account decreased or did not increase by the number of new foundries.", + 48: "Foundry output transitioned without accompanying account on input or output side.", + 49: "Foundry output serial number is invalid.", + 50: "Delegation output validation requires a commitment input.", + 51: "Delegation output cannot be destroyed without a reward input.", + 52: "Invalid delegation mana rewards claiming.", + 53: "Delegation output attempted to be transitioned twice.", + 54: "Delegated amount, validator ID and start epoch cannot be modified.", + 55: "Invalid start epoch.", + 56: "Delegated amount does not match amount.", + 57: "End epoch must be set to zero at output genesis.", + 58: "Delegation end epoch does not match current epoch.", + 59: "Native token burning is not allowed by the transaction capabilities.", + 60: "Mana burning is not allowed by the transaction capabilities.", + 61: "Account destruction is not allowed by the transaction capabilities.", + 62: "Anchor destruction is not allowed by the transaction capabilities.", + 63: "Foundry destruction is not allowed by the transaction capabilities.", + 64: "NFT destruction is not allowed by the transaction capabilities.", + 255: "Semantic validation failed.", }[self.value] diff --git a/bindings/python/iota_sdk/wallet/sync_options.py b/bindings/python/iota_sdk/wallet/sync_options.py index 53be5ea9df..487cb7dac9 100644 --- a/bindings/python/iota_sdk/wallet/sync_options.py +++ b/bindings/python/iota_sdk/wallet/sync_options.py @@ -71,7 +71,7 @@ class SyncOptions(): Try to sync transactions from incoming outputs with their inputs. Some data may not be obtained if it has been pruned. sync_pending_transactions : - Checks pending transactions and reissues them if necessary. + Checks pending transactions. account : Specifies what outputs should be synced for the address of an account output. wallet : diff --git a/bindings/python/iota_sdk/wallet/wallet.py b/bindings/python/iota_sdk/wallet/wallet.py index 2e1169ca11..09d2023f31 100644 --- a/bindings/python/iota_sdk/wallet/wallet.py +++ b/bindings/python/iota_sdk/wallet/wallet.py @@ -620,12 +620,15 @@ def prepare_create_delegation(self, params: CreateDelegationParams, )) return PreparedCreateDelegationTransaction(self, prepared) - def delay_delegation_claiming(self, delegation_id: HexStr, reclaim_excess: bool) -> TransactionWithMetadata: + def delay_delegation_claiming( + self, delegation_id: HexStr, reclaim_excess: bool) -> TransactionWithMetadata: """Delay a delegation's claiming. """ - return self.prepare_delay_delegation_claiming(delegation_id, reclaim_excess).send() + return self.prepare_delay_delegation_claiming( + delegation_id, reclaim_excess).send() - def prepare_delay_delegation_claiming(self, delegation_id: HexStr, reclaim_excess: bool) -> PreparedTransaction: + def prepare_delay_delegation_claiming( + self, delegation_id: HexStr, reclaim_excess: bool) -> PreparedTransaction: """Prepare to delay a delegation's claiming. """ prepared = PreparedTransactionData.from_dict(self._call_method( diff --git a/cli/README.md b/cli/README.md index ba0ca8c71a..3223c6bead 100644 --- a/cli/README.md +++ b/cli/README.md @@ -7,9 +7,9 @@ Command line interface application for the [IOTA sdk wallet](https://github.com/ After downloading the CLI, initialize the signer for the wallet. On Mac and Linux you will first need to `chmod +x ./wallet`. ``` -./wallet init --node http://node.url:port --mnemonic MNEMONIC +./wallet init --node-url http://node.url:port --mnemonic MNEMONIC // Example: -./wallet init --node "http://localhost:8050" --mnemonic "giant dynamic museum toddler six deny defense ostrich bomb access mercy +./wallet init --node-url "http://localhost:8050" --mnemonic "giant dynamic museum toddler six deny defense ostrich bomb access mercy blood explain muscle shoot shallow glad autumn author calm heavy hawk abuse rally" ``` diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index bd84c85dea..52d8a90f00 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -58,7 +58,7 @@ serde = { version = "1.0.195", default-features = false, features = ["derive"] } serde_json = { version = "1.0.111", default-features = false, features = [ "alloc", ] } -strum = { version = "0.25.0", default-features = false, features = ["derive"] } +strum = { version = "0.26.1", default-features = false, features = ["derive"] } # Optional dependencies anymap = { version = "0.12.1", default-features = false, optional = true } diff --git a/sdk/examples/client/07_mqtt.rs b/sdk/examples/client/07_mqtt.rs index 7c59b5c480..d875979241 100644 --- a/sdk/examples/client/07_mqtt.rs +++ b/sdk/examples/client/07_mqtt.rs @@ -38,6 +38,7 @@ async fn main() -> Result<()> { client .subscribe( [ + Topic::new("commitments/latest")?, Topic::new("blocks")?, Topic::new(format!("outputs/unlock/address/{address}"))?, ], @@ -59,7 +60,7 @@ async fn main() -> Result<()> { _ = rx.recv() => { event_count += 1; if event_count == num_events { - client.unsubscribe([Topic::new("commitment-info/latest")?]).await?; + client.unsubscribe([Topic::new("commitments/latest")?]).await?; client.unsubscribe([Topic::new("blocks")?]).await?; client.unsubscribe([Topic::new(format!("outputs/unlock/address/{address}"))?]).await?; break; diff --git a/sdk/src/client/node_api/mqtt/types.rs b/sdk/src/client/node_api/mqtt/types.rs index 91adc8d921..0240c843f9 100644 --- a/sdk/src/client/node_api/mqtt/types.rs +++ b/sdk/src/client/node_api/mqtt/types.rs @@ -165,27 +165,31 @@ impl Topic { let valid_topics = lazy_static!( RegexSet::new([ // Commitment topics. - r"^commitment-info/latest$", - r"^commitment-info/finalized$", - r"^commitments$", + r"^commitments/latest$", + r"^commitments/finalized$", // Block topics. r"^blocks$", - r"^blocks/transaction$", - r"^blocks/transaction/tagged-data$", - r"^blocks/transaction/tagged-data/0x((?:[a-f0-9]{2}){1,64})$", - r"^blocks/tagged-data$", - r"^blocks/tagged-data/0x((?:[a-f0-9]{2}){1,64})$", - r"^block-metadata/0x([a-f0-9]{64})$", + r"^blocks/validation$", + r"^blocks/basic$", + r"^blocks/basic/tagged-data$", + r"^blocks/basic/tagged-data/0x((?:[a-f0-9]{2}){1,64})$", + r"^blocks/basic/transaction$", + r"^blocks/basic/transaction/tagged-data$", + r"^blocks/basic/transaction/tagged-data/0x((?:[a-f0-9]{2}){1,64})$", + // Transaction topics. + r"^transactions/0x([a-f0-9]{72})/included-block$", + r"^transaction-metadata/0x([a-f0-9]{72})$", + // Block metadata topics. + r"^block-metadata/0x([a-f0-9]{72})$", r"^block-metadata/accepted$", r"^block-metadata/confirmed$", - // Transaction topics. - r"^transactions/0x([a-f0-9]{64})/included-block$", // Output topics. - r"^outputs/0x([a-f0-9]{64})(\d{4})$", + r"^outputs/0x([a-f0-9]{76})$", r"^outputs/account/0x([a-f0-9]{64})$", r"^outputs/anchor/0x([a-f0-9]{64})$", - r"^outputs/nft/0x([a-f0-9]{64})$", r"^outputs/foundry/0x([a-f0-9]{76})$", + r"^outputs/nft/0x([a-f0-9]{64})$", + r"^outputs/delegation/0x([a-f0-9]{64})$", r"^outputs/unlock/(\+|address|storage-return|expiration|state-controller|governor|immutable-account)/[\x21-\x7E]{1,30}1[A-Za-z0-9]+(?:/spent)?$", ]).expect("cannot build regex set") => RegexSet); valid_topics.is_match(&self.0) diff --git a/sdk/src/client/secret/ledger_nano.rs b/sdk/src/client/secret/ledger_nano.rs index 2bfe919bcf..8daff56bb2 100644 --- a/sdk/src/client/secret/ledger_nano.rs +++ b/sdk/src/client/secret/ledger_nano.rs @@ -526,8 +526,8 @@ fn merge_unlocks( let slot_index = prepared_transaction_data .transaction .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + .commitment() + .map(|c| c.slot_index()); let transaction_signing_hash = prepared_transaction_data.transaction.signing_hash(); let mut merged_unlocks = Vec::new(); diff --git a/sdk/src/client/secret/mod.rs b/sdk/src/client/secret/mod.rs index d078c78373..be43098461 100644 --- a/sdk/src/client/secret/mod.rs +++ b/sdk/src/client/secret/mod.rs @@ -556,8 +556,8 @@ where let slot_index = prepared_transaction_data .transaction .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + .commitment() + .map(|c| c.slot_index()); // Assuming inputs_data is ordered by address type for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() { diff --git a/sdk/src/types/block/context_input/mod.rs b/sdk/src/types/block/context_input/mod.rs index 09ee7f9c9a..f172c8b93f 100644 --- a/sdk/src/types/block/context_input/mod.rs +++ b/sdk/src/types/block/context_input/mod.rs @@ -5,9 +5,12 @@ mod block_issuance_credit; mod commitment; mod reward; -use core::ops::RangeInclusive; +use alloc::{boxed::Box, vec::Vec}; +use core::{cmp::Ordering, ops::RangeInclusive}; -use derive_more::{Display, From}; +use derive_more::{Deref, Display, From}; +use iterator_sorted::is_unique_sorted_by; +use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable}; pub(crate) use self::reward::RewardContextInputIndex; pub use self::{ @@ -75,6 +78,90 @@ impl core::fmt::Debug for ContextInput { } } +pub(crate) type ContextInputCount = + BoundedU16<{ *CONTEXT_INPUT_COUNT_RANGE.start() }, { *CONTEXT_INPUT_COUNT_RANGE.end() }>; + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deref, Packable)] +#[packable(unpack_error = Error, with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidContextInputCount(p.into())))] +pub struct ContextInputs( + #[packable(verify_with = verify_context_inputs_packable)] BoxedSlicePrefix, +); + +impl TryFrom> for ContextInputs { + type Error = Error; + + #[inline(always)] + fn try_from(features: Vec) -> Result { + Self::from_vec(features) + } +} + +impl IntoIterator for ContextInputs { + type Item = ContextInput; + type IntoIter = alloc::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + Vec::from(Into::>::into(self.0)).into_iter() + } +} + +impl ContextInputs { + /// Creates a new [`ContextInputs`] from a vec. + pub fn from_vec(features: Vec) -> Result { + let mut context_inputs = + BoxedSlicePrefix::::try_from(features.into_boxed_slice()) + .map_err(Error::InvalidContextInputCount)?; + + context_inputs.sort_by(context_inputs_cmp); + // Sort is obviously fine now but uniqueness still needs to be checked. + verify_context_inputs(&context_inputs)?; + + Ok(Self(context_inputs)) + } + + /// Gets a reference to a [`CommitmentContextInput`], if any. + pub fn commitment(&self) -> Option<&CommitmentContextInput> { + self.0.iter().find_map(|c| c.as_commitment_opt()) + } + + /// Returns an iterator over [`BlockIssuanceCreditContextInput`], if any. + pub fn block_issuance_credits(&self) -> impl Iterator { + self.iter().filter_map(|c| c.as_block_issuance_credit_opt()) + } + + /// Returns an iterator over [`RewardContextInput`], if any. + pub fn rewards(&self) -> impl Iterator { + self.iter().filter_map(|c| c.as_reward_opt()) + } +} + +fn verify_context_inputs_packable(context_inputs: &[ContextInput]) -> Result<(), Error> { + if VERIFY { + verify_context_inputs(context_inputs)?; + } + Ok(()) +} + +fn context_inputs_cmp(a: &ContextInput, b: &ContextInput) -> Ordering { + a.kind().cmp(&b.kind()).then_with(|| match (a, b) { + (ContextInput::Commitment(_), ContextInput::Commitment(_)) => Ordering::Equal, + (ContextInput::BlockIssuanceCredit(a), ContextInput::BlockIssuanceCredit(b)) => { + a.account_id().cmp(b.account_id()) + } + (ContextInput::Reward(a), ContextInput::Reward(b)) => a.index().cmp(&b.index()), + // No need to evaluate all combinations as `then_with` is only called on Equal. + _ => unreachable!(), + }) +} + +fn verify_context_inputs(context_inputs: &[ContextInput]) -> Result<(), Error> { + if !is_unique_sorted_by(context_inputs.iter(), |a, b| context_inputs_cmp(a, b)) { + return Err(Error::ContextInputsNotUniqueSorted); + } + + Ok(()) +} + #[cfg(test)] mod tests { use pretty_assertions::assert_eq; diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index ad9174341c..5224424c40 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -15,7 +15,7 @@ use primitive_types::U256; use super::slot::EpochIndex; use crate::types::block::{ address::{AddressCapabilityFlag, WeightedAddressCount}, - context_input::RewardContextInputIndex, + context_input::{ContextInputCount, RewardContextInputIndex}, input::UtxoInput, mana::ManaAllotmentCount, output::{ @@ -26,7 +26,7 @@ use crate::types::block::{ }, payload::{ tagged_data::{TagLength, TaggedDataLength}, - ContextInputCount, InputCount, OutputCount, + InputCount, OutputCount, }, protocol::ProtocolParametersHash, unlock::{UnlockCount, UnlockIndex, UnlocksCount}, diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 6cec0cb8a8..062214b1d7 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -22,7 +22,7 @@ use crate::types::block::{ ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::StateTransitionError, + semantic::TransactionFailureReason, Error, }; @@ -399,9 +399,9 @@ impl AccountOutput { next_state: &Self, input_chains: &HashMap, outputs: &[Output], - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if current_state.immutable_features != next_state.immutable_features { - return Err(StateTransitionError::MutatedImmutableField); + return Err(TransactionFailureReason::ChainOutputImmutableFeaturesChanged); } // TODO update when TIP is updated @@ -437,12 +437,12 @@ impl AccountOutput { created_foundries_count += 1; if foundry.serial_number() != current_state.foundry_counter + created_foundries_count { - return Err(StateTransitionError::UnsortedCreatedFoundries); + return Err(TransactionFailureReason::FoundrySerialInvalid); } } if current_state.foundry_counter + created_foundries_count != next_state.foundry_counter { - return Err(StateTransitionError::InconsistentCreatedFoundriesCount); + return Err(TransactionFailureReason::AccountInvalidFoundryCounter); } Ok(()) diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 4852474489..02dd47722d 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -22,7 +22,7 @@ use crate::types::block::{ ChainId, MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{SemanticValidationContext, StateTransitionError, TransactionFailureReason}, + semantic::{SemanticValidationContext, TransactionFailureReason}, unlock::Unlock, Error, }; @@ -457,9 +457,9 @@ impl AnchorOutput { next_state: &Self, _input_chains: &HashMap, _outputs: &[Output], - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if current_state.immutable_features != next_state.immutable_features { - return Err(StateTransitionError::MutatedImmutableField); + return Err(TransactionFailureReason::ChainOutputImmutableFeaturesChanged); } if next_state.state_index == current_state.state_index + 1 { @@ -468,7 +468,8 @@ impl AnchorOutput { || current_state.governor_address() != next_state.governor_address() || current_state.features.metadata() != next_state.features.metadata() { - return Err(StateTransitionError::MutatedFieldWithoutRights); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } else if next_state.state_index == current_state.state_index { // Governance transition. @@ -476,13 +477,12 @@ impl AnchorOutput { // TODO https://github.com/iotaledger/iota-sdk/issues/1650 // || current_state.state_metadata != next_state.state_metadata { - return Err(StateTransitionError::MutatedFieldWithoutRights); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } else { - return Err(StateTransitionError::UnsupportedStateIndexOperation { - current_state: current_state.state_index, - next_state: next_state.state_index, - }); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } Ok(()) diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index 8ba475490d..27a3e8941e 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -16,7 +16,7 @@ use crate::types::block::{ MinimumOutputAmount, Output, OutputBuilderAmount, OutputId, StorageScore, StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::StateTransitionError, + semantic::TransactionFailureReason, slot::EpochIndex, Error, }; @@ -332,17 +332,16 @@ impl DelegationOutput { } // Transition, just without full SemanticValidationContext. - pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { - #[allow(clippy::nonminimal_bool)] - if !(current_state.delegation_id.is_null() && !next_state.delegation_id.is_null()) { - return Err(StateTransitionError::NonDelayedClaimingTransition); + pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), TransactionFailureReason> { + if !current_state.delegation_id.is_null() || next_state.delegation_id.is_null() { + return Err(TransactionFailureReason::DelegationOutputTransitionedTwice); } if current_state.delegated_amount != next_state.delegated_amount || current_state.start_epoch != next_state.start_epoch || current_state.validator_address != next_state.validator_address { - return Err(StateTransitionError::MutatedImmutableField); + return Err(TransactionFailureReason::DelegationModified); } Ok(()) diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index 66fa6789fd..3eb9e9affc 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -23,7 +23,7 @@ use crate::types::block::{ }, payload::signed_transaction::{TransactionCapabilities, TransactionCapabilityFlag}, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::{StateTransitionError, TransactionFailureReason}, + semantic::TransactionFailureReason, Error, }; @@ -408,12 +408,13 @@ impl FoundryOutput { input_native_tokens: &BTreeMap, output_native_tokens: &BTreeMap, capabilities: &TransactionCapabilities, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if current_state.account_address() != next_state.account_address() || current_state.serial_number != next_state.serial_number || current_state.immutable_features != next_state.immutable_features { - return Err(StateTransitionError::MutatedImmutableField); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::ChainOutputImmutableFeaturesChanged); } let token_id = next_state.token_id(); @@ -423,13 +424,15 @@ impl FoundryOutput { let TokenScheme::Simple(ref next_token_scheme) = next_state.token_scheme; if current_token_scheme.maximum_supply() != next_token_scheme.maximum_supply() { - return Err(StateTransitionError::MutatedImmutableField); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::ChainOutputImmutableFeaturesChanged); } if current_token_scheme.minted_tokens() > next_token_scheme.minted_tokens() || current_token_scheme.melted_tokens() > next_token_scheme.melted_tokens() { - return Err(StateTransitionError::NonMonotonicallyIncreasingNativeTokens); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } match input_tokens.cmp(&output_tokens) { @@ -442,11 +445,11 @@ impl FoundryOutput { let token_diff = output_tokens - input_tokens; if minted_diff != token_diff { - return Err(StateTransitionError::InconsistentNativeTokensMint); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } if current_token_scheme.melted_tokens() != next_token_scheme.melted_tokens() { - return Err(StateTransitionError::InconsistentNativeTokensMint); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } } Ordering::Equal => { @@ -455,7 +458,7 @@ impl FoundryOutput { if current_token_scheme.minted_tokens() != next_token_scheme.minted_tokens() || current_token_scheme.melted_tokens() != next_token_scheme.melted_tokens() { - return Err(StateTransitionError::InconsistentNativeTokensTransition); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } } Ordering::Greater => { @@ -464,7 +467,7 @@ impl FoundryOutput { if current_token_scheme.melted_tokens() != next_token_scheme.melted_tokens() && current_token_scheme.minted_tokens() != next_token_scheme.minted_tokens() { - return Err(StateTransitionError::InconsistentNativeTokensMeltBurn); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } // This can't underflow as it is known that current_melted_tokens <= next_melted_tokens. @@ -473,13 +476,13 @@ impl FoundryOutput { let token_diff = input_tokens - output_tokens; if melted_diff > token_diff { - return Err(StateTransitionError::InconsistentNativeTokensMeltBurn); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } let burned_diff = token_diff - melted_diff; if !burned_diff.is_zero() && !capabilities.has_capability(TransactionCapabilityFlag::BurnNativeTokens) { - return Err(TransactionFailureReason::TransactionCapabilityManaBurningNotAllowed)?; + return Err(TransactionFailureReason::CapabilitiesNativeTokenBurningNotAllowed)?; } } } diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index d641ee1f46..33d9da9802 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -22,7 +22,7 @@ use crate::types::block::{ StorageScoreParameters, }, protocol::{ProtocolParameters, WorkScore, WorkScoreParameters}, - semantic::StateTransitionError, + semantic::TransactionFailureReason, Error, }; @@ -407,10 +407,11 @@ impl NftOutput { } // Transition, just without full SemanticValidationContext - pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { + pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), TransactionFailureReason> { if current_state.immutable_features != next_state.immutable_features { - return Err(StateTransitionError::MutatedImmutableField); + return Err(TransactionFailureReason::ChainOutputImmutableFeaturesChanged); } + Ok(()) } } diff --git a/sdk/src/types/block/payload/mod.rs b/sdk/src/types/block/payload/mod.rs index f72f633450..4f1ba4c336 100644 --- a/sdk/src/types/block/payload/mod.rs +++ b/sdk/src/types/block/payload/mod.rs @@ -18,7 +18,7 @@ use packable::{ Packable, PackableExt, }; -pub(crate) use self::signed_transaction::{ContextInputCount, InputCount, OutputCount}; +pub(crate) use self::signed_transaction::{InputCount, OutputCount}; pub use self::{ candidacy_announcement::CandidacyAnnouncementPayload, signed_transaction::SignedTransactionPayload, tagged_data::TaggedDataPayload, diff --git a/sdk/src/types/block/payload/signed_transaction/mod.rs b/sdk/src/types/block/payload/signed_transaction/mod.rs index 39af5185a6..262315b4af 100644 --- a/sdk/src/types/block/payload/signed_transaction/mod.rs +++ b/sdk/src/types/block/payload/signed_transaction/mod.rs @@ -8,7 +8,7 @@ mod transaction_id; use packable::{Packable, PackableExt}; -pub(crate) use self::transaction::{ContextInputCount, InputCount, OutputCount}; +pub(crate) use self::transaction::{InputCount, OutputCount}; pub use self::{ transaction::{Transaction, TransactionBuilder, TransactionCapabilities, TransactionCapabilityFlag}, transaction_id::{TransactionHash, TransactionId, TransactionSigningHash}, diff --git a/sdk/src/types/block/payload/signed_transaction/transaction.rs b/sdk/src/types/block/payload/signed_transaction/transaction.rs index c66e5d2d81..85f274ff6e 100644 --- a/sdk/src/types/block/payload/signed_transaction/transaction.rs +++ b/sdk/src/types/block/payload/signed_transaction/transaction.rs @@ -2,17 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use alloc::{collections::BTreeSet, vec::Vec}; -use core::cmp::Ordering; use crypto::hashes::{blake2b::Blake2b256, Digest}; use hashbrown::HashSet; -use iterator_sorted::is_unique_sorted_by; use packable::{bounded::BoundedU16, prefix::BoxedSlicePrefix, Packable, PackableExt}; use crate::{ types::block::{ capabilities::{Capabilities, CapabilityFlag}, - context_input::{ContextInput, CONTEXT_INPUT_COUNT_RANGE}, + context_input::{ContextInput, ContextInputs}, input::{Input, INPUT_COUNT_RANGE}, mana::{verify_mana_allotments_sum, ManaAllotment, ManaAllotments}, output::{Output, OUTPUT_COUNT_RANGE}, @@ -128,7 +126,7 @@ impl TransactionBuilder { /// Finishes a [`TransactionBuilder`] into a [`Transaction`]. pub fn finish_with_params<'a>( - mut self, + self, params: impl Into>, ) -> Result { let params = params.into(); @@ -160,16 +158,6 @@ impl TransactionBuilder { }) .ok_or(Error::InvalidField("creation slot"))?; - self.context_inputs.sort_by(context_inputs_cmp); - - let context_inputs: BoxedSlicePrefix = self - .context_inputs - .into_boxed_slice() - .try_into() - .map_err(Error::InvalidContextInputCount)?; - - verify_context_inputs(&context_inputs)?; - let inputs: BoxedSlicePrefix = self .inputs .into_boxed_slice() @@ -199,7 +187,7 @@ impl TransactionBuilder { Ok(Transaction { network_id: self.network_id, creation_slot, - context_inputs, + context_inputs: ContextInputs::from_vec(self.context_inputs)?, inputs, allotments, capabilities: self.capabilities, @@ -215,8 +203,6 @@ impl TransactionBuilder { } } -pub(crate) type ContextInputCount = - BoundedU16<{ *CONTEXT_INPUT_COUNT_RANGE.start() }, { *CONTEXT_INPUT_COUNT_RANGE.end() }>; pub(crate) type InputCount = BoundedU16<{ *INPUT_COUNT_RANGE.start() }, { *INPUT_COUNT_RANGE.end() }>; pub(crate) type OutputCount = BoundedU16<{ *OUTPUT_COUNT_RANGE.start() }, { *OUTPUT_COUNT_RANGE.end() }>; @@ -230,9 +216,7 @@ pub struct Transaction { network_id: u64, /// The slot index in which the transaction was created. creation_slot: SlotIndex, - #[packable(verify_with = verify_context_inputs_packable)] - #[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidContextInputCount(p.into())))] - context_inputs: BoxedSlicePrefix, + context_inputs: ContextInputs, #[packable(verify_with = verify_inputs_packable)] #[packable(unpack_error_with = |e| e.unwrap_item_err_or_else(|p| Error::InvalidInputCount(p.into())))] inputs: BoxedSlicePrefix, @@ -262,7 +246,7 @@ impl Transaction { } /// Returns the context inputs of a [`Transaction`]. - pub fn context_inputs(&self) -> &[ContextInput] { + pub fn context_inputs(&self) -> &ContextInputs { &self.context_inputs } @@ -359,36 +343,6 @@ fn verify_network_id(network_id: &u64, visitor: &ProtocolPar Ok(()) } -fn verify_context_inputs_packable( - context_inputs: &[ContextInput], - _visitor: &ProtocolParameters, -) -> Result<(), Error> { - if VERIFY { - verify_context_inputs(context_inputs)?; - } - Ok(()) -} - -fn context_inputs_cmp(a: &ContextInput, b: &ContextInput) -> Ordering { - a.kind().cmp(&b.kind()).then_with(|| match (a, b) { - (ContextInput::Commitment(_), ContextInput::Commitment(_)) => Ordering::Equal, - (ContextInput::BlockIssuanceCredit(a), ContextInput::BlockIssuanceCredit(b)) => { - a.account_id().cmp(b.account_id()) - } - (ContextInput::Reward(a), ContextInput::Reward(b)) => a.index().cmp(&b.index()), - // No need to evaluate all combinations as `then_with` is only called on Equal. - _ => unreachable!(), - }) -} - -fn verify_context_inputs(context_inputs: &[ContextInput]) -> Result<(), Error> { - if !is_unique_sorted_by(context_inputs.iter(), |a, b| context_inputs_cmp(a, b)) { - return Err(Error::ContextInputsNotUniqueSorted); - } - - Ok(()) -} - fn verify_inputs(inputs: &[Input]) -> Result<(), Error> { let mut seen_utxos = HashSet::new(); diff --git a/sdk/src/types/block/semantic/error.rs b/sdk/src/types/block/semantic/error.rs index 3fbb99968d..f627f197dc 100644 --- a/sdk/src/types/block/semantic/error.rs +++ b/sdk/src/types/block/semantic/error.rs @@ -7,134 +7,227 @@ use crate::types::block::Error; /// Describes the reason of a transaction failure. #[repr(u8)] -#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, packable::Packable, strum::FromRepr)] #[cfg_attr(feature = "serde", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr))] #[packable(unpack_error = Error)] #[packable(tag_type = u8, with_error = Error::InvalidTransactionFailureReason)] #[non_exhaustive] pub enum TransactionFailureReason { - /// The referenced UTXO was already spent. - InputUtxoAlreadySpent = 1, - /// The transaction is conflicting with another transaction. Conflicting specifically means a double spend - /// situation that both transaction pass all validation rules, eventually losing one(s) should have this reason. - ConflictingWithAnotherTx = 2, - /// The referenced UTXO is invalid. - InvalidReferencedUtxo = 3, - /// The transaction is invalid. - InvalidTransaction = 4, - /// The sum of the inputs and output base token amount does not match. - SumInputsOutputsAmountMismatch = 5, - /// The unlock block signature is invalid. - InvalidUnlockBlockSignature = 6, - /// The configured timelock is not yet expired. - TimelockNotExpired = 7, - /// The given native tokens are invalid. - InvalidNativeTokens = 8, - /// The return amount in a transaction is not fulfilled by the output side. - StorageDepositReturnUnfulfilled = 9, - /// An input unlock was invalid. - InvalidInputUnlock = 10, - /// The output contains a Sender with an ident (address) which is not unlocked. - SenderNotUnlocked = 11, - /// The chain state transition is invalid. - InvalidChainStateTransition = 12, - /// The referenced input is created after transaction issuing time. - InvalidTransactionIssuingTime = 13, - /// The mana amount is invalid. - InvalidManaAmount = 14, - /// The Block Issuance Credits amount is invalid. - InvalidBlockIssuanceCreditsAmount = 15, - /// Reward Context Input is invalid. - InvalidRewardContextInput = 16, - /// Commitment Context Input is invalid. - InvalidCommitmentContextInput = 17, - /// Staking Feature is not provided in account output when claiming rewards. - MissingStakingFeature = 18, - /// Failed to claim staking reward. - FailedToClaimStakingReward = 19, - /// Failed to claim delegation reward. - FailedToClaimDelegationReward = 20, - /// Burning of native tokens is not allowed in the transaction capabilities. - TransactionCapabilityNativeTokenBurningNotAllowed = 21, - /// Burning of mana is not allowed in the transaction capabilities. - TransactionCapabilityManaBurningNotAllowed = 22, - /// Destruction of accounts is not allowed in the transaction capabilities. - TransactionCapabilityAccountDestructionNotAllowed = 23, - /// Destruction of anchors is not allowed in the transaction capabilities. - TransactionCapabilityAnchorDestructionNotAllowed = 24, - /// Destruction of foundries is not allowed in the transaction capabilities. - TransactionCapabilityFoundryDestructionNotAllowed = 25, - /// Destruction of nfts is not allowed in the transaction capabilities. - TransactionCapabilityNftDestructionNotAllowed = 26, - /// The semantic validation failed for a reason not covered by the previous variants. + None = 0, + TypeInvalid = 1, + Conflicting = 2, + InputAlreadySpent = 3, + InputCreationAfterTxCreation = 4, + UnlockSignatureInvalid = 5, + // TODO syntactic? https://github.com/iotaledger/iota-sdk/issues/1954 + CommitmentInputMissing = 6, + CommitmentInputReferenceInvalid = 7, + BicInputReferenceInvalid = 8, + RewardInputReferenceInvalid = 9, + StakingRewardCalculationFailure = 10, + DelegationRewardCalculationFailure = 11, + InputOutputBaseTokenMismatch = 12, + ManaOverflow = 13, + InputOutputManaMismatch = 14, + ManaDecayCreationIndexExceedsTargetIndex = 15, + NativeTokenAmountLessThanZero = 16, + NativeTokenSumExceedsUint256 = 17, + NativeTokenSumUnbalanced = 18, + MultiAddressLengthUnlockLengthMismatch = 19, + MultiAddressUnlockThresholdNotReached = 20, + // TODO remove? https://github.com/iotaledger/iota-sdk/issues/1954 + NestedMultiUnlock = 21, + SenderFeatureNotUnlocked = 22, + IssuerFeatureNotUnlocked = 23, + StakingRewardInputMissing = 24, + StakingBlockIssuerFeatureMissing = 25, + StakingCommitmentInputMissing = 26, + StakingRewardClaimingInvalid = 27, + StakingFeatureRemovedBeforeUnbonding = 28, + StakingFeatureModifiedBeforeUnbonding = 29, + StakingStartEpochInvalid = 30, + StakingEndEpochTooEarly = 31, + BlockIssuerCommitmentInputMissing = 32, + BlockIssuanceCreditInputMissing = 33, + BlockIssuerNotExpired = 34, + BlockIssuerExpiryTooEarly = 35, + ManaMovedOffBlockIssuerAccount = 36, + AccountLocked = 37, + TimelockCommitmentInputMissing = 38, + TimelockNotExpired = 39, + ExpirationCommitmentInputMissing = 40, + ExpirationNotUnlockable = 41, + ReturnAmountNotFulFilled = 42, + NewChainOutputHasNonZeroedId = 43, + ChainOutputImmutableFeaturesChanged = 44, + ImplicitAccountDestructionDisallowed = 45, + MultipleImplicitAccountCreationAddresses = 46, + AccountInvalidFoundryCounter = 47, + FoundryTransitionWithoutAccount = 48, + FoundrySerialInvalid = 49, + DelegationCommitmentInputMissing = 50, + DelegationRewardInputMissing = 51, + DelegationRewardsClaimingInvalid = 52, + DelegationOutputTransitionedTwice = 53, + DelegationModified = 54, + DelegationStartEpochInvalid = 55, + DelegationAmountMismatch = 56, + DelegationEndEpochNotZero = 57, + DelegationEndEpochInvalid = 58, + CapabilitiesNativeTokenBurningNotAllowed = 59, + CapabilitiesManaBurningNotAllowed = 60, + CapabilitiesAccountDestructionNotAllowed = 61, + CapabilitiesAnchorDestructionNotAllowed = 62, + CapabilitiesFoundryDestructionNotAllowed = 63, + CapabilitiesNftDestructionNotAllowed = 64, SemanticValidationFailed = 255, } impl fmt::Display for TransactionFailureReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InputUtxoAlreadySpent => write!(f, "The referenced UTXO was already spent."), - Self::ConflictingWithAnotherTx => write!( + Self::None => write!(f, "none."), + Self::TypeInvalid => write!(f, "transaction type is invalid."), + Self::Conflicting => write!(f, "transaction is conflicting."), + Self::InputAlreadySpent => write!(f, "input already spent."), + Self::InputCreationAfterTxCreation => write!(f, "input creation slot after tx creation slot."), + Self::UnlockSignatureInvalid => write!(f, "signature in unlock is invalid."), + Self::CommitmentInputMissing => write!(f, "commitment input required with reward or BIC input."), + Self::CommitmentInputReferenceInvalid => { + write!(f, "commitment input references an invalid or non-existent commitment.") + } + Self::BicInputReferenceInvalid => write!(f, "BIC input reference cannot be loaded."), + Self::RewardInputReferenceInvalid => write!( f, - "The transaction is conflicting with another transaction. Conflicting specifically means a double spend situation that both transactions pass all validation rules, eventually losing one(s) should have this reason." + "reward input does not reference a staking account or a delegation output." ), - Self::InvalidReferencedUtxo => write!(f, "The referenced UTXO is invalid."), - Self::InvalidTransaction => write!(f, "The transaction is invalid."), - Self::SumInputsOutputsAmountMismatch => { - write!(f, "The sum of the inputs and output base token amount does not match.") - } - Self::InvalidUnlockBlockSignature => write!(f, "The unlock block signature is invalid."), - Self::TimelockNotExpired => write!(f, "The configured timelock is not yet expired."), - Self::InvalidNativeTokens => write!(f, "The given native tokens are invalid."), - Self::StorageDepositReturnUnfulfilled => write!( + Self::StakingRewardCalculationFailure => write!( f, - "The return amount in a transaction is not fulfilled by the output side." + "staking rewards could not be calculated due to storage issues or overflow." ), - Self::InvalidInputUnlock => write!(f, "An input unlock was invalid."), - Self::SenderNotUnlocked => write!( + Self::DelegationRewardCalculationFailure => write!( f, - "The output contains a Sender with an ident (address) which is not unlocked." + "delegation rewards could not be calculated due to storage issues or overflow." ), - Self::InvalidChainStateTransition => write!(f, "The chain state transition is invalid."), - Self::InvalidTransactionIssuingTime => { - write!(f, "The referenced input is created after transaction issuing time.") - } - Self::InvalidManaAmount => write!(f, "The mana amount is invalid."), - Self::InvalidBlockIssuanceCreditsAmount => write!(f, "The Block Issuance Credits amount is invalid."), - Self::InvalidRewardContextInput => write!(f, "Reward Context Input is invalid."), - Self::InvalidCommitmentContextInput => write!(f, "Commitment Context Input is invalid."), - Self::MissingStakingFeature => write!( + Self::InputOutputBaseTokenMismatch => write!( f, - "Staking Feature is not provided in account output when claiming rewards." + "inputs and outputs do not spend/deposit the same amount of base tokens." ), - Self::FailedToClaimStakingReward => write!(f, "Failed to claim staking reward."), - Self::FailedToClaimDelegationReward => write!(f, "Failed to claim delegation reward."), - Self::TransactionCapabilityNativeTokenBurningNotAllowed => write!( + Self::ManaOverflow => write!(f, "under- or overflow in Mana calculations."), + Self::InputOutputManaMismatch => write!(f, "inputs and outputs do not contain the same amount of Mana."), + Self::ManaDecayCreationIndexExceedsTargetIndex => write!( f, - "Burning of native tokens is not allowed in the transaction capabilities." + "mana decay creation slot/epoch index exceeds target slot/epoch index." ), - Self::TransactionCapabilityManaBurningNotAllowed => { - write!(f, "Burning of mana is not allowed in the transaction capabilities.") + Self::NativeTokenAmountLessThanZero => write!(f, "native token amount must be greater than zero."), + Self::NativeTokenSumExceedsUint256 => write!(f, "native token sum exceeds max value of a uint256."), + Self::NativeTokenSumUnbalanced => write!(f, "native token sums are unbalanced."), + Self::MultiAddressLengthUnlockLengthMismatch => { + write!(f, "multi address length and multi unlock length do not match.") + } + Self::MultiAddressUnlockThresholdNotReached => write!(f, "multi address unlock threshold not reached."), + Self::NestedMultiUnlock => write!(f, "multi unlocks can't be nested."), + Self::SenderFeatureNotUnlocked => write!(f, "sender feature is not unlocked."), + Self::IssuerFeatureNotUnlocked => write!(f, "issuer feature is not unlocked."), + Self::StakingRewardInputMissing => { + write!(f, "staking feature removal or resetting requires a reward input.") + } + Self::StakingBlockIssuerFeatureMissing => { + write!(f, "block issuer feature missing for account with staking feature.") + } + Self::StakingCommitmentInputMissing => write!(f, "staking feature validation requires a commitment input."), + Self::StakingRewardClaimingInvalid => { + write!(f, "staking feature must be removed or reset in order to claim rewards.") } - Self::TransactionCapabilityAccountDestructionNotAllowed => write!( + Self::StakingFeatureRemovedBeforeUnbonding => { + write!(f, "staking feature can only be removed after the unbonding period.") + } + Self::StakingFeatureModifiedBeforeUnbonding => write!( + f, + "staking start epoch, fixed cost and staked amount cannot be modified while bonded." + ), + Self::StakingStartEpochInvalid => write!(f, "staking start epoch must be the epoch of the transaction."), + Self::StakingEndEpochTooEarly => write!( f, - "Destruction of accounts is not allowed in the transaction capabilities." + "staking end epoch must be set to the transaction epoch plus the unbonding period." ), - Self::TransactionCapabilityAnchorDestructionNotAllowed => write!( + Self::BlockIssuerCommitmentInputMissing => write!(f, "commitment input missing for block issuer feature."), + Self::BlockIssuanceCreditInputMissing => write!( f, - "Destruction of anchors is not allowed in the transaction capabilities." + "block issuance credit input missing for account with block issuer feature." ), - Self::TransactionCapabilityFoundryDestructionNotAllowed => write!( + Self::BlockIssuerNotExpired => write!(f, "block issuer feature has not expired."), + Self::BlockIssuerExpiryTooEarly => write!(f, "block issuer feature expiry set too early."), + Self::ManaMovedOffBlockIssuerAccount => write!( f, - "Destruction of foundries is not allowed in the transaction capabilities." + "mana cannot be moved off block issuer accounts except with manalocks." ), - Self::TransactionCapabilityNftDestructionNotAllowed => { - write!(f, "Destruction of nfts is not allowed in the transaction capabilities.") + Self::AccountLocked => write!(f, "account is locked due to negative block issuance credits."), + Self::TimelockCommitmentInputMissing => write!( + f, + "transaction's containing a timelock condition require a commitment input." + ), + Self::TimelockNotExpired => write!(f, "timelock not expired."), + Self::ExpirationCommitmentInputMissing => write!( + f, + "transaction's containing an expiration condition require a commitment input." + ), + Self::ExpirationNotUnlockable => write!(f, "expiration unlock condition cannot be unlocked."), + Self::ReturnAmountNotFulFilled => write!(f, "return amount not fulfilled."), + Self::NewChainOutputHasNonZeroedId => write!(f, "new chain output has non-zeroed ID."), + Self::ChainOutputImmutableFeaturesChanged => { + write!(f, "immutable features in chain output modified during transition.") + } + Self::ImplicitAccountDestructionDisallowed => { + write!(f, "cannot destroy implicit account; must be transitioned to account.") + } + Self::MultipleImplicitAccountCreationAddresses => { + write!(f, "multiple implicit account creation addresses on the input side.") } - Self::SemanticValidationFailed => write!( + Self::AccountInvalidFoundryCounter => write!( f, - "The semantic validation failed for a reason not covered by the previous variants." + "foundry counter in account decreased or did not increase by the number of new foundries." ), + Self::FoundryTransitionWithoutAccount => write!( + f, + "foundry output transitioned without accompanying account on input or output side." + ), + Self::FoundrySerialInvalid => write!(f, "foundry output serial number is invalid."), + Self::DelegationCommitmentInputMissing => { + write!(f, "delegation output validation requires a commitment input.") + } + Self::DelegationRewardInputMissing => { + write!(f, "delegation output cannot be destroyed without a reward input.") + } + Self::DelegationRewardsClaimingInvalid => write!(f, "invalid delegation mana rewards claiming."), + Self::DelegationOutputTransitionedTwice => { + write!(f, "delegation output attempted to be transitioned twice.") + } + Self::DelegationModified => write!(f, "delegated amount, validator ID and start epoch cannot be modified."), + Self::DelegationStartEpochInvalid => write!(f, "invalid start epoch."), + Self::DelegationAmountMismatch => write!(f, "delegated amount does not match amount."), + Self::DelegationEndEpochNotZero => write!(f, "end epoch must be set to zero at output genesis."), + Self::DelegationEndEpochInvalid => write!(f, "delegation end epoch does not match current epoch."), + Self::CapabilitiesNativeTokenBurningNotAllowed => write!( + f, + "native token burning is not allowed by the transaction capabilities." + ), + Self::CapabilitiesManaBurningNotAllowed => { + write!(f, "mana burning is not allowed by the transaction capabilities.") + } + Self::CapabilitiesAccountDestructionNotAllowed => { + write!(f, "account destruction is not allowed by the transaction capabilities.") + } + Self::CapabilitiesAnchorDestructionNotAllowed => { + write!(f, "anchor destruction is not allowed by the transaction capabilities.") + } + Self::CapabilitiesFoundryDestructionNotAllowed => { + write!(f, "foundry destruction is not allowed by the transaction capabilities.") + } + Self::CapabilitiesNftDestructionNotAllowed => { + write!(f, "NFT destruction is not allowed by the transaction capabilities.") + } + Self::SemanticValidationFailed => write!(f, "semantic validation failed."), } } } @@ -143,35 +236,6 @@ impl TryFrom for TransactionFailureReason { type Error = Error; fn try_from(c: u8) -> Result { - Ok(match c { - 1 => Self::InputUtxoAlreadySpent, - 2 => Self::ConflictingWithAnotherTx, - 3 => Self::InvalidReferencedUtxo, - 4 => Self::InvalidTransaction, - 5 => Self::SumInputsOutputsAmountMismatch, - 6 => Self::InvalidUnlockBlockSignature, - 7 => Self::TimelockNotExpired, - 8 => Self::InvalidNativeTokens, - 9 => Self::StorageDepositReturnUnfulfilled, - 10 => Self::InvalidInputUnlock, - 11 => Self::SenderNotUnlocked, - 12 => Self::InvalidChainStateTransition, - 13 => Self::InvalidTransactionIssuingTime, - 14 => Self::InvalidManaAmount, - 15 => Self::InvalidBlockIssuanceCreditsAmount, - 16 => Self::InvalidRewardContextInput, - 17 => Self::InvalidCommitmentContextInput, - 18 => Self::MissingStakingFeature, - 19 => Self::FailedToClaimStakingReward, - 20 => Self::FailedToClaimDelegationReward, - 21 => Self::TransactionCapabilityNativeTokenBurningNotAllowed, - 22 => Self::TransactionCapabilityManaBurningNotAllowed, - 23 => Self::TransactionCapabilityAccountDestructionNotAllowed, - 24 => Self::TransactionCapabilityAnchorDestructionNotAllowed, - 25 => Self::TransactionCapabilityFoundryDestructionNotAllowed, - 26 => Self::TransactionCapabilityNftDestructionNotAllowed, - 255 => Self::SemanticValidationFailed, - x => return Err(Self::Error::InvalidTransactionFailureReason(x)), - }) + Self::from_repr(c).ok_or(Self::Error::InvalidTransactionFailureReason(c)) } } diff --git a/sdk/src/types/block/semantic/mod.rs b/sdk/src/types/block/semantic/mod.rs index a559673f5d..ac30255a8e 100644 --- a/sdk/src/types/block/semantic/mod.rs +++ b/sdk/src/types/block/semantic/mod.rs @@ -10,10 +10,7 @@ use alloc::collections::BTreeMap; use hashbrown::{HashMap, HashSet}; use primitive_types::U256; -pub use self::{ - error::TransactionFailureReason, - state_transition::{StateTransitionError, StateTransitionVerifier}, -}; +pub use self::{error::TransactionFailureReason, state_transition::StateTransitionVerifier}; use crate::types::block::{ address::Address, context_input::{BlockIssuanceCreditContextInput, CommitmentContextInput, RewardContextInput}, @@ -111,12 +108,7 @@ impl<'a> SemanticValidationContext<'a> { // Validation of inputs. let mut has_implicit_account_creation_address = false; - self.commitment_context_input = self - .transaction - .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt()) - .copied(); + self.commitment_context_input = self.transaction.context_inputs().commitment().copied(); self.bic_context_input = self .transaction @@ -125,16 +117,11 @@ impl<'a> SemanticValidationContext<'a> { .find_map(|c| c.as_block_issuance_credit_opt()) .copied(); - for reward_context_input in self - .transaction - .context_inputs() - .iter() - .filter_map(|c| c.as_reward_opt()) - { + for reward_context_input in self.transaction.context_inputs().rewards() { if let Some(output_id) = self.inputs.get(reward_context_input.index() as usize).map(|v| v.0) { self.reward_context_inputs.insert(*output_id, *reward_context_input); } else { - return Ok(Some(TransactionFailureReason::InvalidRewardContextInput)); + return Ok(Some(TransactionFailureReason::RewardInputReferenceInvalid)); } } @@ -150,7 +137,7 @@ impl<'a> SemanticValidationContext<'a> { if unlock_conditions.addresses().any(Address::is_implicit_account_creation) { if has_implicit_account_creation_address { - return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); + return Ok(Some(TransactionFailureReason::MultipleImplicitAccountCreationAddresses)); } else { has_implicit_account_creation_address = true; } @@ -164,30 +151,31 @@ impl<'a> SemanticValidationContext<'a> { return Ok(Some(TransactionFailureReason::TimelockNotExpired)); } } else { - // Missing CommitmentContextInput - return Ok(Some(TransactionFailureReason::InvalidCommitmentContextInput)); + return Ok(Some(TransactionFailureReason::TimelockCommitmentInputMissing)); } } if let Some(expiration) = unlock_conditions.expiration() { if let Some(commitment_slot_index) = commitment_slot_index { - if expiration.is_expired(commitment_slot_index, self.protocol_parameters.committable_age_range()) - == Some(false) + match expiration.is_expired(commitment_slot_index, self.protocol_parameters.committable_age_range()) { - if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() { - let amount = self - .storage_deposit_returns - .entry(storage_deposit_return.return_address().clone()) - .or_default(); - - *amount = amount - .checked_add(storage_deposit_return.amount()) - .ok_or(Error::StorageDepositReturnOverflow)?; + Some(false) => { + if let Some(storage_deposit_return) = unlock_conditions.storage_deposit_return() { + let amount = self + .storage_deposit_returns + .entry(storage_deposit_return.return_address().clone()) + .or_default(); + + *amount = amount + .checked_add(storage_deposit_return.amount()) + .ok_or(Error::StorageDepositReturnOverflow)?; + } } + None => return Ok(Some(TransactionFailureReason::ExpirationNotUnlockable)), + _ => {} } } else { - // Missing CommitmentContextInput - return Ok(Some(TransactionFailureReason::InvalidCommitmentContextInput)); + return Ok(Some(TransactionFailureReason::ExpirationCommitmentInputMissing)); } } @@ -224,7 +212,7 @@ impl<'a> SemanticValidationContext<'a> { if let Some(unlocks) = self.unlocks { if unlocks.len() != self.inputs.len() { - return Ok(Some(TransactionFailureReason::InvalidInputUnlock)); + return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); } if let Err(conflict) = self.output_unlock(consumed_output, output_id, &unlocks[index]) { @@ -261,7 +249,7 @@ impl<'a> SemanticValidationContext<'a> { if let Some(sender) = features.and_then(|f| f.sender()) { if !self.unlocked_addresses.contains(sender.address()) { - return Ok(Some(TransactionFailureReason::SenderNotUnlocked)); + return Ok(Some(TransactionFailureReason::SenderFeatureNotUnlocked)); } } @@ -289,6 +277,7 @@ impl<'a> SemanticValidationContext<'a> { *native_token_amount = native_token_amount .checked_add(created_native_token.amount()) + // TODO should be a tx failure reason ? .ok_or(Error::CreatedNativeTokensAmountOverflow)?; } } @@ -297,27 +286,25 @@ impl<'a> SemanticValidationContext<'a> { for (return_address, return_amount) in self.storage_deposit_returns.iter() { if let Some(deposit_amount) = self.simple_deposits.get(return_address) { if deposit_amount < return_amount { - return Ok(Some(TransactionFailureReason::StorageDepositReturnUnfulfilled)); + return Ok(Some(TransactionFailureReason::ReturnAmountNotFulFilled)); } } else { - return Ok(Some(TransactionFailureReason::StorageDepositReturnUnfulfilled)); + return Ok(Some(TransactionFailureReason::ReturnAmountNotFulFilled)); } } // Validation of amounts. if self.input_amount != self.output_amount { - return Ok(Some(TransactionFailureReason::SumInputsOutputsAmountMismatch)); + return Ok(Some(TransactionFailureReason::InputOutputBaseTokenMismatch)); } if self.input_mana != self.output_mana { if self.input_mana > self.output_mana { if !self.transaction.has_capability(TransactionCapabilityFlag::BurnMana) { - return Ok(Some( - TransactionFailureReason::TransactionCapabilityManaBurningNotAllowed, - )); + return Ok(Some(TransactionFailureReason::CapabilitiesManaBurningNotAllowed)); } } else { - return Ok(Some(TransactionFailureReason::InvalidManaAmount)); + return Ok(Some(TransactionFailureReason::InputOutputManaMismatch)); } } @@ -333,39 +320,32 @@ impl<'a> SemanticValidationContext<'a> { .output_chains .contains_key(&ChainId::from(FoundryId::from(*token_id))) { - return Ok(Some(TransactionFailureReason::InvalidNativeTokens)); + return Ok(Some(TransactionFailureReason::NativeTokenSumUnbalanced)); } native_token_ids.insert(token_id); } if native_token_ids.len() > NativeTokens::COUNT_MAX as usize { - return Ok(Some(TransactionFailureReason::InvalidNativeTokens)); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Ok(Some(TransactionFailureReason::SemanticValidationFailed)); } // Validation of state transitions and destructions. for (chain_id, current_state) in self.input_chains.iter() { - match self.verify_state_transition( + if let Err(e) = self.verify_state_transition( Some(*current_state), self.output_chains.get(chain_id).map(|(id, o)| (id, *o)), ) { - Err(StateTransitionError::TransactionFailure(f)) => return Ok(Some(f)), - Err(_) => { - return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); - } - _ => {} + return Ok(Some(e)); } } // Validation of state creations. for (chain_id, next_state) in self.output_chains.iter() { if self.input_chains.get(chain_id).is_none() { - match self.verify_state_transition(None, Some((&next_state.0, next_state.1))) { - Err(StateTransitionError::TransactionFailure(f)) => return Ok(Some(f)), - Err(_) => { - return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); - } - _ => {} + if let Err(e) = self.verify_state_transition(None, Some((&next_state.0, next_state.1))) { + return Ok(Some(e)); } } } diff --git a/sdk/src/types/block/semantic/state_transition.rs b/sdk/src/types/block/semantic/state_transition.rs index 5e1ccc5841..87a1259095 100644 --- a/sdk/src/types/block/semantic/state_transition.rs +++ b/sdk/src/types/block/semantic/state_transition.rs @@ -10,44 +10,6 @@ use crate::types::block::{ semantic::{SemanticValidationContext, TransactionFailureReason}, }; -/// -#[allow(missing_docs)] -#[derive(Debug, Eq, PartialEq)] -pub enum StateTransitionError { - InconsistentCreatedFoundriesCount, - InconsistentFoundrySerialNumber, - InconsistentNativeTokensFoundryCreation, - InconsistentNativeTokensFoundryDestruction, - InconsistentNativeTokensMint, - InconsistentNativeTokensTransition, - InconsistentNativeTokensMeltBurn, - InvalidDelegatedAmount, - InvalidBlockIssuerTransition, - IssuerNotUnlocked, - MissingAccountForFoundry, - MissingCommitmentContextInput, - MissingRewardInput, - MutatedFieldWithoutRights, - MutatedImmutableField, - NonDelayedClaimingTransition, - NonMonotonicallyIncreasingNativeTokens, - NonZeroCreatedId, - NonZeroCreatedFoundryCounter, - NonZeroCreatedStateIndex, - NonZeroDelegationEndEpoch, - UnsortedCreatedFoundries, - UnsupportedStateIndexOperation { current_state: u32, next_state: u32 }, - UnsupportedStateTransition, - TransactionFailure(TransactionFailureReason), - ZeroCreatedId, -} - -impl From for StateTransitionError { - fn from(error: TransactionFailureReason) -> Self { - Self::TransactionFailure(error) - } -} - /// pub trait StateTransitionVerifier { /// @@ -55,7 +17,7 @@ pub trait StateTransitionVerifier { output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError>; + ) -> Result<(), TransactionFailureReason>; /// fn transition( @@ -64,14 +26,14 @@ pub trait StateTransitionVerifier { next_output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError>; + ) -> Result<(), TransactionFailureReason>; /// fn destruction( output_id: &OutputId, current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError>; + ) -> Result<(), TransactionFailureReason>; } impl SemanticValidationContext<'_> { @@ -80,7 +42,7 @@ impl SemanticValidationContext<'_> { &self, current_state: Option<(&OutputId, &Output)>, next_state: Option<(&OutputId, &Output)>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { match (current_state, next_state) { // Creations. (None, Some((output_id, Output::Account(next_state)))) => { @@ -102,7 +64,7 @@ impl SemanticValidationContext<'_> { if current_state.is_implicit_account() { BasicOutput::implicit_account_transition(current_state, next_state, self) } else { - Err(StateTransitionError::UnsupportedStateTransition) + Err(TransactionFailureReason::SemanticValidationFailed) } } ( @@ -123,6 +85,13 @@ impl SemanticValidationContext<'_> { ) => DelegationOutput::transition(current_output_id, current_state, next_output_id, next_state, self), // Destructions. + (Some((_output_id, Output::Basic(current_state))), None) => { + if current_state.is_implicit_account() { + Err(TransactionFailureReason::ImplicitAccountDestructionDisallowed) + } else { + Err(TransactionFailureReason::SemanticValidationFailed) + } + } (Some((output_id, Output::Account(current_state))), None) => { AccountOutput::destruction(output_id, current_state, self) } @@ -137,7 +106,7 @@ impl SemanticValidationContext<'_> { } // Unsupported. - _ => Err(StateTransitionError::UnsupportedStateTransition), + _ => Err(TransactionFailureReason::SemanticValidationFailed), } } } @@ -147,9 +116,10 @@ impl BasicOutput { _current_state: &Self, next_state: &AccountOutput, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if next_state.account_id().is_null() { - return Err(StateTransitionError::ZeroCreatedId); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } if let Some(_block_issuer) = next_state.features().block_issuer() { @@ -158,12 +128,13 @@ impl BasicOutput { // account contained a Block Issuer Feature with its Expiry Slot set to the maximum value of // slot indices and the feature was transitioned. } else { - return Err(StateTransitionError::InvalidBlockIssuerTransition); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } if let Some(issuer) = next_state.immutable_features().issuer() { if !context.unlocked_addresses.contains(issuer.address()) { - return Err(StateTransitionError::IssuerNotUnlocked); + return Err(TransactionFailureReason::IssuerFeatureNotUnlocked); } } @@ -176,14 +147,14 @@ impl StateTransitionVerifier for AccountOutput { _output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !next_state.account_id().is_null() { - return Err(StateTransitionError::NonZeroCreatedId); + return Err(TransactionFailureReason::NewChainOutputHasNonZeroedId); } if let Some(issuer) = next_state.immutable_features().issuer() { if !context.unlocked_addresses.contains(issuer.address()) { - return Err(StateTransitionError::IssuerNotUnlocked); + return Err(TransactionFailureReason::IssuerFeatureNotUnlocked); } } @@ -196,7 +167,7 @@ impl StateTransitionVerifier for AccountOutput { _next_output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { Self::transition_inner( current_state, next_state, @@ -209,12 +180,12 @@ impl StateTransitionVerifier for AccountOutput { _output_id: &OutputId, _current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !context .transaction .has_capability(TransactionCapabilityFlag::DestroyAccountOutputs) { - return Err(TransactionFailureReason::TransactionCapabilityAccountDestructionNotAllowed)?; + return Err(TransactionFailureReason::CapabilitiesAccountDestructionNotAllowed)?; } Ok(()) } @@ -225,14 +196,14 @@ impl StateTransitionVerifier for AnchorOutput { _output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !next_state.anchor_id().is_null() { - return Err(StateTransitionError::NonZeroCreatedId); + return Err(TransactionFailureReason::NewChainOutputHasNonZeroedId); } if let Some(issuer) = next_state.immutable_features().issuer() { if !context.unlocked_addresses.contains(issuer.address()) { - return Err(StateTransitionError::IssuerNotUnlocked); + return Err(TransactionFailureReason::IssuerFeatureNotUnlocked); } } @@ -245,7 +216,7 @@ impl StateTransitionVerifier for AnchorOutput { _next_output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { Self::transition_inner( current_state, next_state, @@ -258,13 +229,13 @@ impl StateTransitionVerifier for AnchorOutput { _output_id: &OutputId, _current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !context .transaction .capabilities() .has_capability(TransactionCapabilityFlag::DestroyAnchorOutputs) { - return Err(TransactionFailureReason::TransactionCapabilityAccountDestructionNotAllowed)?; + return Err(TransactionFailureReason::CapabilitiesAnchorDestructionNotAllowed)?; } Ok(()) } @@ -275,7 +246,7 @@ impl StateTransitionVerifier for FoundryOutput { _output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { let account_chain_id = ChainId::from(*next_state.account_address().account_id()); if let (Some((_, Output::Account(input_account))), Some((_, Output::Account(output_account)))) = ( @@ -285,10 +256,10 @@ impl StateTransitionVerifier for FoundryOutput { if input_account.foundry_counter() >= next_state.serial_number() || next_state.serial_number() > output_account.foundry_counter() { - return Err(StateTransitionError::InconsistentFoundrySerialNumber); + return Err(TransactionFailureReason::FoundrySerialInvalid); } } else { - return Err(StateTransitionError::MissingAccountForFoundry); + return Err(TransactionFailureReason::FoundryTransitionWithoutAccount); } let token_id = next_state.token_id(); @@ -297,11 +268,11 @@ impl StateTransitionVerifier for FoundryOutput { // No native tokens should be referenced prior to the foundry creation. if context.input_native_tokens.contains_key(&token_id) { - return Err(StateTransitionError::InconsistentNativeTokensFoundryCreation); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } if output_tokens != next_token_scheme.minted_tokens() || !next_token_scheme.melted_tokens().is_zero() { - return Err(StateTransitionError::InconsistentNativeTokensFoundryCreation); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } Ok(()) @@ -313,7 +284,7 @@ impl StateTransitionVerifier for FoundryOutput { _next_output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { Self::transition_inner( current_state, next_state, @@ -327,12 +298,12 @@ impl StateTransitionVerifier for FoundryOutput { _output_id: &OutputId, current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !context .transaction .has_capability(TransactionCapabilityFlag::DestroyFoundryOutputs) { - return Err(TransactionFailureReason::TransactionCapabilityFoundryDestructionNotAllowed)?; + return Err(TransactionFailureReason::CapabilitiesFoundryDestructionNotAllowed)?; } let token_id = current_state.token_id(); @@ -341,14 +312,14 @@ impl StateTransitionVerifier for FoundryOutput { // No native tokens should be referenced after the foundry destruction. if context.output_native_tokens.contains_key(&token_id) { - return Err(StateTransitionError::InconsistentNativeTokensFoundryDestruction); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } // This can't underflow as it is known that minted_tokens >= melted_tokens (syntactic rule). let minted_melted_diff = current_token_scheme.minted_tokens() - current_token_scheme.melted_tokens(); if minted_melted_diff != input_tokens { - return Err(StateTransitionError::InconsistentNativeTokensFoundryDestruction); + return Err(TransactionFailureReason::NativeTokenSumUnbalanced); } Ok(()) @@ -360,14 +331,14 @@ impl StateTransitionVerifier for NftOutput { _output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !next_state.nft_id().is_null() { - return Err(StateTransitionError::NonZeroCreatedId); + return Err(TransactionFailureReason::NewChainOutputHasNonZeroedId); } if let Some(issuer) = next_state.immutable_features().issuer() { if !context.unlocked_addresses.contains(issuer.address()) { - return Err(StateTransitionError::IssuerNotUnlocked); + return Err(TransactionFailureReason::IssuerFeatureNotUnlocked); } } @@ -380,7 +351,7 @@ impl StateTransitionVerifier for NftOutput { _next_output_id: &OutputId, next_state: &Self, _context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { Self::transition_inner(current_state, next_state) } @@ -388,12 +359,12 @@ impl StateTransitionVerifier for NftOutput { _output_id: &OutputId, _current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { if !context .transaction .has_capability(TransactionCapabilityFlag::DestroyNftOutputs) { - return Err(TransactionFailureReason::TransactionCapabilityNftDestructionNotAllowed)?; + return Err(TransactionFailureReason::CapabilitiesNftDestructionNotAllowed)?; } Ok(()) } @@ -404,31 +375,28 @@ impl StateTransitionVerifier for DelegationOutput { _output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { let protocol_parameters = &context.protocol_parameters; if !next_state.delegation_id().is_null() { - return Err(StateTransitionError::NonZeroCreatedId); + return Err(TransactionFailureReason::NewChainOutputHasNonZeroedId); } if next_state.amount() != next_state.delegated_amount() { - return Err(StateTransitionError::InvalidDelegatedAmount); + return Err(TransactionFailureReason::DelegationAmountMismatch); } if next_state.end_epoch() != 0 { - return Err(StateTransitionError::NonZeroDelegationEndEpoch); + return Err(TransactionFailureReason::DelegationEndEpochNotZero); } let slot_commitment_id = context .commitment_context_input .map(|c| c.slot_commitment_id()) - .ok_or(StateTransitionError::MissingCommitmentContextInput)?; + .ok_or(TransactionFailureReason::DelegationCommitmentInputMissing)?; if next_state.start_epoch() != protocol_parameters.delegation_start_epoch(slot_commitment_id) { - // TODO: specific tx failure reason https://github.com/iotaledger/iota-core/issues/679 - return Err(StateTransitionError::TransactionFailure( - TransactionFailureReason::SemanticValidationFailed, - )); + return Err(TransactionFailureReason::DelegationStartEpochInvalid); } Ok(()) @@ -440,7 +408,7 @@ impl StateTransitionVerifier for DelegationOutput { _next_output_id: &OutputId, next_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { + ) -> Result<(), TransactionFailureReason> { Self::transition_inner(current_state, next_state)?; let protocol_parameters = &context.protocol_parameters; @@ -448,10 +416,10 @@ impl StateTransitionVerifier for DelegationOutput { let slot_commitment_id = context .commitment_context_input .map(|c| c.slot_commitment_id()) - .ok_or(StateTransitionError::MissingCommitmentContextInput)?; + .ok_or(TransactionFailureReason::DelegationCommitmentInputMissing)?; if next_state.end_epoch() != protocol_parameters.delegation_end_epoch(slot_commitment_id) { - return Err(StateTransitionError::NonDelayedClaimingTransition); + return Err(TransactionFailureReason::DelegationEndEpochInvalid); } Ok(()) @@ -461,13 +429,13 @@ impl StateTransitionVerifier for DelegationOutput { output_id: &OutputId, _current_state: &Self, context: &SemanticValidationContext<'_>, - ) -> Result<(), StateTransitionError> { - // If a mana reward was provided but no reward context input exists - if context.mana_rewards.get(output_id).is_some() && !context.reward_context_inputs.contains_key(output_id) { - return Err(StateTransitionError::MissingRewardInput); + ) -> Result<(), TransactionFailureReason> { + if !context.mana_rewards.contains_key(output_id) || !context.reward_context_inputs.contains_key(output_id) { + return Err(TransactionFailureReason::DelegationRewardInputMissing); } + if context.commitment_context_input.is_none() { - return Err(StateTransitionError::MissingCommitmentContextInput); + return Err(TransactionFailureReason::DelegationCommitmentInputMissing); } Ok(()) diff --git a/sdk/src/types/block/semantic/unlock.rs b/sdk/src/types/block/semantic/unlock.rs index 814848e448..f6ff3ed2cc 100644 --- a/sdk/src/types/block/semantic/unlock.rs +++ b/sdk/src/types/block/semantic/unlock.rs @@ -15,7 +15,7 @@ impl SemanticValidationContext<'_> { match (address, unlock) { (Address::Ed25519(ed25519_address), Unlock::Signature(unlock)) => { if self.unlocked_addresses.contains(address) { - return Err(TransactionFailureReason::InvalidInputUnlock); + return Err(TransactionFailureReason::SemanticValidationFailed); } let Signature::Ed25519(signature) = unlock.signature(); @@ -24,7 +24,7 @@ impl SemanticValidationContext<'_> { .is_valid(self.transaction_signing_hash.as_ref(), ed25519_address) .is_err() { - return Err(TransactionFailureReason::InvalidUnlockBlockSignature); + return Err(TransactionFailureReason::UnlockSignatureInvalid); } self.unlocked_addresses.insert(address.clone()); @@ -32,36 +32,41 @@ impl SemanticValidationContext<'_> { (Address::Ed25519(_), Unlock::Reference(_)) => { // TODO actually check that it was unlocked by the same signature. if !self.unlocked_addresses.contains(address) { - return Err(TransactionFailureReason::InvalidInputUnlock); + return Err(TransactionFailureReason::SemanticValidationFailed); } } (Address::Account(account_address), Unlock::Account(unlock)) => { // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. if let (output_id, Output::Account(account_output)) = self.inputs[unlock.index() as usize] { if &account_output.account_id_non_null(output_id) != account_address.account_id() { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } if !self.unlocked_addresses.contains(address) { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } else { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } (Address::Nft(nft_address), Unlock::Nft(unlock)) => { // PANIC: indexing is fine as it is already syntactically verified that indexes reference below. if let (output_id, Output::Nft(nft_output)) = self.inputs[unlock.index() as usize] { if &nft_output.nft_id_non_null(output_id) != nft_address.nft_id() { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } if !self.unlocked_addresses.contains(address) { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } else { - return Err(TransactionFailureReason::InvalidInputUnlock); + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + return Err(TransactionFailureReason::SemanticValidationFailed); } } - // TODO maybe shouldn't be a semantic error but this function currently returns a TransactionFailureReason. (Address::Anchor(_), _) => return Err(TransactionFailureReason::SemanticValidationFailed), (Address::ImplicitAccountCreation(implicit_account_creation_address), _) => { return self.address_unlock( @@ -71,7 +76,7 @@ impl SemanticValidationContext<'_> { } (Address::Multi(multi_address), Unlock::Multi(unlock)) => { if multi_address.len() != unlock.len() { - return Err(TransactionFailureReason::InvalidInputUnlock); + return Err(TransactionFailureReason::MultiAddressLengthUnlockLengthMismatch); } let mut cumulative_unlocked_weight = 0u16; @@ -84,13 +89,14 @@ impl SemanticValidationContext<'_> { } if cumulative_unlocked_weight < multi_address.threshold() { - return Err(TransactionFailureReason::InvalidInputUnlock); + return Err(TransactionFailureReason::MultiAddressUnlockThresholdNotReached); } } (Address::Restricted(restricted_address), _) => { return self.address_unlock(restricted_address.address(), unlock); } - _ => return Err(TransactionFailureReason::InvalidInputUnlock), + // TODO https://github.com/iotaledger/iota-sdk/issues/1954 + _ => return Err(TransactionFailureReason::SemanticValidationFailed), } Ok(()) @@ -104,11 +110,7 @@ impl SemanticValidationContext<'_> { ) -> Result<(), TransactionFailureReason> { match output { Output::Basic(output) => { - let slot_index = self - .transaction - .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + let slot_index = self.transaction.context_inputs().commitment().map(|c| c.slot_index()); let locked_address = output .unlock_conditions() .locked_address( @@ -116,9 +118,8 @@ impl SemanticValidationContext<'_> { slot_index, self.protocol_parameters.committable_age_range(), ) - .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? - // because of expiration the input can't be unlocked at this time - .ok_or(TransactionFailureReason::SemanticValidationFailed)?; + .map_err(|_| TransactionFailureReason::ExpirationCommitmentInputMissing)? + .ok_or(TransactionFailureReason::ExpirationNotUnlockable)?; self.address_unlock(locked_address, unlock)?; } @@ -139,11 +140,7 @@ impl SemanticValidationContext<'_> { // Output::Anchor(_) => return Err(Error::UnsupportedOutputKind(AnchorOutput::KIND)), Output::Foundry(output) => self.address_unlock(&Address::from(*output.account_address()), unlock)?, Output::Nft(output) => { - let slot_index = self - .transaction - .context_inputs() - .iter() - .find_map(|c| c.as_commitment_opt().map(|c| c.slot_index())); + let slot_index = self.transaction.context_inputs().commitment().map(|c| c.slot_index()); let locked_address = output .unlock_conditions() .locked_address( @@ -151,9 +148,8 @@ impl SemanticValidationContext<'_> { slot_index, self.protocol_parameters.committable_age_range(), ) - .map_err(|_| TransactionFailureReason::InvalidCommitmentContextInput)? - // because of expiration the input can't be unlocked at this time - .ok_or(TransactionFailureReason::SemanticValidationFailed)?; + .map_err(|_| TransactionFailureReason::ExpirationCommitmentInputMissing)? + .ok_or(TransactionFailureReason::ExpirationNotUnlockable)?; self.address_unlock(locked_address, unlock)?; diff --git a/sdk/src/wallet/operations/balance.rs b/sdk/src/wallet/operations/balance.rs index 56a8d28a80..e9efdbfd80 100644 --- a/sdk/src/wallet/operations/balance.rs +++ b/sdk/src/wallet/operations/balance.rs @@ -69,6 +69,10 @@ where output_id.transaction_id().slot_index(), slot_index, )?; + // Add mana rewards + if let Ok(response) = self.client().get_output_mana_rewards(output_id, slot_index).await { + balance.mana.rewards += response.rewards; + } // Add storage deposit balance.required_storage_deposit.account += storage_cost; @@ -98,6 +102,10 @@ where Output::Delegation(delegation) => { // Add amount balance.base_coin.total += delegation.amount(); + // Add mana rewards + if let Ok(response) = self.client().get_output_mana_rewards(output_id, slot_index).await { + balance.mana.rewards += response.rewards; + } // Add storage deposit balance.required_storage_deposit.delegation += storage_cost; if !wallet_data.locked_outputs.contains(output_id) { diff --git a/sdk/src/wallet/operations/syncing/outputs.rs b/sdk/src/wallet/operations/syncing/outputs.rs index 39e11fd735..c317953024 100644 --- a/sdk/src/wallet/operations/syncing/outputs.rs +++ b/sdk/src/wallet/operations/syncing/outputs.rs @@ -69,10 +69,12 @@ where for output_id in output_ids { match wallet_data.outputs.get_mut(&output_id) { - // set unspent + // set unspent if not already Some(output_data) => { - log::warn!("Removing spent output metadata for {output_id}, because it's still unspent"); - output_data.metadata.spent = None; + if output_data.is_spent() { + log::warn!("Removing spent output metadata for {output_id}, because it's still unspent"); + output_data.metadata.spent = None; + } unspent_outputs.push((output_id, output_data.clone())); outputs.push(OutputWithMetadata::new( output_data.output.clone(), diff --git a/sdk/src/wallet/operations/syncing/transactions.rs b/sdk/src/wallet/operations/syncing/transactions.rs index 6e380dc0ad..b5ab3a80ee 100644 --- a/sdk/src/wallet/operations/syncing/transactions.rs +++ b/sdk/src/wallet/operations/syncing/transactions.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - client::{secret::SecretManage, unix_timestamp_now}, + client::secret::SecretManage, types::{ api::core::TransactionState, block::{input::Input, output::OutputId, BlockId}, @@ -15,7 +15,7 @@ use crate::{ }; // ignore outputs and transactions from other networks -// check if outputs are unspent, rebroadcast, reissue... +// check if outputs are unspent // also revalidate that the locked outputs needs to be there, maybe there was a conflict or the transaction got // confirmed, then they should get removed @@ -24,7 +24,7 @@ where crate::wallet::Error: From, crate::client::Error: From, { - /// Sync transactions and reissue them if unconfirmed. Returns the transaction with updated metadata and spent + /// Sync transactions. Returns the transaction with updated metadata and spent /// output ids that don't need to be locked anymore /// Return true if a transaction got confirmed for which we don't have an output already, based on this outputs will /// be synced again @@ -47,11 +47,10 @@ where // Inputs from conflicting transactions that are unspent, but should be removed from the locked outputs so they // are available again let mut output_ids_to_unlock = Vec::new(); - let mut transactions_to_reissue = Vec::new(); for transaction_id in &wallet_data.pending_transactions { log::debug!("[SYNC] sync pending transaction {transaction_id}"); - let transaction = wallet_data + let mut transaction = wallet_data .transactions .get(transaction_id) // panic during development to easier detect if something is wrong, should be handled different later @@ -98,8 +97,8 @@ where } } - if let Some(block_id) = transaction.block_id { - match self.client().get_block_metadata(&block_id).await { + if let Some(block_id) = &transaction.block_id { + match self.client().get_block_metadata(block_id).await { Ok(metadata) => { if let Some(tx_state) = metadata.transaction_metadata.map(|m| m.transaction_state) { match tx_state { @@ -151,27 +150,16 @@ where // Do nothing, just need to wait a bit more TransactionState::Pending => {} } - } else { - // no need to reissue if one input got spent - if input_got_spent { - process_transaction_with_unknown_state( - &wallet_data, - transaction, - &mut updated_transactions, - &mut output_ids_to_unlock, - )?; - } else { - let time_now = unix_timestamp_now().as_millis(); - // Reissue if older than 30 seconds - if transaction.timestamp + 30000 < time_now { - // only reissue if inputs are still unspent - transactions_to_reissue.push(transaction); - } - } + } else if input_got_spent { + process_transaction_with_unknown_state( + &wallet_data, + transaction, + &mut updated_transactions, + &mut output_ids_to_unlock, + )?; } } Err(crate::client::Error::Node(crate::client::node_api::error::Error::NotFound(_))) => { - // no need to reissue if one input got spent if input_got_spent { process_transaction_with_unknown_state( &wallet_data, @@ -179,38 +167,29 @@ where &mut updated_transactions, &mut output_ids_to_unlock, )?; - } else { - let time_now = unix_timestamp_now().as_millis(); - // Reissue if older than 30 seconds - if transaction.timestamp + 30000 < time_now { - // only reissue if inputs are still unspent - transactions_to_reissue.push(transaction); - } } } Err(e) => return Err(e.into()), } + } else if input_got_spent { + process_transaction_with_unknown_state( + &wallet_data, + transaction, + &mut updated_transactions, + &mut output_ids_to_unlock, + )?; } else { - // transaction wasn't submitted yet, so we have to send it again - // no need to reissue if one input got spent - if input_got_spent { - } else { - // only reissue if inputs are still unspent - transactions_to_reissue.push(transaction); - } + // Reissue if there was no block id yet, because then we also didn't burn any mana + log::debug!("[SYNC] reissue transaction {}", transaction.transaction_id); + let reissued_block = self + .submit_signed_transaction(transaction.payload.clone(), None) + .await?; + transaction.block_id.replace(reissued_block); + updated_transactions.push(transaction); } } drop(wallet_data); - for mut transaction in transactions_to_reissue { - log::debug!("[SYNC] reissue transaction"); - let reissued_block = self - .submit_signed_transaction(transaction.payload.clone(), None) - .await?; - transaction.block_id.replace(reissued_block); - updated_transactions.push(transaction); - } - // updates account with balances, output ids, outputs self.update_with_transactions(updated_transactions, spent_output_ids, output_ids_to_unlock) .await?; diff --git a/sdk/src/wallet/types/balance.rs b/sdk/src/wallet/types/balance.rs index 2454f2dc04..facdc4e150 100644 --- a/sdk/src/wallet/types/balance.rs +++ b/sdk/src/wallet/types/balance.rs @@ -85,6 +85,8 @@ pub struct ManaBalance { pub(crate) total: DecayedMana, /// Available mana. pub(crate) available: DecayedMana, + /// Mana rewards. + pub(crate) rewards: u64, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, CopyGetters, derive_more::AddAssign)] diff --git a/sdk/tests/client/mqtt/topic.rs b/sdk/tests/client/mqtt/topic.rs index 4eb12c2c11..b2fbf84178 100644 --- a/sdk/tests/client/mqtt/topic.rs +++ b/sdk/tests/client/mqtt/topic.rs @@ -5,19 +5,30 @@ use iota_sdk::client::mqtt::{Error, Topic}; #[test] fn valid_topics() { + assert!(Topic::new("commitments/latest").is_ok()); + assert!(Topic::new("commitments/finalized").is_ok()); assert!(Topic::new("blocks").is_ok()); - assert!(Topic::new("blocks/transaction").is_ok()); - assert!(Topic::new("blocks/transaction/tagged-data").is_ok()); - assert!(Topic::new("blocks/transaction/tagged-data/0x0123456789abcdef").is_ok()); - assert!(Topic::new("blocks/tagged-data").is_ok()); - assert!(Topic::new("blocks/tagged-data/0x0123456789abcdef").is_ok()); - assert!(Topic::new("block-metadata/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c4").is_ok()); + assert!(Topic::new("blocks/validation").is_ok()); + assert!(Topic::new("blocks/basic").is_ok()); + assert!(Topic::new("blocks/basic/tagged-data").is_ok()); + assert!(Topic::new("blocks/basic/tagged-data/0x0123456789abcdef").is_ok()); + assert!(Topic::new("blocks/basic/transaction").is_ok()); + assert!(Topic::new("blocks/basic/transaction/tagged-data").is_ok()); + assert!(Topic::new("blocks/basic/transaction/tagged-data/0x0123456789abcdef").is_ok()); + assert!( + Topic::new("block-metadata/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c400000000").is_ok() + ); assert!(Topic::new("block-metadata/accepted").is_ok()); + assert!(Topic::new("block-metadata/confirmed").is_ok()); assert!( - Topic::new("transactions/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c4/included-block") - .is_ok() + Topic::new( + "transactions/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c400000000/included-block" + ) + .is_ok() + ); + assert!( + Topic::new("outputs/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c4000000000000").is_ok() ); - assert!(Topic::new("outputs/0x36845227a59864ac12d3d2389fcb4ea0bdd1a5d1d4ed464bde3154216c3246c40000").is_ok()); assert!(Topic::new("outputs/account/0xb21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151").is_ok()); assert!(Topic::new("outputs/nft/0x38500750eb788bfb89b4589634a82b0cee9c6a9724bafde505ffa1bb875ab0b5").is_ok()); assert!( @@ -37,42 +48,42 @@ fn valid_topics() { fn invalid_topics() { // Empty. assert!(matches!( - Topic::new("blocks/transaction/tagged-data/0x"), + Topic::new("blocks/basic/transaction/tagged-data/0x"), Err(Error::InvalidTopic(_)) )); assert!(matches!( - Topic::new("blocks/tagged-data/0x"), + Topic::new("blocks/basic/tagged-data/0x"), Err(Error::InvalidTopic(_)) )); // Uneven. assert!(matches!( - Topic::new("blocks/transaction/tagged-data/0x0123456789abcde"), + Topic::new("blocks/basic/transaction/tagged-data/0x0123456789abcde"), Err(Error::InvalidTopic(_)) )); assert!(matches!( - Topic::new("blocks/tagged-data/0x0123456789abcde"), + Topic::new("blocks/basic/tagged-data/0x0123456789abcde"), Err(Error::InvalidTopic(_)) )); // Too large. assert!(matches!( Topic::new( - "blocks/transaction/tagged-data/0xb21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151b21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151ff" + "blocks/basic/transaction/tagged-data/0xb21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151b21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151ff" ), Err(Error::InvalidTopic(_)) )); assert!(matches!( Topic::new( - "blocks/tagged-data/0xb21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151b21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151ff" + "blocks/basic/tagged-data/0xb21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151b21517992e96865d5fd90b403fe05fe25c6d4acfb6cdd6e7c9bbfb4266d05151ff" ), Err(Error::InvalidTopic(_)) )); // Invalid chars. assert!(matches!( - Topic::new("blocks/transaction/tagged-data/0x012345@789abcde"), + Topic::new("blocks/basic/transaction/tagged-data/0x012345@789abcde"), Err(Error::InvalidTopic(_)) )); assert!(matches!( - Topic::new("blocks/tagged-data/0x012345@789abcde"), + Topic::new("blocks/basic/tagged-data/0x012345@789abcde"), Err(Error::InvalidTopic(_)) )); } diff --git a/sdk/tests/client/node_api/mqtt.rs b/sdk/tests/client/node_api/mqtt.rs index 06fa0d88fe..2e7a5fcd46 100644 --- a/sdk/tests/client/node_api/mqtt.rs +++ b/sdk/tests/client/node_api/mqtt.rs @@ -19,17 +19,14 @@ async fn test_mqtt() { client .subscribe( - [ - Topic::new("milestone-info/latest").unwrap(), - Topic::new("blocks").unwrap(), - ], + [Topic::new("commitments/latest").unwrap(), Topic::new("blocks").unwrap()], move |evt| { match &evt.payload { MqttPayload::Block(_) => { assert_eq!(evt.topic, "blocks"); } MqttPayload::Json(_) => { - assert_eq!(evt.topic, "milestone-info/latest"); + assert_eq!(evt.topic, "commitments/latest"); } _ => panic!("unexpected mqtt payload type: {:?}", evt), }