From 86a839a3d4832e5f3e99634b2bd737322eb459f6 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Mon, 30 Oct 2023 11:28:02 +0100 Subject: [PATCH 1/3] Nodejs: Anchor related changes (#1528) * Nodejs: Anchor related changes * Nit * Remove StateMetadataOutput --- bindings/nodejs/lib/types/block/address.ts | 29 +++++++- bindings/nodejs/lib/types/block/id.ts | 5 ++ .../nodejs/lib/types/block/output/output.ts | 67 +++++++++++++++++-- .../payload/signed_transaction/unlock.ts | 27 ++++++++ 4 files changed, 122 insertions(+), 6 deletions(-) diff --git a/bindings/nodejs/lib/types/block/address.ts b/bindings/nodejs/lib/types/block/address.ts index 1e6420e265..14e1308a86 100644 --- a/bindings/nodejs/lib/types/block/address.ts +++ b/bindings/nodejs/lib/types/block/address.ts @@ -3,7 +3,7 @@ import { plainToInstance, Type } from 'class-transformer'; import { HexEncodedString } from '../utils'; -import { AccountId, NftId } from './id'; +import { AccountId, AnchorId, NftId } from './id'; /** * An address prepended by its network type. @@ -22,6 +22,8 @@ enum AddressType { Nft = 16, /** An implicit account creation address. */ ImplicitAccountCreation = 24, + /** An Anchor address. */ + Anchor = 28, /** An address with restricted capabilities. */ Restricted = 40, } @@ -65,6 +67,8 @@ abstract class Address { ImplicitAccountCreationAddress, data, ) as any as ImplicitAccountCreationAddress; + } else if (data.type == AddressType.Anchor) { + return plainToInstance(AnchorAddress, data) as any as AnchorAddress; } else if (data.type == AddressType.Restricted) { return plainToInstance( RestrictedAddress, @@ -160,6 +164,27 @@ class ImplicitAccountCreationAddress extends Address { } } +/** + * An Anchor address. + */ +class AnchorAddress extends Address { + /** + * The anchor ID. + */ + readonly anchorId: AnchorId; + /** + * @param address An anchor address as anchor ID. + */ + constructor(address: AnchorId) { + super(AddressType.Anchor); + this.anchorId = address; + } + + toString(): string { + return this.anchorId; + } +} + const RestrictedAddressDiscriminator = { property: 'type', subTypes: [ @@ -234,6 +259,7 @@ const AddressDiscriminator = { value: ImplicitAccountCreationAddress, name: AddressType.ImplicitAccountCreation as any, }, + { value: AnchorAddress, name: AddressType.Anchor as any }, { value: RestrictedAddress, name: AddressType.Restricted as any }, ], }; @@ -246,4 +272,5 @@ export { Ed25519Address, AccountAddress, NftAddress, + AnchorAddress, }; diff --git a/bindings/nodejs/lib/types/block/id.ts b/bindings/nodejs/lib/types/block/id.ts index 33adab1d9e..3267996d9d 100644 --- a/bindings/nodejs/lib/types/block/id.ts +++ b/bindings/nodejs/lib/types/block/id.ts @@ -8,6 +8,11 @@ import { HexEncodedString } from '../utils'; */ export type AccountId = HexEncodedString; +/** + * An Anchor ID represented as hex-encoded string. + */ +export type AnchorId = HexEncodedString; + /** * An NFT ID represented as hex-encoded string. */ diff --git a/bindings/nodejs/lib/types/block/output/output.ts b/bindings/nodejs/lib/types/block/output/output.ts index f7d1b11b03..95bfdd45af 100644 --- a/bindings/nodejs/lib/types/block/output/output.ts +++ b/bindings/nodejs/lib/types/block/output/output.ts @@ -12,7 +12,7 @@ import { plainToInstance, Type } from 'class-transformer'; import { HexEncodedString, hexToBigInt, NumericString, u64 } from '../../utils'; import { TokenScheme, TokenSchemeDiscriminator } from './token-scheme'; import { INativeToken } from '../../models'; -import { AccountId, DelegationId } from '../id'; +import { AccountId, NftId, AnchorId, DelegationId } from '../id'; import { EpochIndex } from '../../block/slot'; export type OutputId = HexEncodedString; @@ -31,6 +31,8 @@ enum OutputType { Nft = 3, /** A Delegation output. */ Delegation = 4, + /** An Anchor output. */ + Anchor = 5, } /** @@ -76,6 +78,13 @@ abstract class Output { return plainToInstance(FoundryOutput, data) as any as FoundryOutput; } else if (data.type == OutputType.Nft) { return plainToInstance(NftOutput, data) as any as NftOutput; + } else if (data.type == OutputType.Delegation) { + return plainToInstance( + DelegationOutput, + data, + ) as any as DelegationOutput; + } else if (data.type == OutputType.Anchor) { + return plainToInstance(AnchorOutput, data) as any as AnchorOutput; } throw new Error('Invalid JSON'); } @@ -185,7 +194,7 @@ class AccountOutput extends ImmutableFeaturesOutput { * Unique identifier of the account, which is the BLAKE2b-256 hash of the Output ID that created it. * Unless its a newly created account, then the id is zeroed. */ - readonly accountId: HexEncodedString; + readonly accountId: AccountId; /** * A counter that denotes the number of foundries created by this account output. */ @@ -205,7 +214,7 @@ class AccountOutput extends ImmutableFeaturesOutput { constructor( amount: u64, mana: u64, - accountId: HexEncodedString, + accountId: AccountId, foundryCounter: number, unlockConditions: UnlockCondition[], ) { @@ -215,6 +224,53 @@ class AccountOutput extends ImmutableFeaturesOutput { this.mana = mana; } } + +/** + * An Anchor output. + */ +class AnchorOutput extends ImmutableFeaturesOutput { + /** + * Unique identifier of the anchor, which is the BLAKE2b-256 hash of the Output ID that created it. + * Unless its a newly created anchor, then the id is zeroed. + */ + readonly anchorId: AnchorId; + /** + * A counter that must increase by 1 every time the anchor output is state transitioned. + */ + readonly stateIndex: number; + /** + * The amount of (stored) Mana held by the output. + */ + readonly mana: u64; + /** + * Metadata that can only be changed by the state controller. + */ + readonly stateMetadata?: HexEncodedString; + + /** + * @param amount The amount of the output. + * @param mana The amount of stored mana. + * @param anchorId The anchor ID as hex-encoded string. + * @param stateIndex A counter that must increase by 1 every time the anchor output is state transitioned. + * @param unlockConditions The unlock conditions of the output. + * @param stateMetadata Metadata that can only be changed by the state controller. + */ + constructor( + amount: u64, + mana: u64, + anchorId: AnchorId, + stateIndex: number, + unlockConditions: UnlockCondition[], + stateMetadata?: HexEncodedString, + ) { + super(OutputType.Account, amount, unlockConditions); + this.anchorId = anchorId; + this.stateIndex = stateIndex; + this.mana = mana; + this.stateMetadata = stateMetadata; + } +} + /** * An NFT output. */ @@ -223,7 +279,7 @@ class NftOutput extends ImmutableFeaturesOutput { * Unique identifier of the NFT, which is the BLAKE2b-256 hash of the Output ID that created it. * Unless its newly minted, then the id is zeroed. */ - readonly nftId: HexEncodedString; + readonly nftId: NftId; /** * The amount of (stored) Mana held by the output. @@ -239,7 +295,7 @@ class NftOutput extends ImmutableFeaturesOutput { constructor( amount: u64, mana: u64, - nftId: HexEncodedString, + nftId: NftId, unlockConditions: UnlockCondition[], ) { super(OutputType.Nft, amount, unlockConditions); @@ -359,6 +415,7 @@ export { CommonOutput, BasicOutput, AccountOutput, + AnchorOutput, NftOutput, FoundryOutput, DelegationOutput, diff --git a/bindings/nodejs/lib/types/block/payload/signed_transaction/unlock.ts b/bindings/nodejs/lib/types/block/payload/signed_transaction/unlock.ts index 339027c761..0df204647d 100644 --- a/bindings/nodejs/lib/types/block/payload/signed_transaction/unlock.ts +++ b/bindings/nodejs/lib/types/block/payload/signed_transaction/unlock.ts @@ -24,6 +24,10 @@ enum UnlockType { * An NFT unlock. */ Nft = 3, + /** + * An Anchor unlock. + */ + Anchor = 4, } /** @@ -117,6 +121,24 @@ class NftUnlock extends Unlock { } } +/** + * An unlock which must reference a previous unlock which unlocks the anchor that the input is locked to. + */ +class AnchorUnlock extends Unlock { + /** + * The reference. + */ + readonly reference: number; + + /** + * @param reference An index referencing a previous unlock. + */ + constructor(reference: number) { + super(UnlockType.Anchor); + this.reference = reference; + } +} + const UnlockDiscriminator = { property: 'type', subTypes: [ @@ -136,6 +158,10 @@ const UnlockDiscriminator = { value: NftUnlock, name: UnlockType.Nft as any, }, + { + value: AnchorUnlock, + name: UnlockType.Anchor as any, + }, ], }; @@ -146,5 +172,6 @@ export { ReferenceUnlock, AccountUnlock, NftUnlock, + AnchorUnlock, UnlockDiscriminator, }; From 50a513999c553837c846751c36f39d3949cc9243 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Mon, 30 Oct 2023 11:51:39 +0100 Subject: [PATCH 2/3] ISA: handle restricted addresses (#1526) * ISA: handle restricted addresses * Avoid clone * Nit * Add 2 more restricted tests --------- Co-authored-by: /alex/ --- .../api/block_builder/input_selection/mod.rs | 7 +- .../input_selection/requirement/sender.rs | 5 + .../client/input_selection/basic_outputs.rs | 180 +++++++++++++++++- 3 files changed, 190 insertions(+), 2 deletions(-) diff --git a/sdk/src/client/api/block_builder/input_selection/mod.rs b/sdk/src/client/api/block_builder/input_selection/mod.rs index 445a7cc3cc..113513f103 100644 --- a/sdk/src/client/api/block_builder/input_selection/mod.rs +++ b/sdk/src/client/api/block_builder/input_selection/mod.rs @@ -68,6 +68,7 @@ impl InputSelection { Address::Account(account_address) => Ok(Some(Requirement::Account(*account_address.account_id()))), Address::Nft(nft_address) => Ok(Some(Requirement::Nft(*nft_address.nft_id()))), Address::Anchor(_) => Err(Error::UnsupportedAddressType(AnchorAddress::KIND)), + Address::Restricted(_) => Ok(None), _ => todo!("What do we do here?"), } } @@ -234,7 +235,11 @@ impl InputSelection { .unwrap() .0; - self.addresses.contains(&required_address) + if let Address::Restricted(restricted_address) = required_address { + self.addresses.contains(restricted_address.address()) + } else { + self.addresses.contains(&required_address) + } }) } diff --git a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs index 0dfc1fc516..ecebdc5826 100644 --- a/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs +++ b/sdk/src/client/api/block_builder/input_selection/requirement/sender.rs @@ -42,6 +42,11 @@ impl InputSelection { Err(e) => Err(e), } } + Address::Restricted(restricted_address) => { + log::debug!("Forwarding {address:?} sender requirement to inner address"); + + self.fulfill_sender_requirement(restricted_address.address()) + } _ => Err(Error::UnsupportedAddressType(address.kind())), } } diff --git a/sdk/tests/client/input_selection/basic_outputs.rs b/sdk/tests/client/input_selection/basic_outputs.rs index 201d6c0686..8eedd759ac 100644 --- a/sdk/tests/client/input_selection/basic_outputs.rs +++ b/sdk/tests/client/input_selection/basic_outputs.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use iota_sdk::{ client::api::input_selection::{Error, InputSelection, Requirement}, types::block::{ - address::{AccountAddress, Address, Bech32Address, NftAddress}, + address::{AccountAddress, Address, Bech32Address, NftAddress, RestrictedAddress, ToBech32Ext}, output::{AccountId, NftId}, protocol::protocol_parameters, }, @@ -1389,3 +1389,181 @@ fn too_many_outputs_with_remainder() { iota_sdk::client::api::input_selection::Error::InvalidOutputCount(129) ) } + +#[test] +fn restricted_ed25519() { + let protocol_parameters = protocol_parameters(); + let address = Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(); + let restricted = RestrictedAddress::new(address.clone()).unwrap(); + let restricted_bech32 = restricted.to_bech32_unchecked("rms").to_string(); + + let inputs = build_inputs([ + Basic(1_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(1_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(1_000_000, &restricted_bech32, None, None, None, None, None, None), + Basic(1_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(1_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + ]); + let outputs = build_outputs([Basic( + 1_000_000, + BECH32_ADDRESS_ED25519_0, + None, + None, + None, + None, + None, + None, + )]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + addresses([BECH32_ADDRESS_ED25519_1]), + protocol_parameters, + ) + .select() + .unwrap(); + + assert_eq!(selected.inputs.len(), 1); + assert_eq!(selected.inputs, [inputs[2].clone()]); + assert!(unsorted_eq(&selected.outputs, &outputs)); +} + +#[test] +fn restricted_nft() { + let protocol_parameters = protocol_parameters(); + let nft_id_1 = NftId::from_str(NFT_ID_1).unwrap(); + let nft_address = Address::from(nft_id_1); + let restricted = RestrictedAddress::new(nft_address.clone()).unwrap(); + let restricted_bech32 = restricted.to_bech32_unchecked("rms").to_string(); + + let inputs = build_inputs([ + Basic(2_000_000, &restricted_bech32, None, None, None, None, None, None), + Nft( + 2_000_000, + nft_id_1, + BECH32_ADDRESS_ED25519_0, + None, + None, + None, + None, + None, + None, + ), + ]); + let outputs = build_outputs([Basic( + 3_000_000, + BECH32_ADDRESS_ED25519_0, + None, + None, + None, + None, + None, + None, + )]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + addresses([BECH32_ADDRESS_ED25519_0]), + protocol_parameters, + ) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs, &inputs)); + assert_eq!(selected.outputs.len(), 2); + assert!(selected.outputs.contains(&outputs[0])); +} + +#[test] +fn restricted_account() { + let protocol_parameters = protocol_parameters(); + let account_id_1 = AccountId::from_str(ACCOUNT_ID_1).unwrap(); + let account_address = Address::from(account_id_1); + let restricted = RestrictedAddress::new(account_address.clone()).unwrap(); + let restricted_bech32 = restricted.to_bech32_unchecked("rms").to_string(); + + let inputs = build_inputs([ + Basic(2_000_000, &restricted_bech32, None, None, None, None, None, None), + Account( + 2_000_000, + account_id_1, + BECH32_ADDRESS_ED25519_0, + None, + None, + None, + None, + ), + ]); + + let outputs = build_outputs([Basic( + 3_000_000, + BECH32_ADDRESS_ED25519_0, + None, + None, + None, + None, + None, + None, + )]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + addresses([BECH32_ADDRESS_ED25519_0]), + protocol_parameters, + ) + .select() + .unwrap(); + + assert!(unsorted_eq(&selected.inputs, &inputs)); + assert_eq!(selected.outputs.len(), 2); + assert!(selected.outputs.contains(&outputs[0])); +} + +#[test] +fn restricted_ed25519_sender() { + let protocol_parameters = protocol_parameters(); + let sender = Address::try_from_bech32(BECH32_ADDRESS_ED25519_1).unwrap(); + let restricted_sender = RestrictedAddress::new(sender.clone()).unwrap(); + let restricted_sender_bech32 = restricted_sender.to_bech32_unchecked("rms").to_string(); + + let inputs = build_inputs([ + Basic(2_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(2_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(1_000_000, BECH32_ADDRESS_ED25519_1, None, None, None, None, None, None), + Basic(2_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + Basic(2_000_000, BECH32_ADDRESS_ED25519_0, None, None, None, None, None, None), + ]); + let outputs = build_outputs([Basic( + 2_000_000, + BECH32_ADDRESS_ED25519_0, + None, + Some(&restricted_sender_bech32), + None, + None, + None, + None, + )]); + + let selected = InputSelection::new( + inputs.clone(), + outputs.clone(), + addresses([BECH32_ADDRESS_ED25519_0, BECH32_ADDRESS_ED25519_1]), + protocol_parameters, + ) + .select() + .unwrap(); + + // Sender + another for amount + assert_eq!(selected.inputs.len(), 2); + assert!( + selected + .inputs + .iter() + .any(|input| *input.output.as_basic().address() == sender) + ); + // Provided output + remainder + assert_eq!(selected.outputs.len(), 2); +} From fe71ccaa258ac13f490121ca817e222d63d4cb63 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Mon, 30 Oct 2023 11:51:59 +0100 Subject: [PATCH 3/3] Add new TransactionFailureReason (#1525) * Add transaction caps related tx failures * Use the errors * Add to Python * Nodejs * camelCase --------- Co-authored-by: /alex/ --- .../models/transaction-failure-reason.ts | 42 +++++++++++ .../python/iota_sdk/types/block/metadata.py | 18 +++++ sdk/src/types/block/output/account.rs | 3 +- sdk/src/types/block/output/anchor.rs | 3 +- sdk/src/types/block/output/foundry.rs | 6 +- sdk/src/types/block/output/nft.rs | 3 +- .../types/block/output/state_transition.rs | 9 ++- sdk/src/types/block/semantic.rs | 69 ++++++++++++++++--- 8 files changed, 132 insertions(+), 21 deletions(-) diff --git a/bindings/nodejs/lib/types/models/transaction-failure-reason.ts b/bindings/nodejs/lib/types/models/transaction-failure-reason.ts index 988e9b5f6d..5bb9c857fb 100644 --- a/bindings/nodejs/lib/types/models/transaction-failure-reason.ts +++ b/bindings/nodejs/lib/types/models/transaction-failure-reason.ts @@ -107,6 +107,36 @@ export enum TransactionFailureReason { */ 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. */ @@ -158,6 +188,18 @@ export const TRANSACTION_FAILURE_REASON_STRINGS: { '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.semanticValidationFailed]: 'The semantic validation failed for a reason not covered by the previous variants.', }; diff --git a/bindings/python/iota_sdk/types/block/metadata.py b/bindings/python/iota_sdk/types/block/metadata.py index a80982d6a2..de34a46e6d 100644 --- a/bindings/python/iota_sdk/types/block/metadata.py +++ b/bindings/python/iota_sdk/types/block/metadata.py @@ -116,6 +116,12 @@ class TransactionFailureReason(Enum): 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. """ InputUtxoAlreadySpent = 1 @@ -138,6 +144,12 @@ class TransactionFailureReason(Enum): MissingStakingFeature = 18 FailedToClaimStakingReward = 19 FailedToClaimDelegationReward = 20 + TransactionCapabilityNativeTokenBurningNotAllowed = 21 + TransactionCapabilityManaBurningNotAllowed = 22 + TransactionCapabilityAccountDestructionNotAllowed = 23 + TransactionCapabilityAnchorDestructionNotAllowed = 24 + TransactionCapabilityFoundryDestructionNotAllowed = 25 + TransactionCapabilityNftDestructionNotAllowed = 26 SemanticValidationFailed = 255 def __str__(self): @@ -162,5 +174,11 @@ def __str__(self): 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." }[self.value] diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 14ae4a60f3..9509345002 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -544,8 +544,7 @@ impl StateTransitionVerifier for AccountOutput { .transaction .has_capability(TransactionCapabilityFlag::DestroyAccountOutputs) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Err(StateTransitionError::UnsupportedStateTransition); + return Err(TransactionFailureReason::TransactionCapabilityAccountDestructionNotAllowed)?; } Ok(()) } diff --git a/sdk/src/types/block/output/anchor.rs b/sdk/src/types/block/output/anchor.rs index 703b7719fd..21f363386f 100644 --- a/sdk/src/types/block/output/anchor.rs +++ b/sdk/src/types/block/output/anchor.rs @@ -604,8 +604,7 @@ impl StateTransitionVerifier for AnchorOutput { .capabilities() .has_capability(TransactionCapabilityFlag::DestroyAnchorOutputs) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Err(StateTransitionError::UnsupportedStateTransition); + return Err(TransactionFailureReason::TransactionCapabilityAccountDestructionNotAllowed)?; } Ok(()) } diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index 2799fd9ac6..56c2782e14 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -532,8 +532,7 @@ impl FoundryOutput { let burned_diff = token_diff - melted_diff; if !burned_diff.is_zero() && !capabilities.has_capability(TransactionCapabilityFlag::BurnNativeTokens) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Err(StateTransitionError::UnsupportedStateTransition); + return Err(TransactionFailureReason::TransactionCapabilityManaBurningNotAllowed)?; } } } @@ -594,8 +593,7 @@ impl StateTransitionVerifier for FoundryOutput { .transaction .has_capability(TransactionCapabilityFlag::DestroyFoundryOutputs) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Err(StateTransitionError::UnsupportedStateTransition); + return Err(TransactionFailureReason::TransactionCapabilityFoundryDestructionNotAllowed)?; } let token_id = current_state.token_id(); diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index 973a67df1a..3b8b397888 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -464,8 +464,7 @@ impl StateTransitionVerifier for NftOutput { .transaction .has_capability(TransactionCapabilityFlag::DestroyNftOutputs) { - // TODO: add a variant https://github.com/iotaledger/iota-sdk/issues/1430 - return Err(StateTransitionError::UnsupportedStateTransition); + return Err(TransactionFailureReason::TransactionCapabilityNftDestructionNotAllowed)?; } Ok(()) } diff --git a/sdk/src/types/block/output/state_transition.rs b/sdk/src/types/block/output/state_transition.rs index 08ddbc78ff..bc1ca6fc36 100644 --- a/sdk/src/types/block/output/state_transition.rs +++ b/sdk/src/types/block/output/state_transition.rs @@ -1,7 +1,7 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::types::block::semantic::SemanticValidationContext; +use crate::types::block::semantic::{SemanticValidationContext, TransactionFailureReason}; /// #[allow(missing_docs)] @@ -28,6 +28,13 @@ pub enum StateTransitionError { UnsortedCreatedFoundries, UnsupportedStateIndexOperation { current_state: u32, next_state: u32 }, UnsupportedStateTransition, + TransactionFailure(TransactionFailureReason), +} + +impl From for StateTransitionError { + fn from(error: TransactionFailureReason) -> Self { + Self::TransactionFailure(error) + } } /// diff --git a/sdk/src/types/block/semantic.rs b/sdk/src/types/block/semantic.rs index bd5a8b1cb8..8edeacaebb 100644 --- a/sdk/src/types/block/semantic.rs +++ b/sdk/src/types/block/semantic.rs @@ -9,7 +9,10 @@ use primitive_types::U256; use crate::types::block::{ address::{Address, AddressCapabilityFlag}, - output::{AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, TokenId, UnlockCondition}, + output::{ + AnchorOutput, ChainId, FoundryId, NativeTokens, Output, OutputId, StateTransitionError, TokenId, + UnlockCondition, + }, payload::signed_transaction::{Transaction, TransactionCapabilityFlag, TransactionId, TransactionSigningHash}, unlock::Unlocks, Error, @@ -64,6 +67,18 @@ pub enum TransactionFailureReason { 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. SemanticValidationFailed = 255, } @@ -107,6 +122,28 @@ impl fmt::Display for TransactionFailureReason { ), Self::FailedToClaimStakingReward => write!(f, "Failed to claim staking reward."), Self::FailedToClaimDelegationReward => write!(f, "Failed to claim delegation reward."), + Self::TransactionCapabilityNativeTokenBurningNotAllowed => write!( + f, + "Burning of native tokens is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityManaBurningNotAllowed => { + write!(f, "Burning of mana is not allowed in the transaction capabilities.") + } + Self::TransactionCapabilityAccountDestructionNotAllowed => write!( + f, + "Destruction of accounts is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityAnchorDestructionNotAllowed => write!( + f, + "Destruction of anchors is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityFoundryDestructionNotAllowed => write!( + f, + "Destruction of foundries is not allowed in the transaction capabilities." + ), + Self::TransactionCapabilityNftDestructionNotAllowed => { + write!(f, "Destruction of nfts is not allowed in the transaction capabilities.") + } Self::SemanticValidationFailed => write!( f, "The semantic validation failed for a reason not covered by the previous variants." @@ -140,6 +177,12 @@ impl TryFrom for TransactionFailureReason { 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)), }) @@ -462,23 +505,29 @@ impl<'a> SemanticValidationContext<'a> { // Validation of state transitions and destructions. for (chain_id, current_state) in self.input_chains.iter() { - if Output::verify_state_transition( + match Output::verify_state_transition( Some(current_state), self.output_chains.get(chain_id).map(core::ops::Deref::deref), &self, - ) - .is_err() - { - return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); + ) { + Err(StateTransitionError::TransactionFailure(f)) => return Ok(Some(f)), + Err(_) => { + return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); + } + _ => {} } } // Validation of state creations. for (chain_id, next_state) in self.output_chains.iter() { - if self.input_chains.get(chain_id).is_none() - && Output::verify_state_transition(None, Some(next_state), &self).is_err() - { - return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); + if self.input_chains.get(chain_id).is_none() { + match Output::verify_state_transition(None, Some(next_state), &self) { + Err(StateTransitionError::TransactionFailure(f)) => return Ok(Some(f)), + Err(_) => { + return Ok(Some(TransactionFailureReason::InvalidChainStateTransition)); + } + _ => {} + } } }