From cdcaf92ac2a1bb7db6b083788d11463f4ef8546d Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Mon, 29 Sep 2025 23:20:56 -0400 Subject: [PATCH 1/7] feat: implement hiero hooks Signed-off-by: Rob Walworth --- Sources/Hiero/AnyTransaction.swift | 4 + Sources/Hiero/FeeSchedule/RequestType.swift | 10 + Sources/Hiero/Hooks/EvmHookCall.swift | 50 + Sources/Hiero/Hooks/EvmHookSpec.swift | 39 + Sources/Hiero/Hooks/HookCall.swift | 91 ++ Sources/Hiero/Hooks/HookCreationDetails.swift | 73 ++ Sources/Hiero/Hooks/HookEntityId.swift | 41 + Sources/Hiero/Hooks/HookExtensionPoint.swift | 29 + Sources/Hiero/Hooks/HookId.swift | 50 + Sources/Hiero/Hooks/HookType.swift | 25 + Sources/Hiero/Hooks/LambdaEvmHook.swift | 58 ++ .../Hiero/Hooks/LambdaMappingEntries.swift | 73 ++ Sources/Hiero/Hooks/LambdaMappingEntry.swift | 63 ++ .../Hiero/Hooks/LambdaSStoreTransaction.swift | 100 ++ Sources/Hiero/Hooks/LambdaStorageSlot.swift | 50 + Sources/Hiero/Hooks/LambdaStorageUpdate.swift | 70 ++ .../Transaction+FromProtobuf.swift | 4 + Sources/Hiero/Version.swift | 2 +- .../Generated/services/basic_types.pb.swift | 806 ++++++++++++++- .../services/contract_create.pb.swift | 15 + .../services/contract_types.pb.swift | 8 +- .../services/contract_update.pb.swift | 30 + .../Generated/services/crypto_create.pb.swift | 15 + .../Generated/services/crypto_update.pb.swift | 30 + .../Generated/services/hook_dispatch.pb.swift | 249 +++++ .../Generated/services/hook_types.pb.swift | 917 ++++++++++++++++++ .../Generated/services/lambda_sstore.pb.swift | 96 ++ .../Generated/services/node_update.pb.swift | 4 +- .../Generated/services/response_code.pb.swift | 130 +++ .../schedulable_transaction_body.pb.swift | 19 + .../smart_contract_service.grpc.swift | 113 +++ .../Generated/services/transaction.pb.swift | 70 +- .../services/transaction_contents.pb.swift | 15 + .../Protos/services/basic_types.proto | 154 +++ .../Protos/services/contract_create.proto | 6 + .../Protos/services/contract_types.proto | 8 +- .../Protos/services/contract_update.proto | 11 + .../Protos/services/crypto_create.proto | 6 + .../Protos/services/crypto_update.proto | 10 + .../Protos/services/hook_dispatch.proto | 49 + .../Protos/services/hook_types.proto | 215 ++++ .../Protos/services/lambda_sstore.proto | 26 + .../Protos/services/node_update.proto | 4 +- .../Protos/services/response_code.proto | 82 ++ .../schedulable_transaction_body.proto | 10 + .../services/smart_contract_service.proto | 5 + .../Protos/services/transaction.proto | 13 + .../services/transaction_contents.proto | 10 + Sources/HieroProtobufs/update_protos.py | 3 + protobufs | 2 +- 50 files changed, 3911 insertions(+), 52 deletions(-) create mode 100644 Sources/Hiero/Hooks/EvmHookCall.swift create mode 100644 Sources/Hiero/Hooks/EvmHookSpec.swift create mode 100644 Sources/Hiero/Hooks/HookCall.swift create mode 100644 Sources/Hiero/Hooks/HookCreationDetails.swift create mode 100644 Sources/Hiero/Hooks/HookEntityId.swift create mode 100644 Sources/Hiero/Hooks/HookExtensionPoint.swift create mode 100644 Sources/Hiero/Hooks/HookId.swift create mode 100644 Sources/Hiero/Hooks/HookType.swift create mode 100644 Sources/Hiero/Hooks/LambdaEvmHook.swift create mode 100644 Sources/Hiero/Hooks/LambdaMappingEntries.swift create mode 100644 Sources/Hiero/Hooks/LambdaMappingEntry.swift create mode 100644 Sources/Hiero/Hooks/LambdaSStoreTransaction.swift create mode 100644 Sources/Hiero/Hooks/LambdaStorageSlot.swift create mode 100644 Sources/Hiero/Hooks/LambdaStorageUpdate.swift create mode 100644 Sources/HieroProtobufs/Generated/services/hook_dispatch.pb.swift create mode 100644 Sources/HieroProtobufs/Generated/services/hook_types.pb.swift create mode 100644 Sources/HieroProtobufs/Generated/services/lambda_sstore.pb.swift create mode 100644 Sources/HieroProtobufs/Protos/services/hook_dispatch.proto create mode 100644 Sources/HieroProtobufs/Protos/services/hook_types.proto create mode 100644 Sources/HieroProtobufs/Protos/services/lambda_sstore.proto diff --git a/Sources/Hiero/AnyTransaction.swift b/Sources/Hiero/AnyTransaction.swift index a532e402..88813f50 100644 --- a/Sources/Hiero/AnyTransaction.swift +++ b/Sources/Hiero/AnyTransaction.swift @@ -63,6 +63,8 @@ internal enum ServicesTransactionDataList { case hintsPartialSignature([Com_Hedera_Hapi_Services_Auxiliary_Hints_HintsPartialSignatureTransactionBody]) case crsPublication([Com_Hedera_Hapi_Services_Auxiliary_Hints_CrsPublicationTransactionBody]) case atomicBatch([Proto_AtomicBatchTransactionBody]) + case lambdaSstore([Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody]) + case hookDispatch([Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody]) internal mutating func append(_ transaction: Proto_TransactionBody.OneOf_Data) throws { switch (self, transaction) { @@ -370,6 +372,8 @@ extension ServicesTransactionDataList: TryFromProtobuf { case .tokenCancelAirdrop(let data): value = .tokenCancelAirdrop([data]) case .tokenClaimAirdrop(let data): value = .tokenClaimAirdrop([data]) case .atomicBatch(let data): value = .atomicBatch([data]) + case .lambdaSstore(let data): value = .lambdaSstore([data]) + case .hookDispatch(let data): value = .hookDispatch([data]) case .stateSignatureTransaction: throw HError.fromProtobuf("Unsupported transaction `StateSignatureTransaction`") case .hintsPreprocessingVote: diff --git a/Sources/Hiero/FeeSchedule/RequestType.swift b/Sources/Hiero/FeeSchedule/RequestType.swift index ea89f8fe..ef1de2d8 100644 --- a/Sources/Hiero/FeeSchedule/RequestType.swift +++ b/Sources/Hiero/FeeSchedule/RequestType.swift @@ -278,6 +278,12 @@ public enum RequestType { /// Atomic batch transaction case atomicBatch + /// Lambda Sstore transaction + case lambdaSstore + + /// Hook dispatch + case hookDispatch + // swiftlint:disable:next function_body_length internal init?(protobuf proto: Proto_HederaFunctionality) throws { switch proto { @@ -372,6 +378,8 @@ public enum RequestType { case .historyProofVote: self = .historyProofVote case .crsPublication: self = .crsPublication case .atomicBatch: self = .atomicBatch + case .lambdaSstore: self = .lambdaSstore + case .hookDispatch: self = .hookDispatch case .UNRECOGNIZED(let code): throw HError.fromProtobuf("unrecognized RequestType: `\(code)`") } @@ -469,6 +477,8 @@ public enum RequestType { case .historyProofVote: return .historyProofVote case .crsPublication: return .crsPublication case .atomicBatch: return .atomicBatch + case .lambdaSstore: return .lambdaSstore + case .hookDispatch: return .hookDispatch } } } diff --git a/Sources/Hiero/Hooks/EvmHookCall.swift b/Sources/Hiero/Hooks/EvmHookCall.swift new file mode 100644 index 00000000..d14170bb --- /dev/null +++ b/Sources/Hiero/Hooks/EvmHookCall.swift @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Specifies the details of a call to an EVM hook. +public struct EvmHookCall { + /// The call data to pass to the hook. + public var data: Data + + /// The gas limit to use. + public var gasLimit: UInt64 + + public init(data: Data = Data(), gasLimit: UInt64 = 0) { + self.data = data + self.gasLimit = gasLimit + } + + /// Set the call data to pass to the hook. + @discardableResult + public mutating func data(_ callData: Data) -> Self { + self.data = callData + return self + } + + /// Set the gas limit for the hook. + @discardableResult + public mutating func gasLimit(_ gasLimit: UInt64) -> Self { + self.gasLimit = gasLimit + return self + } +} + +extension EvmHookCall: TryProtobufCodable { + internal typealias Protobuf = Proto_EvmHookCall + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.data = Data(proto.data) + self.gasLimit = UInt64(proto.gasLimit) + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + .with { proto in + proto.data = data + proto.gasLimit = UInt64(gasLimit) + } + } +} diff --git a/Sources/Hiero/Hooks/EvmHookSpec.swift b/Sources/Hiero/Hooks/EvmHookSpec.swift new file mode 100644 index 00000000..a253d740 --- /dev/null +++ b/Sources/Hiero/Hooks/EvmHookSpec.swift @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Shared specifications of an EVM hook. May be used for any extension point. +public struct EvmHookSpec { + /// The source of the EVM bytecode for the hook. + public var contractId: ContractId? + + public init(contractId: ContractId? = nil) { + self.contractId = contractId + } + + /// Set the contract that contains the hook EVM bytecode. Resets other bytecode sources. + @discardableResult + public mutating func contractId(_ contractId: ContractId) -> Self { + self.contractId = contractId + return self + } +} + +extension EvmHookSpec: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_EvmHookSpec + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.contractId = try ContractId.fromProtobuf(proto.contractID) + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + .with { proto in + if let id = contractId { + proto.contractID = id.toProtobuf() + } + } + } +} diff --git a/Sources/Hiero/Hooks/HookCall.swift b/Sources/Hiero/Hooks/HookCall.swift new file mode 100644 index 00000000..d9d9ade8 --- /dev/null +++ b/Sources/Hiero/Hooks/HookCall.swift @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +public struct HookCall { + public var fullHookId: HookId? + public var hookId: Int64? + public var evmHookCall: EvmHookCall? + + public init(fullHookId: HookId? = nil, evmHookCall: EvmHookCall? = nil) { + self.fullHookId = fullHookId + self.hookId = nil + self.evmHookCall = evmHookCall + } + + public init(hookId: Int64? = nil, evmHookCall: EvmHookCall? = nil) { + self.fullHookId = nil + self.hookId = hookId + self.evmHookCall = evmHookCall + } + + @discardableResult + public mutating func fullHookId(_ hookId: HookId) -> Self { + self.fullHookId = hookId + self.hookId = nil + return self + } + + @discardableResult + public mutating func hookId(_ hookId: Int64) -> Self { + self.hookId = hookId + self.fullHookId = nil + return self + } + + @discardableResult + public mutating func evmHookCall(_ evmHookCall: EvmHookCall) -> Self { + self.evmHookCall = evmHookCall + return self + } +} + +extension HookCall: TryProtobufCodable { + internal typealias Protobuf = Proto_HookCall + + internal init(protobuf proto: Protobuf) throws { + // Map the `oneof id` + switch proto.id { + case .fullHookID(let v): + self.fullHookId = try HookId(protobuf: v) + self.hookId = nil + case .hookID(let v): + self.fullHookId = nil + self.hookId = v + case nil: + self.fullHookId = nil + self.hookId = nil + } + + // Map the `oneof callSpec` + switch proto.callSpec { + case .evmHookCall(let v): + self.evmHookCall = try EvmHookCall(protobuf: v) + case nil: + self.evmHookCall = nil + } + } + + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + + // Set the `oneof id` + if let full = fullHookId { + proto.id = .fullHookID(full.toProtobuf()) + } else if let id = hookId { + proto.id = .hookID(id) + } else { + proto.id = nil + } + + // Set the `oneof callSpec` + if let evm = evmHookCall { + proto.callSpec = .evmHookCall(evm.toProtobuf()) + } else { + proto.callSpec = nil + } + + return proto + } +} diff --git a/Sources/Hiero/Hooks/HookCreationDetails.swift b/Sources/Hiero/Hooks/HookCreationDetails.swift new file mode 100644 index 00000000..db3742e6 --- /dev/null +++ b/Sources/Hiero/Hooks/HookCreationDetails.swift @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +public struct HookCreationDetails { + public var extensionPoint: HookExtensionPoint + public var hookId: Int64 + public var lambdaEvmHook: LambdaEvmHook? + public var adminKey: Key? + + public init( + extensionPoint: HookExtensionPoint, + hookId: Int64 = 0, + lambdaEvmHook: LambdaEvmHook? = nil, + adminKey: Key? = nil + ) { + self.extensionPoint = extensionPoint + self.hookId = hookId + self.lambdaEvmHook = lambdaEvmHook + self.adminKey = adminKey + } + + @discardableResult + public mutating func setLambdaEvmHook(_ hook: LambdaEvmHook) -> Self { + self.lambdaEvmHook = hook + return self + } + + @discardableResult + public mutating func setAdminKey(_ key: Key?) -> Self { + self.adminKey = key + return self + } +} + +extension HookCreationDetails: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_HookCreationDetails + + internal init(protobuf proto: Protobuf) throws { + self.extensionPoint = try HookExtensionPoint(protobuf: proto.extensionPoint) + self.hookId = proto.hookID + + // Only accept lambda; anything else => nil + switch proto.hook { + case .lambdaEvmHook(let v): + self.lambdaEvmHook = try LambdaEvmHook(protobuf: v) + default: + self.lambdaEvmHook = nil + } + + self.adminKey = proto.hasAdminKey ? try Key(protobuf: proto.adminKey) : nil + } + + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + proto.extensionPoint = extensionPoint.toProtobuf() + proto.hookID = hookId + + // Only encode lambda; otherwise leave the oneof unset (nil) + if let lambda = lambdaEvmHook { + proto.hook = .lambdaEvmHook(lambda.toProtobuf()) + } else { + proto.hook = nil + } + + if let key = adminKey { + proto.adminKey = key.toProtobuf() + } // else: leave unset + + return proto + } +} diff --git a/Sources/Hiero/Hooks/HookEntityId.swift b/Sources/Hiero/Hooks/HookEntityId.swift new file mode 100644 index 00000000..7faec7ef --- /dev/null +++ b/Sources/Hiero/Hooks/HookEntityId.swift @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +import HieroProtobufs + +public final class HookEntityId { + /// ID of the account that owns a hook. + public var accountId: AccountId? + + public init(_ accountId: AccountId) { + self.accountId = accountId + } + + /// Sets the ID of the account that owns a hook. + @discardableResult + public func accountId(_ accountId: AccountId) -> Self { + self.accountId = accountId + return self + } +} + +extension HookEntityId: TryProtobufCodable { + internal typealias Protobuf = Proto_HookEntityId + + /// Creates a hook entity ID from a hook entity ID protobuf. + /// + /// - Parameters: + /// - proto: the hook entity ID protobuf. + internal convenience init(protobuf proto: Protobuf) throws { + let id = try AccountId.fromProtobuf(proto.accountID) + self.init(id) + } + + /// Converts this hook entity ID to a protobuf. + internal func toProtobuf() -> Protobuf { + .with { proto in + if let id = accountId { + proto.accountID = id.toProtobuf() + } + } + } +} diff --git a/Sources/Hiero/Hooks/HookExtensionPoint.swift b/Sources/Hiero/Hooks/HookExtensionPoint.swift new file mode 100644 index 00000000..2888e166 --- /dev/null +++ b/Sources/Hiero/Hooks/HookExtensionPoint.swift @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +public enum HookExtensionPoint { + case accountAllowanceHook +} + +extension HookExtensionPoint: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_HookExtensionPoint + + internal init(protobuf proto: Protobuf) throws { + switch proto { + case .accountAllowanceHook: + self = .accountAllowanceHook + case .UNRECOGNIZED: + // Default / fallback + self = .accountAllowanceHook + } + } + + internal func toProtobuf() -> Protobuf { + switch self { + case .accountAllowanceHook: + return .accountAllowanceHook + } + } +} diff --git a/Sources/Hiero/Hooks/HookId.swift b/Sources/Hiero/Hooks/HookId.swift new file mode 100644 index 00000000..1098a1f7 --- /dev/null +++ b/Sources/Hiero/Hooks/HookId.swift @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// An ID for a hook. +public struct HookId { + /// The hook's owning entity ID. + public var entityId: HookEntityId + + /// The ID for the hook. + public var hookId: Int64 + + public init(entityId: HookEntityId, hookId: Int64 = 0) { + self.entityId = entityId + self.hookId = hookId + } + + /// Set the ID of the owning entity. + @discardableResult + public mutating func entityId(_ entityId: HookEntityId) -> Self { + self.entityId = entityId + return self + } + + /// Set the ID of the hook. + @discardableResult + public mutating func hookId(_ hookId: Int64) -> Self { + self.hookId = hookId + return self + } +} + +extension HookId: TryProtobufCodable { + internal typealias Protobuf = Proto_HookId + + /// Construct a `HookId` from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.entityId = try HookEntityId(protobuf: proto.entityID) + self.hookId = proto.hookID + } + + /// Convert this `HookId` to protobuf. + internal func toProtobuf() -> Protobuf { + .with { proto in + proto.entityID = entityId.toProtobuf() + proto.hookID = hookId + } + } +} diff --git a/Sources/Hiero/Hooks/HookType.swift b/Sources/Hiero/Hooks/HookType.swift new file mode 100644 index 00000000..468b77b3 --- /dev/null +++ b/Sources/Hiero/Hooks/HookType.swift @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Enumeration specifying the different types of hooks. +public enum HookType { + /// Execute the hook before the transaction. + case preHook + + /// Execute the hook before and after the transaction. + case prePostHook + + /// Execute the hook for the sender before the transaction. + case preHookSender + + /// Execute the hook for the sender before and after the transaction. + case prePostHookSender + + /// Execute the hook for the receiver before the transaction. + case preHookReceiver + + /// Execute the hook for the receiver before and after the transaction. + case prePostHookReceiver +} diff --git a/Sources/Hiero/Hooks/LambdaEvmHook.swift b/Sources/Hiero/Hooks/LambdaEvmHook.swift new file mode 100644 index 00000000..7f35cdd0 --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaEvmHook.swift @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Definition of a lambda EVM hook. +public struct LambdaEvmHook { + /// Shared EVM hook spec (e.g., contract housing the hook bytecode). + public var spec: EvmHookSpec + + /// The initial storage updates for the lambda, if any. + public var storageUpdates: [LambdaStorageUpdate] + + public init(spec: EvmHookSpec = .init(), storageUpdates: [LambdaStorageUpdate] = []) { + self.spec = spec + self.storageUpdates = storageUpdates + } + + /// Add a storage update to this hook. + @discardableResult + public mutating func addStorageUpdate(_ storageUpdate: LambdaStorageUpdate) -> Self { + storageUpdates.append(storageUpdate) + return self + } + + /// Set the storage updates for this hook. + @discardableResult + public mutating func setStorageUpdates(_ storageUpdates: [LambdaStorageUpdate]) -> Self { + self.storageUpdates = storageUpdates + return self + } + + /// Clear the storage updates for this hook. + @discardableResult + public mutating func clearStorageUpdates() -> Self { + storageUpdates.removeAll() + return self + } +} + +extension LambdaEvmHook: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + // Assuming the generated message has `spec` and `storageUpdates` fields. + self.spec = try EvmHookSpec(protobuf: proto.spec) + self.storageUpdates = try proto.storageUpdates.map { try LambdaStorageUpdate(protobuf: $0) } + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + proto.spec = spec.toProtobuf() + proto.storageUpdates = storageUpdates.map { $0.toProtobuf() } + return proto + } +} diff --git a/Sources/Hiero/Hooks/LambdaMappingEntries.swift b/Sources/Hiero/Hooks/LambdaMappingEntries.swift new file mode 100644 index 00000000..c36f3a7a --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaMappingEntries.swift @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Specifies storage slot updates via indirection into a Solidity mapping. +/// +/// Concretely, if the Solidity mapping is itself at slot `mapping_slot`, then the +/// storage slot for key `key` in the mapping is defined by: +/// `key_storage_slot = keccak256(abi.encodePacked(mapping_slot, key))`. +/// +/// This message lets a metaprotocol be specified in terms of changes to a Solidity +/// mapping's entries. If only raw slots could be updated, then a block stream consumer +/// following the metaprotocol would have to invert the Keccak256 hash to determine +/// which mapping entry was being updated, which is not possible. +public struct LambdaMappingEntries { + /// The slot corresponding to the Solidity mapping. + public var mappingSlot: Data + + /// The mapping entries for this mapping slot. + public var entries: [LambdaMappingEntry] + + public init(mappingSlot: Data = Data(), entries: [LambdaMappingEntry] = []) { + self.mappingSlot = mappingSlot + self.entries = entries + } + + /// Set the Solidity mapping slot. + @discardableResult + public mutating func mappingSlot(_ mappingSlot: Data) -> Self { + self.mappingSlot = mappingSlot + return self + } + + /// Add a mapping entry. + @discardableResult + public mutating func addEntry(_ entry: LambdaMappingEntry) -> Self { + self.entries.append(entry) + return self + } + + /// Set all mapping entries. + @discardableResult + public mutating func setEntries(_ entries: [LambdaMappingEntry]) -> Self { + self.entries = entries + return self + } + + /// Clear the mapping entries. + @discardableResult + public mutating func clearEntries() -> Self { + self.entries.removeAll() + return self + } +} + +extension LambdaMappingEntries: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.mappingSlot = proto.mappingSlot + self.entries = try proto.entries.map { try LambdaMappingEntry(protobuf: $0) } + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + proto.mappingSlot = mappingSlot + proto.entries = entries.map { $0.toProtobuf() } + return proto + } +} diff --git a/Sources/Hiero/Hooks/LambdaMappingEntry.swift b/Sources/Hiero/Hooks/LambdaMappingEntry.swift new file mode 100644 index 00000000..6b9e5fc1 --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaMappingEntry.swift @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// An implicit storage slot specified as a Solidity mapping entry. +public struct LambdaMappingEntry { + /// The slot corresponding to the Solidity mapping. + public var mappingSlot: Data + + /// The 32-byte key of the mapping entry. + public var key: Data + + /// The 32-byte value of the mapping entry (leave empty to delete). + public var value: Data + + public init(mappingSlot: Data = Data(), key: Data = Data(), value: Data = Data()) { + self.mappingSlot = mappingSlot + self.key = key + self.value = value + } + + /// Set the Solidity mapping slot. + @discardableResult + public mutating func setMappingSlot(_ mappingSlot: Data) -> Self { + self.mappingSlot = mappingSlot + return self + } + + /// Set the key for the mapping entry. + @discardableResult + public mutating func setKey(_ key: Data) -> Self { + self.key = key + return self + } + + /// Set the value for the mapping entry. + @discardableResult + public mutating func setValue(_ value: Data) -> Self { + self.value = value + return self + } +} + +extension LambdaMappingEntry: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.mappingSlot = proto.mappingSlot + self.key = proto.key + self.value = proto.value + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + proto.mappingSlot = mappingSlot + proto.key = key + proto.value = value + return proto + } +} diff --git a/Sources/Hiero/Hooks/LambdaSStoreTransaction.swift b/Sources/Hiero/Hooks/LambdaSStoreTransaction.swift new file mode 100644 index 00000000..b3941365 --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaSStoreTransaction.swift @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import GRPC +import HieroProtobufs + +/// Updates storage for a Lambda EVM hook. +public final class LambdaSStoreTransaction: Transaction { + /// Create a new `LambdaSStoreTransaction` ready for configuration. + public override init() { + super.init() + } + + /// The ID of the hook to update. + public var hookId: HookId? { + willSet { ensureNotFrozen() } + } + + /// The storage updates to apply to the hook. + public var storageUpdates: [LambdaStorageUpdate] = [] { + willSet { ensureNotFrozen() } + } + + /// Sets the ID of the hook to update. + @discardableResult + public func hookId(_ hookId: HookId) -> Self { + self.hookId = hookId + return self + } + + /// Adds a storage update. + @discardableResult + public func addStorageUpdate(_ update: LambdaStorageUpdate) -> Self { + storageUpdates.append(update) + return self + } + + /// Sets all storage updates. + @discardableResult + public func storageUpdates(_ updates: [LambdaStorageUpdate]) -> Self { + self.storageUpdates = updates + return self + } + + /// Clears all storage updates. + @discardableResult + public func clearStorageUpdates() -> Self { + storageUpdates.removeAll() + return self + } + + /// Construct from `TransactionBody` and the concrete Lambda SSTORE body. + internal init( + protobuf proto: Proto_TransactionBody, + _ data: Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody + ) throws { + // hookID (message presence); treat default instance as nil if your SDK does that. + self.hookId = try HookId(protobuf: data.hookID) + + // storageUpdates (repeated) + self.storageUpdates = try data.storageUpdates.map { try LambdaStorageUpdate(protobuf: $0) } + + try super.init(protobuf: proto) + } + + internal override func validateChecksums(on ledgerId: LedgerId) throws { + // If HookId/HookEntityId/AccountId supports checksum validation, invoke it here. + // Example (adjust to your actual API): + // try hookId?.entityId.accountId?.validateChecksums(on: ledgerId) + try super.validateChecksums(on: ledgerId) + } + + internal override func transactionExecute( + _ channel: GRPCChannel, + _ request: Proto_Transaction + ) async throws -> Proto_TransactionResponse { + // TODO: Adjust the service/type name to your generated gRPC client and method. + // This is the conventional naming SwiftProtobuf generates for a `HooksService` with rpc `lambdaSStore`. + return try await Proto_SmartContractServiceAsyncClient(channel: channel) + .lambdaSStore(request, callOptions: applyGrpcHeader()) + } + + internal override func toTransactionDataProtobuf(_ chunkInfo: ChunkInfo) -> Proto_TransactionBody.OneOf_Data { + _ = chunkInfo.assertSingleTransaction() + return .lambdaSstore(toProtobuf()) + } +} + +extension LambdaSStoreTransaction: ToProtobuf { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody + + internal func toProtobuf() -> Protobuf { + .with { proto in + if let id = hookId { + proto.hookID = id.toProtobuf() + } + proto.storageUpdates = storageUpdates.map { $0.toProtobuf() } + } + } +} diff --git a/Sources/Hiero/Hooks/LambdaStorageSlot.swift b/Sources/Hiero/Hooks/LambdaStorageSlot.swift new file mode 100644 index 00000000..6cfcdca6 --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaStorageSlot.swift @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// An explicit storage slot update. +public struct LambdaStorageSlot { + /// The 32-byte storage slot key. + public var key: Data + + /// The 32-byte storage slot value (leave empty to delete). + public var value: Data + + public init(key: Data = Data(), value: Data = Data()) { + self.key = key + self.value = value + } + + /// Set the storage slot key. + @discardableResult + public mutating func setKey(_ key: Data) -> Self { + self.key = key + return self + } + + /// Set the storage slot value. + @discardableResult + public mutating func setValue(_ value: Data) -> Self { + self.value = value + return self + } +} + +extension LambdaStorageSlot: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.key = proto.key + self.value = proto.value + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + proto.key = key + proto.value = value + return proto + } +} diff --git a/Sources/Hiero/Hooks/LambdaStorageUpdate.swift b/Sources/Hiero/Hooks/LambdaStorageUpdate.swift new file mode 100644 index 00000000..a9510dfb --- /dev/null +++ b/Sources/Hiero/Hooks/LambdaStorageUpdate.swift @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Specifies a key/value pair in the storage of a lambda, either by the explicit storage slot contents; +/// or by a combination of a Solidity mapping's slot key and the key into that mapping. +public struct LambdaStorageUpdate { + /// An explicit storage slot update. + public var storageSlot: LambdaStorageSlot? + + /// An implicit storage slot update specified as Solidity mapping entries. + public var mappingEntries: LambdaMappingEntries? + + public init(storageSlot: LambdaStorageSlot? = nil, mappingEntries: LambdaMappingEntries? = nil) { + self.storageSlot = storageSlot + self.mappingEntries = mappingEntries + } + + /// Set an update for an explicit storage slot. Resets any mapping-based update. + @discardableResult + public mutating func setStorageSlot(_ storageSlot: LambdaStorageSlot) -> Self { + self.storageSlot = storageSlot + self.mappingEntries = nil + return self + } + + /// Set an update for Solidity-mapped entries. Resets any explicit-slot update. + @discardableResult + public mutating func setMappingEntries(_ mappingEntries: LambdaMappingEntries) -> Self { + self.mappingEntries = mappingEntries + self.storageSlot = nil + return self + } +} + +extension LambdaStorageUpdate: TryProtobufCodable { + internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate + + internal init(protobuf proto: Protobuf) throws { + // Default to nils + self.storageSlot = nil + self.mappingEntries = nil + + // Handle the `oneof` for which update is present. + if let which = proto.update { + switch which { + case .storageSlot(let slot): + self.storageSlot = try LambdaStorageSlot(protobuf: slot) + case .mappingEntries(let entries): + self.mappingEntries = try LambdaMappingEntries(protobuf: entries) + } + } + } + + internal func toProtobuf() -> Protobuf { + var proto = Protobuf() + + // Set exactly one branch of the oneof (or leave nil if neither is set). + if let slot = storageSlot { + proto.update = .storageSlot(slot.toProtobuf()) + } else if let entries = mappingEntries { + proto.update = .mappingEntries(entries.toProtobuf()) + } else { + proto.update = nil + } + + return proto + } +} diff --git a/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift b/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift index 2d0fee28..c4938db9 100644 --- a/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift +++ b/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift @@ -216,6 +216,10 @@ extension Transaction { let value = try intoOnlyValue(value) return try BatchTransaction(protobuf: firstBody, value) + case .lambdaSstore(let value): + let value = try intoOnlyValue(value) + return try LambdaSStoreTransaction(protobuf: firstBody, value) + case .stateSignatureTransaction(let code): throw HError.fromProtobuf("unrecognized: stateSignatureTransaction `\(code)`") diff --git a/Sources/Hiero/Version.swift b/Sources/Hiero/Version.swift index 4ad512c9..5b882521 100644 --- a/Sources/Hiero/Version.swift +++ b/Sources/Hiero/Version.swift @@ -1,4 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 public enum VersionInfo { - public static let version = "v0.43.0" + public static let version = "v0.43.0-dev" } diff --git a/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift b/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift index da08698e..3a28719f 100644 --- a/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift @@ -973,6 +973,14 @@ public enum Proto_HederaFunctionality: SwiftProtobuf.Enum, Swift.CaseIterable { ///* /// Submit a batch of transactions to run atomically case atomicBatch // = 108 + + ///* + /// Update one or more storage slots in an lambda EVM hook. + case lambdaSstore // = 109 + + ///* + /// (Internal-only) Dispatch a hook action. + case hookDispatch // = 110 case UNRECOGNIZED(Int) public init() { @@ -1072,6 +1080,8 @@ public enum Proto_HederaFunctionality: SwiftProtobuf.Enum, Swift.CaseIterable { case 106: self = .historyProofVote case 107: self = .crsPublication case 108: self = .atomicBatch + case 109: self = .lambdaSstore + case 110: self = .hookDispatch default: self = .UNRECOGNIZED(rawValue) } } @@ -1169,6 +1179,8 @@ public enum Proto_HederaFunctionality: SwiftProtobuf.Enum, Swift.CaseIterable { case .historyProofVote: return 106 case .crsPublication: return 107 case .atomicBatch: return 108 + case .lambdaSstore: return 109 + case .hookDispatch: return 110 case .UNRECOGNIZED(let i): return i } } @@ -1266,6 +1278,8 @@ public enum Proto_HederaFunctionality: SwiftProtobuf.Enum, Swift.CaseIterable { .historyProofVote, .crsPublication, .atomicBatch, + .lambdaSstore, + .hookDispatch, ] } @@ -1769,6 +1783,162 @@ public struct Proto_TransactionID: Sendable { fileprivate var _accountID: Proto_AccountID? = nil } +///* +/// Once a hook is created, its full id. +///

+/// A composite of its creating entity's id and an arbitrary 64-bit hook id +/// (which need not be sequential). +public struct Proto_HookId: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The hook's creating entity id. + public var entityID: Proto_HookEntityId { + get {return _entityID ?? Proto_HookEntityId()} + set {_entityID = newValue} + } + /// Returns true if `entityID` has been explicitly set. + public var hasEntityID: Bool {return self._entityID != nil} + /// Clears the value of `entityID`. Subsequent reads from it will return its default value. + public mutating func clearEntityID() {self._entityID = nil} + + ///* + /// An arbitrary 64-bit identifier. + public var hookID: Int64 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _entityID: Proto_HookEntityId? = nil +} + +///* +/// The id of an entity using a hook. +public struct Proto_HookEntityId: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var entityID: Proto_HookEntityId.OneOf_EntityID? = nil + + ///* + /// An account using a hook. + public var accountID: Proto_AccountID { + get { + if case .accountID(let v)? = entityID {return v} + return Proto_AccountID() + } + set {entityID = .accountID(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_EntityID: Equatable, Sendable { + ///* + /// An account using a hook. + case accountID(Proto_AccountID) + + } + + public init() {} +} + +///* +/// Specifies a call to a hook from within a transaction. +///

+/// Often the hook's entity is implied by the nature of the call site. For example, when using an account allowance hook +/// inside a crypto transfer, the hook's entity is necessarily the account whose authorization is required. +///

+/// For future extension points where the hook owner is not forced by the context, we include the option to fully +/// specify the hook id for the call. +public struct Proto_HookCall: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var id: Proto_HookCall.OneOf_ID? = nil + + ///* + /// The full id of the hook to call, when the owning entity is not forced by the call site. + public var fullHookID: Proto_HookId { + get { + if case .fullHookID(let v)? = id {return v} + return Proto_HookId() + } + set {id = .fullHookID(newValue)} + } + + ///* + /// The numeric id of the hook to call, when the owning entity is forced by the call site. + public var hookID: Int64 { + get { + if case .hookID(let v)? = id {return v} + return 0 + } + set {id = .hookID(newValue)} + } + + ///* + /// Specifies details of the call. + public var callSpec: Proto_HookCall.OneOf_CallSpec? = nil + + ///* + /// Specification of how to call an EVM hook. + public var evmHookCall: Proto_EvmHookCall { + get { + if case .evmHookCall(let v)? = callSpec {return v} + return Proto_EvmHookCall() + } + set {callSpec = .evmHookCall(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_ID: Equatable, Sendable { + ///* + /// The full id of the hook to call, when the owning entity is not forced by the call site. + case fullHookID(Proto_HookId) + ///* + /// The numeric id of the hook to call, when the owning entity is forced by the call site. + case hookID(Int64) + + } + + ///* + /// Specifies details of the call. + public enum OneOf_CallSpec: Equatable, Sendable { + ///* + /// Specification of how to call an EVM hook. + case evmHookCall(Proto_EvmHookCall) + + } + + public init() {} +} + +///* +/// Specifies details of a call to an EVM hook. +public struct Proto_EvmHookCall: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// Call data to pass to the hook via the IHieroHook.HookContext#data field. + public var data: Data = Data() + + ///* + /// The gas limit to use. + public var gasLimit: UInt64 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + ///* /// An account, and the amount that it sends or receives during a token transfer. /// @@ -1809,8 +1979,60 @@ public struct Proto_AccountAmount: Sendable { /// The default value SHALL be false (unset). public var isApproval: Bool = false + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` on scoped + /// account; the hook's invoked methods must not revert and must return + /// true for the containing CryptoTransfer to succeed. + ///

+ /// Cannot be set if `is_approval` is true. + public var hookCall: Proto_AccountAmount.OneOf_HookCall? = nil + + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + public var preTxAllowanceHook: Proto_HookCall { + get { + if case .preTxAllowanceHook(let v)? = hookCall {return v} + return Proto_HookCall() + } + set {hookCall = .preTxAllowanceHook(newValue)} + } + + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + public var prePostTxAllowanceHook: Proto_HookCall { + get { + if case .prePostTxAllowanceHook(let v)? = hookCall {return v} + return Proto_HookCall() + } + set {hookCall = .prePostTxAllowanceHook(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` on scoped + /// account; the hook's invoked methods must not revert and must return + /// true for the containing CryptoTransfer to succeed. + ///

+ /// Cannot be set if `is_approval` is true. + public enum OneOf_HookCall: Equatable, Sendable { + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + case preTxAllowanceHook(Proto_HookCall) + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + case prePostTxAllowanceHook(Proto_HookCall) + + } + public init() {} fileprivate var _accountID: Proto_AccountID? = nil @@ -1850,7 +2072,7 @@ public struct Proto_TransferList: Sendable { /// Each `NftTransfer` SHALL be contained in another message (typically /// `TokenTransferList`) that details which `Token` type applies to this NFT /// transfer. -public struct Proto_NftTransfer: Sendable { +public struct Proto_NftTransfer: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1858,28 +2080,31 @@ public struct Proto_NftTransfer: Sendable { ///* /// An Account identifier for the sender. public var senderAccountID: Proto_AccountID { - get {return _senderAccountID ?? Proto_AccountID()} - set {_senderAccountID = newValue} + get {return _storage._senderAccountID ?? Proto_AccountID()} + set {_uniqueStorage()._senderAccountID = newValue} } /// Returns true if `senderAccountID` has been explicitly set. - public var hasSenderAccountID: Bool {return self._senderAccountID != nil} + public var hasSenderAccountID: Bool {return _storage._senderAccountID != nil} /// Clears the value of `senderAccountID`. Subsequent reads from it will return its default value. - public mutating func clearSenderAccountID() {self._senderAccountID = nil} + public mutating func clearSenderAccountID() {_uniqueStorage()._senderAccountID = nil} ///* /// An Account identifier for the receiver. public var receiverAccountID: Proto_AccountID { - get {return _receiverAccountID ?? Proto_AccountID()} - set {_receiverAccountID = newValue} + get {return _storage._receiverAccountID ?? Proto_AccountID()} + set {_uniqueStorage()._receiverAccountID = newValue} } /// Returns true if `receiverAccountID` has been explicitly set. - public var hasReceiverAccountID: Bool {return self._receiverAccountID != nil} + public var hasReceiverAccountID: Bool {return _storage._receiverAccountID != nil} /// Clears the value of `receiverAccountID`. Subsequent reads from it will return its default value. - public mutating func clearReceiverAccountID() {self._receiverAccountID = nil} + public mutating func clearReceiverAccountID() {_uniqueStorage()._receiverAccountID = nil} ///* /// A serial number for the NFT to transfer. - public var serialNumber: Int64 = 0 + public var serialNumber: Int64 { + get {return _storage._serialNumber} + set {_uniqueStorage()._serialNumber = newValue} + } ///* /// An approved allowance flag.
@@ -1890,14 +2115,128 @@ public struct Proto_NftTransfer: Sendable { /// If set, the `senderAccountID` MUST be the "payer" account for /// the transaction
/// The default value SHALL be false (unset). - public var isApproval: Bool = false + public var isApproval: Bool { + get {return _storage._isApproval} + set {_uniqueStorage()._isApproval = newValue} + } + + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + /// senderAccountID that must succeed for the transaction to occur. + ///

+ /// Cannot be set if `is_approval` is true. + public var senderAllowanceHookCall: OneOf_SenderAllowanceHookCall? { + get {return _storage._senderAllowanceHookCall} + set {_uniqueStorage()._senderAllowanceHookCall = newValue} + } + + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + public var preTxSenderAllowanceHook: Proto_HookCall { + get { + if case .preTxSenderAllowanceHook(let v)? = _storage._senderAllowanceHookCall {return v} + return Proto_HookCall() + } + set {_uniqueStorage()._senderAllowanceHookCall = .preTxSenderAllowanceHook(newValue)} + } + + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + public var prePostTxSenderAllowanceHook: Proto_HookCall { + get { + if case .prePostTxSenderAllowanceHook(let v)? = _storage._senderAllowanceHookCall {return v} + return Proto_HookCall() + } + set {_uniqueStorage()._senderAllowanceHookCall = .prePostTxSenderAllowanceHook(newValue)} + } + + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + /// receiverAccountID that must succeed for the transaction to occur. + ///

+ /// May be set even if `is_approval` is true. In this case, the approval applies + /// to the sender authorization, and the hook applies to the receiver authorization + /// (if needed, e.g. because of a fallback royalty fee or receiver signature + /// requirement). + public var receiverAllowanceHookCall: OneOf_ReceiverAllowanceHookCall? { + get {return _storage._receiverAllowanceHookCall} + set {_uniqueStorage()._receiverAllowanceHookCall = newValue} + } + + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + public var preTxReceiverAllowanceHook: Proto_HookCall { + get { + if case .preTxReceiverAllowanceHook(let v)? = _storage._receiverAllowanceHookCall {return v} + return Proto_HookCall() + } + set {_uniqueStorage()._receiverAllowanceHookCall = .preTxReceiverAllowanceHook(newValue)} + } + + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + public var prePostTxReceiverAllowanceHook: Proto_HookCall { + get { + if case .prePostTxReceiverAllowanceHook(let v)? = _storage._receiverAllowanceHookCall {return v} + return Proto_HookCall() + } + set {_uniqueStorage()._receiverAllowanceHookCall = .prePostTxReceiverAllowanceHook(newValue)} + } public var unknownFields = SwiftProtobuf.UnknownStorage() + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + /// senderAccountID that must succeed for the transaction to occur. + ///

+ /// Cannot be set if `is_approval` is true. + public enum OneOf_SenderAllowanceHookCall: Equatable, Sendable { + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + case preTxSenderAllowanceHook(Proto_HookCall) + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + case prePostTxSenderAllowanceHook(Proto_HookCall) + + } + + ///* + /// If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + /// receiverAccountID that must succeed for the transaction to occur. + ///

+ /// May be set even if `is_approval` is true. In this case, the approval applies + /// to the sender authorization, and the hook applies to the receiver authorization + /// (if needed, e.g. because of a fallback royalty fee or receiver signature + /// requirement). + public enum OneOf_ReceiverAllowanceHookCall: Equatable, Sendable { + ///* + /// A single call made before attempting the CryptoTransfer, to a + /// method with logical signature allow(HookContext, ProposedTransfers) + case preTxReceiverAllowanceHook(Proto_HookCall) + ///* + /// Two calls, the first call before attempting the CryptoTransfer, to a + /// method with logical signature allowPre(HookContext, ProposedTransfers); + /// and the second call after attempting the CryptoTransfer, to a method + /// with logical signature allowPost(HookContext, ProposedTransfers). + case prePostTxReceiverAllowanceHook(Proto_HookCall) + + } + public init() {} - fileprivate var _senderAccountID: Proto_AccountID? = nil - fileprivate var _receiverAccountID: Proto_AccountID? = nil + fileprivate var _storage = _StorageClass.defaultInstance } ///* @@ -3781,6 +4120,8 @@ extension Proto_HederaFunctionality: SwiftProtobuf._ProtoNameProviding { 106: .same(proto: "HistoryProofVote"), 107: .same(proto: "CrsPublication"), 108: .same(proto: "AtomicBatch"), + 109: .same(proto: "LambdaSStore"), + 110: .same(proto: "HookDispatch"), ] } @@ -4270,12 +4611,225 @@ extension Proto_TransactionID: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl } } +extension Proto_HookId: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookId" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "entity_id"), + 2: .standard(proto: "hook_id"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._entityID) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self.hookID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._entityID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.hookID != 0 { + try visitor.visitSingularInt64Field(value: self.hookID, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Proto_HookId, rhs: Proto_HookId) -> Bool { + if lhs._entityID != rhs._entityID {return false} + if lhs.hookID != rhs.hookID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Proto_HookEntityId: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookEntityId" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "account_id"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Proto_AccountID? + var hadOneofValue = false + if let current = self.entityID { + hadOneofValue = true + if case .accountID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.entityID = .accountID(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if case .accountID(let v)? = self.entityID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Proto_HookEntityId, rhs: Proto_HookEntityId) -> Bool { + if lhs.entityID != rhs.entityID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Proto_HookCall: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookCall" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "full_hook_id"), + 2: .standard(proto: "hook_id"), + 3: .standard(proto: "evm_hook_call"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Proto_HookId? + var hadOneofValue = false + if let current = self.id { + hadOneofValue = true + if case .fullHookID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.id = .fullHookID(v) + } + }() + case 2: try { + var v: Int64? + try decoder.decodeSingularInt64Field(value: &v) + if let v = v { + if self.id != nil {try decoder.handleConflictingOneOf()} + self.id = .hookID(v) + } + }() + case 3: try { + var v: Proto_EvmHookCall? + var hadOneofValue = false + if let current = self.callSpec { + hadOneofValue = true + if case .evmHookCall(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.callSpec = .evmHookCall(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.id { + case .fullHookID?: try { + guard case .fullHookID(let v)? = self.id else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .hookID?: try { + guard case .hookID(let v)? = self.id else { preconditionFailure() } + try visitor.visitSingularInt64Field(value: v, fieldNumber: 2) + }() + case nil: break + } + try { if case .evmHookCall(let v)? = self.callSpec { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Proto_HookCall, rhs: Proto_HookCall) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.callSpec != rhs.callSpec {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Proto_EvmHookCall: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".EvmHookCall" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "data"), + 2: .standard(proto: "gas_limit"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.data) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.gasLimit) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.data.isEmpty { + try visitor.visitSingularBytesField(value: self.data, fieldNumber: 1) + } + if self.gasLimit != 0 { + try visitor.visitSingularUInt64Field(value: self.gasLimit, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Proto_EvmHookCall, rhs: Proto_EvmHookCall) -> Bool { + if lhs.data != rhs.data {return false} + if lhs.gasLimit != rhs.gasLimit {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Proto_AccountAmount: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".AccountAmount" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "accountID"), 2: .same(proto: "amount"), 3: .standard(proto: "is_approval"), + 4: .standard(proto: "pre_tx_allowance_hook"), + 5: .standard(proto: "pre_post_tx_allowance_hook"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4287,6 +4841,32 @@ extension Proto_AccountAmount: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 1: try { try decoder.decodeSingularMessageField(value: &self._accountID) }() case 2: try { try decoder.decodeSingularSInt64Field(value: &self.amount) }() case 3: try { try decoder.decodeSingularBoolField(value: &self.isApproval) }() + case 4: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = self.hookCall { + hadOneofValue = true + if case .preTxAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.hookCall = .preTxAllowanceHook(v) + } + }() + case 5: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = self.hookCall { + hadOneofValue = true + if case .prePostTxAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.hookCall = .prePostTxAllowanceHook(v) + } + }() default: break } } @@ -4306,6 +4886,17 @@ extension Proto_AccountAmount: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.isApproval != false { try visitor.visitSingularBoolField(value: self.isApproval, fieldNumber: 3) } + switch self.hookCall { + case .preTxAllowanceHook?: try { + guard case .preTxAllowanceHook(let v)? = self.hookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .prePostTxAllowanceHook?: try { + guard case .prePostTxAllowanceHook(let v)? = self.hookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } @@ -4313,6 +4904,7 @@ extension Proto_AccountAmount: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs._accountID != rhs._accountID {return false} if lhs.amount != rhs.amount {return false} if lhs.isApproval != rhs.isApproval {return false} + if lhs.hookCall != rhs.hookCall {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4357,48 +4949,178 @@ extension Proto_NftTransfer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 2: .same(proto: "receiverAccountID"), 3: .same(proto: "serialNumber"), 4: .standard(proto: "is_approval"), + 5: .standard(proto: "pre_tx_sender_allowance_hook"), + 6: .standard(proto: "pre_post_tx_sender_allowance_hook"), + 7: .standard(proto: "pre_tx_receiver_allowance_hook"), + 8: .standard(proto: "pre_post_tx_receiver_allowance_hook"), ] + fileprivate class _StorageClass { + var _senderAccountID: Proto_AccountID? = nil + var _receiverAccountID: Proto_AccountID? = nil + var _serialNumber: Int64 = 0 + var _isApproval: Bool = false + var _senderAllowanceHookCall: Proto_NftTransfer.OneOf_SenderAllowanceHookCall? + var _receiverAllowanceHookCall: Proto_NftTransfer.OneOf_ReceiverAllowanceHookCall? + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _senderAccountID = source._senderAccountID + _receiverAccountID = source._receiverAccountID + _serialNumber = source._serialNumber + _isApproval = source._isApproval + _senderAllowanceHookCall = source._senderAllowanceHookCall + _receiverAllowanceHookCall = source._receiverAllowanceHookCall + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._senderAccountID) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._receiverAccountID) }() - case 3: try { try decoder.decodeSingularInt64Field(value: &self.serialNumber) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.isApproval) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._senderAccountID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._receiverAccountID) }() + case 3: try { try decoder.decodeSingularInt64Field(value: &_storage._serialNumber) }() + case 4: try { try decoder.decodeSingularBoolField(value: &_storage._isApproval) }() + case 5: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = _storage._senderAllowanceHookCall { + hadOneofValue = true + if case .preTxSenderAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._senderAllowanceHookCall = .preTxSenderAllowanceHook(v) + } + }() + case 6: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = _storage._senderAllowanceHookCall { + hadOneofValue = true + if case .prePostTxSenderAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._senderAllowanceHookCall = .prePostTxSenderAllowanceHook(v) + } + }() + case 7: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = _storage._receiverAllowanceHookCall { + hadOneofValue = true + if case .preTxReceiverAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._receiverAllowanceHookCall = .preTxReceiverAllowanceHook(v) + } + }() + case 8: try { + var v: Proto_HookCall? + var hadOneofValue = false + if let current = _storage._receiverAllowanceHookCall { + hadOneofValue = true + if case .prePostTxReceiverAllowanceHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._receiverAllowanceHookCall = .prePostTxReceiverAllowanceHook(v) + } + }() + default: break + } } } } public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._senderAccountID { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._receiverAccountID { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if self.serialNumber != 0 { - try visitor.visitSingularInt64Field(value: self.serialNumber, fieldNumber: 3) - } - if self.isApproval != false { - try visitor.visitSingularBoolField(value: self.isApproval, fieldNumber: 4) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._senderAccountID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._receiverAccountID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if _storage._serialNumber != 0 { + try visitor.visitSingularInt64Field(value: _storage._serialNumber, fieldNumber: 3) + } + if _storage._isApproval != false { + try visitor.visitSingularBoolField(value: _storage._isApproval, fieldNumber: 4) + } + switch _storage._senderAllowanceHookCall { + case .preTxSenderAllowanceHook?: try { + guard case .preTxSenderAllowanceHook(let v)? = _storage._senderAllowanceHookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .prePostTxSenderAllowanceHook?: try { + guard case .prePostTxSenderAllowanceHook(let v)? = _storage._senderAllowanceHookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case nil: break + } + switch _storage._receiverAllowanceHookCall { + case .preTxReceiverAllowanceHook?: try { + guard case .preTxReceiverAllowanceHook(let v)? = _storage._receiverAllowanceHookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .prePostTxReceiverAllowanceHook?: try { + guard case .prePostTxReceiverAllowanceHook(let v)? = _storage._receiverAllowanceHookCall else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case nil: break + } } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Proto_NftTransfer, rhs: Proto_NftTransfer) -> Bool { - if lhs._senderAccountID != rhs._senderAccountID {return false} - if lhs._receiverAccountID != rhs._receiverAccountID {return false} - if lhs.serialNumber != rhs.serialNumber {return false} - if lhs.isApproval != rhs.isApproval {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._senderAccountID != rhs_storage._senderAccountID {return false} + if _storage._receiverAccountID != rhs_storage._receiverAccountID {return false} + if _storage._serialNumber != rhs_storage._serialNumber {return false} + if _storage._isApproval != rhs_storage._isApproval {return false} + if _storage._senderAllowanceHookCall != rhs_storage._senderAllowanceHookCall {return false} + if _storage._receiverAllowanceHookCall != rhs_storage._receiverAllowanceHookCall {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/HieroProtobufs/Generated/services/contract_create.pb.swift b/Sources/HieroProtobufs/Generated/services/contract_create.pb.swift index 097dea26..f3db0fe2 100644 --- a/Sources/HieroProtobufs/Generated/services/contract_create.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/contract_create.pb.swift @@ -366,6 +366,13 @@ public struct Proto_ContractCreateTransactionBody: @unchecked Sendable { set {_uniqueStorage()._declineReward = newValue} } + ///* + /// Details of hooks to add immediately after creating this contract. + public var hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] { + get {return _storage._hookCreationDetails} + set {_uniqueStorage()._hookCreationDetails = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_InitcodeSource: Equatable, @unchecked Sendable { @@ -441,6 +448,7 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto 17: .standard(proto: "staked_account_id"), 18: .standard(proto: "staked_node_id"), 19: .standard(proto: "decline_reward"), + 20: .standard(proto: "hook_creation_details"), ] fileprivate class _StorageClass { @@ -459,6 +467,7 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto var _autoRenewAccountID: Proto_AccountID? = nil var _stakedID: Proto_ContractCreateTransactionBody.OneOf_StakedID? var _declineReward: Bool = false + var _hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] = [] #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -488,6 +497,7 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto _autoRenewAccountID = source._autoRenewAccountID _stakedID = source._stakedID _declineReward = source._declineReward + _hookCreationDetails = source._hookCreationDetails } } @@ -561,6 +571,7 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto } }() case 19: try { try decoder.decodeSingularBoolField(value: &_storage._declineReward) }() + case 20: try { try decoder.decodeRepeatedMessageField(value: &_storage._hookCreationDetails) }() default: break } } @@ -629,6 +640,9 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto if _storage._declineReward != false { try visitor.visitSingularBoolField(value: _storage._declineReward, fieldNumber: 19) } + if !_storage._hookCreationDetails.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._hookCreationDetails, fieldNumber: 20) + } } try unknownFields.traverse(visitor: &visitor) } @@ -653,6 +667,7 @@ extension Proto_ContractCreateTransactionBody: SwiftProtobuf.Message, SwiftProto if _storage._autoRenewAccountID != rhs_storage._autoRenewAccountID {return false} if _storage._stakedID != rhs_storage._stakedID {return false} if _storage._declineReward != rhs_storage._declineReward {return false} + if _storage._hookCreationDetails != rhs_storage._hookCreationDetails {return false} return true } if !storagesAreEqual {return false} diff --git a/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift b/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift index 2359cb85..89db36ea 100644 --- a/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift @@ -33,6 +33,9 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP ///* /// Context of an internal call in an EVM transaction that is not otherwise externalized.
+/// This message does not say anything about whether an EVM transaction is itself a logical +/// transaction in a Hiero transactional unit. It simply provides context on an internal +/// message call within an EVM transaction. public struct Proto_InternalCallContext: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -56,7 +59,7 @@ public struct Proto_InternalCallContext: @unchecked Sendable { } ///* -/// Results of executing EVM transaction.
+/// Results of executing a EVM transaction.
public struct Proto_EvmTransactionResult: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -99,7 +102,8 @@ public struct Proto_EvmTransactionResult: @unchecked Sendable { public var gasUsed: UInt64 = 0 ///* - /// If not already externalized, the context of the internal call producing this result. + /// If not already externalized in a transaction body, the context of the + /// internal call producing this result. public var internalCallContext: Proto_InternalCallContext { get {return _internalCallContext ?? Proto_InternalCallContext()} set {_internalCallContext = newValue} diff --git a/Sources/HieroProtobufs/Generated/services/contract_update.pb.swift b/Sources/HieroProtobufs/Generated/services/contract_update.pb.swift index e3f7ec94..5b540d3e 100644 --- a/Sources/HieroProtobufs/Generated/services/contract_update.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/contract_update.pb.swift @@ -276,6 +276,20 @@ public struct Proto_ContractUpdateTransactionBody: @unchecked Sendable { /// Clears the value of `declineReward`. Subsequent reads from it will return its default value. public mutating func clearDeclineReward() {_uniqueStorage()._declineReward = nil} + ///* + /// The ids the hooks to delete from the contract. + public var hookIdsToDelete: [Int64] { + get {return _storage._hookIdsToDelete} + set {_uniqueStorage()._hookIdsToDelete = newValue} + } + + ///* + /// The hooks to create for the contract. + public var hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] { + get {return _storage._hookCreationDetails} + set {_uniqueStorage()._hookCreationDetails = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// This should be condensed to just a field instead of a oneof and field 9 reserved. @@ -351,6 +365,8 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto 13: .standard(proto: "staked_account_id"), 14: .standard(proto: "staked_node_id"), 15: .standard(proto: "decline_reward"), + 16: .standard(proto: "hook_ids_to_delete"), + 17: .standard(proto: "hook_creation_details"), ] fileprivate class _StorageClass { @@ -365,6 +381,8 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto var _autoRenewAccountID: Proto_AccountID? = nil var _stakedID: Proto_ContractUpdateTransactionBody.OneOf_StakedID? var _declineReward: SwiftProtobuf.Google_Protobuf_BoolValue? = nil + var _hookIdsToDelete: [Int64] = [] + var _hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] = [] #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -390,6 +408,8 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto _autoRenewAccountID = source._autoRenewAccountID _stakedID = source._stakedID _declineReward = source._declineReward + _hookIdsToDelete = source._hookIdsToDelete + _hookCreationDetails = source._hookCreationDetails } } @@ -459,6 +479,8 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto } }() case 15: try { try decoder.decodeSingularMessageField(value: &_storage._declineReward) }() + case 16: try { try decoder.decodeRepeatedInt64Field(value: &_storage._hookIdsToDelete) }() + case 17: try { try decoder.decodeRepeatedMessageField(value: &_storage._hookCreationDetails) }() default: break } } @@ -520,6 +542,12 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto try { if let v = _storage._declineReward { try visitor.visitSingularMessageField(value: v, fieldNumber: 15) } }() + if !_storage._hookIdsToDelete.isEmpty { + try visitor.visitPackedInt64Field(value: _storage._hookIdsToDelete, fieldNumber: 16) + } + if !_storage._hookCreationDetails.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._hookCreationDetails, fieldNumber: 17) + } } try unknownFields.traverse(visitor: &visitor) } @@ -540,6 +568,8 @@ extension Proto_ContractUpdateTransactionBody: SwiftProtobuf.Message, SwiftProto if _storage._autoRenewAccountID != rhs_storage._autoRenewAccountID {return false} if _storage._stakedID != rhs_storage._stakedID {return false} if _storage._declineReward != rhs_storage._declineReward {return false} + if _storage._hookIdsToDelete != rhs_storage._hookIdsToDelete {return false} + if _storage._hookCreationDetails != rhs_storage._hookCreationDetails {return false} return true } if !storagesAreEqual {return false} diff --git a/Sources/HieroProtobufs/Generated/services/crypto_create.pb.swift b/Sources/HieroProtobufs/Generated/services/crypto_create.pb.swift index b282756e..6df603ff 100644 --- a/Sources/HieroProtobufs/Generated/services/crypto_create.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/crypto_create.pb.swift @@ -282,6 +282,13 @@ public struct Proto_CryptoCreateTransactionBody: @unchecked Sendable { set {_uniqueStorage()._alias = newValue} } + ///* + /// Details of hooks to add immediately after creating this account. + public var hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] { + get {return _storage._hookCreationDetails} + set {_uniqueStorage()._hookCreationDetails = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_StakedID: Equatable, Sendable { @@ -331,6 +338,7 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu 16: .standard(proto: "staked_node_id"), 17: .standard(proto: "decline_reward"), 18: .same(proto: "alias"), + 19: .standard(proto: "hook_creation_details"), ] fileprivate class _StorageClass { @@ -349,6 +357,7 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu var _stakedID: Proto_CryptoCreateTransactionBody.OneOf_StakedID? var _declineReward: Bool = false var _alias: Data = Data() + var _hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] = [] #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -378,6 +387,7 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu _stakedID = source._stakedID _declineReward = source._declineReward _alias = source._alias + _hookCreationDetails = source._hookCreationDetails } } @@ -431,6 +441,7 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu }() case 17: try { try decoder.decodeSingularBoolField(value: &_storage._declineReward) }() case 18: try { try decoder.decodeSingularBytesField(value: &_storage._alias) }() + case 19: try { try decoder.decodeRepeatedMessageField(value: &_storage._hookCreationDetails) }() default: break } } @@ -496,6 +507,9 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu if !_storage._alias.isEmpty { try visitor.visitSingularBytesField(value: _storage._alias, fieldNumber: 18) } + if !_storage._hookCreationDetails.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._hookCreationDetails, fieldNumber: 19) + } } try unknownFields.traverse(visitor: &visitor) } @@ -520,6 +534,7 @@ extension Proto_CryptoCreateTransactionBody: SwiftProtobuf.Message, SwiftProtobu if _storage._stakedID != rhs_storage._stakedID {return false} if _storage._declineReward != rhs_storage._declineReward {return false} if _storage._alias != rhs_storage._alias {return false} + if _storage._hookCreationDetails != rhs_storage._hookCreationDetails {return false} return true } if !storagesAreEqual {return false} diff --git a/Sources/HieroProtobufs/Generated/services/crypto_update.pb.swift b/Sources/HieroProtobufs/Generated/services/crypto_update.pb.swift index 890efec0..ffa4628e 100644 --- a/Sources/HieroProtobufs/Generated/services/crypto_update.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/crypto_update.pb.swift @@ -335,6 +335,20 @@ public struct Proto_CryptoUpdateTransactionBody: @unchecked Sendable { /// Clears the value of `declineReward`. Subsequent reads from it will return its default value. public mutating func clearDeclineReward() {_uniqueStorage()._declineReward = nil} + ///* + /// The ids the hooks to delete from the account. + public var hookIdsToDelete: [Int64] { + get {return _storage._hookIdsToDelete} + set {_uniqueStorage()._hookIdsToDelete = newValue} + } + + ///* + /// The hooks to create for the account. + public var hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] { + get {return _storage._hookCreationDetails} + set {_uniqueStorage()._hookCreationDetails = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() /// This entire oneOf is deprecated, and the concept is not implemented. @@ -447,6 +461,8 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu 16: .standard(proto: "staked_account_id"), 17: .standard(proto: "staked_node_id"), 18: .standard(proto: "decline_reward"), + 19: .standard(proto: "hook_ids_to_delete"), + 20: .standard(proto: "hook_creation_details"), ] fileprivate class _StorageClass { @@ -463,6 +479,8 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu var _maxAutomaticTokenAssociations: SwiftProtobuf.Google_Protobuf_Int32Value? = nil var _stakedID: Proto_CryptoUpdateTransactionBody.OneOf_StakedID? var _declineReward: SwiftProtobuf.Google_Protobuf_BoolValue? = nil + var _hookIdsToDelete: [Int64] = [] + var _hookCreationDetails: [Com_Hedera_Hapi_Node_Hooks_HookCreationDetails] = [] #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -490,6 +508,8 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu _maxAutomaticTokenAssociations = source._maxAutomaticTokenAssociations _stakedID = source._stakedID _declineReward = source._declineReward + _hookIdsToDelete = source._hookIdsToDelete + _hookCreationDetails = source._hookCreationDetails } } @@ -601,6 +621,8 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu } }() case 18: try { try decoder.decodeSingularMessageField(value: &_storage._declineReward) }() + case 19: try { try decoder.decodeRepeatedInt64Field(value: &_storage._hookIdsToDelete) }() + case 20: try { try decoder.decodeRepeatedMessageField(value: &_storage._hookCreationDetails) }() default: break } } @@ -669,6 +691,12 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu try { if let v = _storage._declineReward { try visitor.visitSingularMessageField(value: v, fieldNumber: 18) } }() + if !_storage._hookIdsToDelete.isEmpty { + try visitor.visitPackedInt64Field(value: _storage._hookIdsToDelete, fieldNumber: 19) + } + if !_storage._hookCreationDetails.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._hookCreationDetails, fieldNumber: 20) + } } try unknownFields.traverse(visitor: &visitor) } @@ -691,6 +719,8 @@ extension Proto_CryptoUpdateTransactionBody: SwiftProtobuf.Message, SwiftProtobu if _storage._maxAutomaticTokenAssociations != rhs_storage._maxAutomaticTokenAssociations {return false} if _storage._stakedID != rhs_storage._stakedID {return false} if _storage._declineReward != rhs_storage._declineReward {return false} + if _storage._hookIdsToDelete != rhs_storage._hookIdsToDelete {return false} + if _storage._hookCreationDetails != rhs_storage._hookCreationDetails {return false} return true } if !storagesAreEqual {return false} diff --git a/Sources/HieroProtobufs/Generated/services/hook_dispatch.pb.swift b/Sources/HieroProtobufs/Generated/services/hook_dispatch.pb.swift new file mode 100644 index 00000000..e014fa71 --- /dev/null +++ b/Sources/HieroProtobufs/Generated/services/hook_dispatch.pb.swift @@ -0,0 +1,249 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: services/hook_dispatch.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +///* +/// Dispatches a hook action to an appropriate service. +public struct Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var action: Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody.OneOf_Action? = nil + + ///* + /// The id of the hook to delete. + public var hookIDToDelete: Proto_HookId { + get { + if case .hookIDToDelete(let v)? = action {return v} + return Proto_HookId() + } + set {action = .hookIDToDelete(newValue)} + } + + ///* + /// Creation details for a new hook. + public var creation: Com_Hedera_Hapi_Node_Hooks_HookCreation { + get { + if case .creation(let v)? = action {return v} + return Com_Hedera_Hapi_Node_Hooks_HookCreation() + } + set {action = .creation(newValue)} + } + + ///* + /// Execution details of an existing hook. + public var execution: Com_Hedera_Hapi_Node_Hooks_HookExecution { + get { + if case .execution(let v)? = action {return v} + return Com_Hedera_Hapi_Node_Hooks_HookExecution() + } + set {action = .execution(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Action: Equatable, Sendable { + ///* + /// The id of the hook to delete. + case hookIDToDelete(Proto_HookId) + ///* + /// Creation details for a new hook. + case creation(Com_Hedera_Hapi_Node_Hooks_HookCreation) + ///* + /// Execution details of an existing hook. + case execution(Com_Hedera_Hapi_Node_Hooks_HookExecution) + + } + + public init() {} +} + +///* +/// Details the execution of a hook. +public struct Com_Hedera_Hapi_Node_Hooks_HookExecution: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The id of the hook's entity. + public var hookEntityID: Proto_HookEntityId { + get {return _hookEntityID ?? Proto_HookEntityId()} + set {_hookEntityID = newValue} + } + /// Returns true if `hookEntityID` has been explicitly set. + public var hasHookEntityID: Bool {return self._hookEntityID != nil} + /// Clears the value of `hookEntityID`. Subsequent reads from it will return its default value. + public mutating func clearHookEntityID() {self._hookEntityID = nil} + + ///* + /// The details of the call, including which hook id to call. + public var call: Proto_HookCall { + get {return _call ?? Proto_HookCall()} + set {_call = newValue} + } + /// Returns true if `call` has been explicitly set. + public var hasCall: Bool {return self._call != nil} + /// Clears the value of `call`. Subsequent reads from it will return its default value. + public mutating func clearCall() {self._call = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _hookEntityID: Proto_HookEntityId? = nil + fileprivate var _call: Proto_HookCall? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "com.hedera.hapi.node.hooks" + +extension Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookDispatchTransactionBody" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "hook_id_to_delete"), + 2: .same(proto: "creation"), + 3: .same(proto: "execution"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Proto_HookId? + var hadOneofValue = false + if let current = self.action { + hadOneofValue = true + if case .hookIDToDelete(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.action = .hookIDToDelete(v) + } + }() + case 2: try { + var v: Com_Hedera_Hapi_Node_Hooks_HookCreation? + var hadOneofValue = false + if let current = self.action { + hadOneofValue = true + if case .creation(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.action = .creation(v) + } + }() + case 3: try { + var v: Com_Hedera_Hapi_Node_Hooks_HookExecution? + var hadOneofValue = false + if let current = self.action { + hadOneofValue = true + if case .execution(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.action = .execution(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.action { + case .hookIDToDelete?: try { + guard case .hookIDToDelete(let v)? = self.action else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .creation?: try { + guard case .creation(let v)? = self.action else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .execution?: try { + guard case .execution(let v)? = self.action else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody, rhs: Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody) -> Bool { + if lhs.action != rhs.action {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_HookExecution: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookExecution" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "hook_entity_id"), + 2: .same(proto: "call"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._hookEntityID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._call) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._hookEntityID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._call { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_HookExecution, rhs: Com_Hedera_Hapi_Node_Hooks_HookExecution) -> Bool { + if lhs._hookEntityID != rhs._hookEntityID {return false} + if lhs._call != rhs._call {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift b/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift new file mode 100644 index 00000000..457b2655 --- /dev/null +++ b/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift @@ -0,0 +1,917 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: services/hook_types.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +///** +/// The Hiero extension points that accept a hook. +public enum Com_Hedera_Hapi_Node_Hooks_HookExtensionPoint: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + + ///* + /// Used to customize an account's allowances during a CryptoTransfer transaction. + case accountAllowanceHook // = 0 + case UNRECOGNIZED(Int) + + public init() { + self = .accountAllowanceHook + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .accountAllowanceHook + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .accountAllowanceHook: return 0 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Com_Hedera_Hapi_Node_Hooks_HookExtensionPoint] = [ + .accountAllowanceHook, + ] + +} + +///* +/// Specifies the creation of a new hook at the given id for the given entity. +public struct Com_Hedera_Hapi_Node_Hooks_HookCreation: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The id of the hook's entity. + public var entityID: Proto_HookEntityId { + get {return _entityID ?? Proto_HookEntityId()} + set {_entityID = newValue} + } + /// Returns true if `entityID` has been explicitly set. + public var hasEntityID: Bool {return self._entityID != nil} + /// Clears the value of `entityID`. Subsequent reads from it will return its default value. + public mutating func clearEntityID() {self._entityID = nil} + + ///* + /// The creation details. + public var details: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails { + get {return _details ?? Com_Hedera_Hapi_Node_Hooks_HookCreationDetails()} + set {_details = newValue} + } + /// Returns true if `details` has been explicitly set. + public var hasDetails: Bool {return self._details != nil} + /// Clears the value of `details`. Subsequent reads from it will return its default value. + public mutating func clearDetails() {self._details = nil} + + ///* + /// If set, the id of the hook following this one in the owner's + /// doubly-linked list of hooks. + public var nextHookID: SwiftProtobuf.Google_Protobuf_Int64Value { + get {return _nextHookID ?? SwiftProtobuf.Google_Protobuf_Int64Value()} + set {_nextHookID = newValue} + } + /// Returns true if `nextHookID` has been explicitly set. + public var hasNextHookID: Bool {return self._nextHookID != nil} + /// Clears the value of `nextHookID`. Subsequent reads from it will return its default value. + public mutating func clearNextHookID() {self._nextHookID = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _entityID: Proto_HookEntityId? = nil + fileprivate var _details: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails? = nil + fileprivate var _nextHookID: SwiftProtobuf.Google_Protobuf_Int64Value? = nil +} + +///** +/// The details of a hook's creation. +public struct Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The extension point for the hook. + public var extensionPoint: Com_Hedera_Hapi_Node_Hooks_HookExtensionPoint = .accountAllowanceHook + + ///* + /// The id to create the hook at. + public var hookID: Int64 = 0 + + ///* + /// The hook implementation. + public var hook: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails.OneOf_Hook? = nil + + ///* + /// A hook programmed in EVM bytecode that does not require access to state + /// or interactions with external contracts. + public var pureEvmHook: Com_Hedera_Hapi_Node_Hooks_PureEvmHook { + get { + if case .pureEvmHook(let v)? = hook {return v} + return Com_Hedera_Hapi_Node_Hooks_PureEvmHook() + } + set {hook = .pureEvmHook(newValue)} + } + + ///* + /// A hook programmed in EVM bytecode that may access state or interact with + /// external contracts. + public var lambdaEvmHook: Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook { + get { + if case .lambdaEvmHook(let v)? = hook {return v} + return Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook() + } + set {hook = .lambdaEvmHook(newValue)} + } + + ///* + /// If set, a key that that can be used to remove or replace the hook; or (if + /// applicable, as with a lambda EVM hook) perform transactions that customize + /// the hook. + public var adminKey: Proto_Key { + get {return _adminKey ?? Proto_Key()} + set {_adminKey = newValue} + } + /// Returns true if `adminKey` has been explicitly set. + public var hasAdminKey: Bool {return self._adminKey != nil} + /// Clears the value of `adminKey`. Subsequent reads from it will return its default value. + public mutating func clearAdminKey() {self._adminKey = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + ///* + /// The hook implementation. + public enum OneOf_Hook: Equatable, Sendable { + ///* + /// A hook programmed in EVM bytecode that does not require access to state + /// or interactions with external contracts. + case pureEvmHook(Com_Hedera_Hapi_Node_Hooks_PureEvmHook) + ///* + /// A hook programmed in EVM bytecode that may access state or interact with + /// external contracts. + case lambdaEvmHook(Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook) + + } + + public init() {} + + fileprivate var _adminKey: Proto_Key? = nil +} + +///* +/// Definition of a lambda EVM hook. +public struct Com_Hedera_Hapi_Node_Hooks_PureEvmHook: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The specification for the hook. + public var spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec { + get {return _spec ?? Com_Hedera_Hapi_Node_Hooks_EvmHookSpec()} + set {_spec = newValue} + } + /// Returns true if `spec` has been explicitly set. + public var hasSpec: Bool {return self._spec != nil} + /// Clears the value of `spec`. Subsequent reads from it will return its default value. + public mutating func clearSpec() {self._spec = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec? = nil +} + +///* +/// Definition of a lambda EVM hook. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The specification for the hook. + public var spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec { + get {return _spec ?? Com_Hedera_Hapi_Node_Hooks_EvmHookSpec()} + set {_spec = newValue} + } + /// Returns true if `spec` has been explicitly set. + public var hasSpec: Bool {return self._spec != nil} + /// Clears the value of `spec`. Subsequent reads from it will return its default value. + public mutating func clearSpec() {self._spec = nil} + + ///* + /// Initial storage updates for the lambda, if any. + public var storageUpdates: [Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec? = nil +} + +///* +/// Shared specifications for an EVM hook. May be used for any extension point. +public struct Com_Hedera_Hapi_Node_Hooks_EvmHookSpec: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The source of the EVM bytecode for the hook. + public var bytecodeSource: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec.OneOf_BytecodeSource? = nil + + ///* + /// The id of a contract that implements the extension point API with EVM bytecode. + public var contractID: Proto_ContractID { + get { + if case .contractID(let v)? = bytecodeSource {return v} + return Proto_ContractID() + } + set {bytecodeSource = .contractID(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + ///* + /// The source of the EVM bytecode for the hook. + public enum OneOf_BytecodeSource: Equatable, Sendable { + ///* + /// The id of a contract that implements the extension point API with EVM bytecode. + case contractID(Proto_ContractID) + + } + + public init() {} +} + +///* +/// Specifies a key/value pair in the storage of a lambda, either by the explicit storage +/// slot contents; or by a combination of a Solidity mapping's slot key and the key into +/// that mapping. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var update: Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate.OneOf_Update? = nil + + ///* + /// An explicit storage slot update. + public var storageSlot: Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot { + get { + if case .storageSlot(let v)? = update {return v} + return Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot() + } + set {update = .storageSlot(newValue)} + } + + ///* + /// Implicit storage slot updates specified as Solidity mapping entries. + public var mappingEntries: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries { + get { + if case .mappingEntries(let v)? = update {return v} + return Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries() + } + set {update = .mappingEntries(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Update: Equatable, Sendable { + ///* + /// An explicit storage slot update. + case storageSlot(Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot) + ///* + /// Implicit storage slot updates specified as Solidity mapping entries. + case mappingEntries(Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries) + + } + + public init() {} +} + +///* +/// Specifies storage slot updates via indirection into a Solidity mapping. +///

+/// Concretely, if the Solidity mapping is itself at slot `mapping_slot`, then +/// the * storage slot for key `key` in the mapping is defined by the relationship +/// `key_storage_slot = keccak256(abi.encodePacked(mapping_slot, key))`. +///

+/// This message lets a metaprotocol be specified in terms of changes to a +/// Solidity mapping's entries. If only raw slots could be updated, then a block +/// stream consumer following the metaprotocol would have to invert the Keccak256 +/// hash to determine which mapping entry was being updated, which is not possible. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The slot that corresponds to the Solidity mapping itself. Must use a + /// minimal byte representation (no leading zeros). + public var mappingSlot: Data = Data() + + ///* + /// The entries in the mapping at the given slot. + public var entries: [Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +///* +/// An entry in a Solidity mapping. Very helpful for protocols that apply +/// `LambdaSStore` to manage the entries of a hook contract's mapping instead +/// its raw storage slots. +///

+/// This is especially attractive when the mapping value itself fits in a single +/// word; for more complicated value storage layouts it becomes necessary to +/// combine the mapping update with additional `LambdaStorageSlot` updates that +/// specify the complete storage slots of the value type. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var entryKey: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry.OneOf_EntryKey? = nil + + ///* + /// The explicit bytes of the mapping entry. Must use a minimal byte representation; + /// may not exceed 32 bytes in length. + public var key: Data { + get { + if case .key(let v)? = entryKey {return v} + return Data() + } + set {entryKey = .key(newValue)} + } + + ///* + /// The bytes that are the preimage of the Keccak256 hash that forms the mapping key. + /// May be longer or shorter than 32 bytes and may have leading zeros, since Solidity + /// supports variable-length keys in mappings. + public var preimage: Data { + get { + if case .preimage(let v)? = entryKey {return v} + return Data() + } + set {entryKey = .preimage(newValue)} + } + + ///* + /// If the mapping entry is present and non-zero, its value. May not be longer than + /// 32 bytes in length; must use a minimal byte representation (no leading zeros). + /// Leaving this field empty in an update removes the entry from the mapping. + public var value: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_EntryKey: Equatable, @unchecked Sendable { + ///* + /// The explicit bytes of the mapping entry. Must use a minimal byte representation; + /// may not exceed 32 bytes in length. + case key(Data) + ///* + /// The bytes that are the preimage of the Keccak256 hash that forms the mapping key. + /// May be longer or shorter than 32 bytes and may have leading zeros, since Solidity + /// supports variable-length keys in mappings. + case preimage(Data) + + } + + public init() {} +} + +///* +/// A slot in the storage of a lambda EVM hook. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The key of the slot. Must use a minimal byte representation (no + /// leading zeros); may not exceed 32 bytes in length. + public var key: Data = Data() + + ///* + /// If the slot is present and non-zero, its value. Must use a minimal + /// byte representation (no leading zeros); may not exceed 32 bytes in + /// length Leaving this field empty in an update removes the slot from + /// storage. + public var value: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "com.hedera.hapi.node.hooks" + +extension Com_Hedera_Hapi_Node_Hooks_HookExtensionPoint: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "ACCOUNT_ALLOWANCE_HOOK"), + ] +} + +extension Com_Hedera_Hapi_Node_Hooks_HookCreation: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookCreation" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "entity_id"), + 2: .same(proto: "details"), + 9: .standard(proto: "next_hook_id"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._entityID) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._details) }() + case 9: try { try decoder.decodeSingularMessageField(value: &self._nextHookID) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._entityID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._details { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._nextHookID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_HookCreation, rhs: Com_Hedera_Hapi_Node_Hooks_HookCreation) -> Bool { + if lhs._entityID != rhs._entityID {return false} + if lhs._details != rhs._details {return false} + if lhs._nextHookID != rhs._nextHookID {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HookCreationDetails" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "extension_point"), + 2: .standard(proto: "hook_id"), + 3: .standard(proto: "pure_evm_hook"), + 4: .standard(proto: "lambda_evm_hook"), + 5: .standard(proto: "admin_key"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.extensionPoint) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self.hookID) }() + case 3: try { + var v: Com_Hedera_Hapi_Node_Hooks_PureEvmHook? + var hadOneofValue = false + if let current = self.hook { + hadOneofValue = true + if case .pureEvmHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.hook = .pureEvmHook(v) + } + }() + case 4: try { + var v: Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook? + var hadOneofValue = false + if let current = self.hook { + hadOneofValue = true + if case .lambdaEvmHook(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.hook = .lambdaEvmHook(v) + } + }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._adminKey) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.extensionPoint != .accountAllowanceHook { + try visitor.visitSingularEnumField(value: self.extensionPoint, fieldNumber: 1) + } + if self.hookID != 0 { + try visitor.visitSingularInt64Field(value: self.hookID, fieldNumber: 2) + } + switch self.hook { + case .pureEvmHook?: try { + guard case .pureEvmHook(let v)? = self.hook else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .lambdaEvmHook?: try { + guard case .lambdaEvmHook(let v)? = self.hook else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + try { if let v = self._adminKey { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails, rhs: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails) -> Bool { + if lhs.extensionPoint != rhs.extensionPoint {return false} + if lhs.hookID != rhs.hookID {return false} + if lhs.hook != rhs.hook {return false} + if lhs._adminKey != rhs._adminKey {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_PureEvmHook: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".PureEvmHook" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "spec"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._spec) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._spec { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_PureEvmHook, rhs: Com_Hedera_Hapi_Node_Hooks_PureEvmHook) -> Bool { + if lhs._spec != rhs._spec {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaEvmHook" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "spec"), + 2: .standard(proto: "storage_updates"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._spec) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.storageUpdates) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._spec { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.storageUpdates.isEmpty { + try visitor.visitRepeatedMessageField(value: self.storageUpdates, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook) -> Bool { + if lhs._spec != rhs._spec {return false} + if lhs.storageUpdates != rhs.storageUpdates {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_EvmHookSpec: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".EvmHookSpec" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "contract_id"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Proto_ContractID? + var hadOneofValue = false + if let current = self.bytecodeSource { + hadOneofValue = true + if case .contractID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.bytecodeSource = .contractID(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if case .contractID(let v)? = self.bytecodeSource { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec, rhs: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec) -> Bool { + if lhs.bytecodeSource != rhs.bytecodeSource {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaStorageUpdate" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "storage_slot"), + 2: .standard(proto: "mapping_entries"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot? + var hadOneofValue = false + if let current = self.update { + hadOneofValue = true + if case .storageSlot(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.update = .storageSlot(v) + } + }() + case 2: try { + var v: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries? + var hadOneofValue = false + if let current = self.update { + hadOneofValue = true + if case .mappingEntries(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.update = .mappingEntries(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.update { + case .storageSlot?: try { + guard case .storageSlot(let v)? = self.update else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .mappingEntries?: try { + guard case .mappingEntries(let v)? = self.update else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate) -> Bool { + if lhs.update != rhs.update {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaMappingEntries" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "mapping_slot"), + 2: .same(proto: "entries"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.mappingSlot) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.entries) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.mappingSlot.isEmpty { + try visitor.visitSingularBytesField(value: self.mappingSlot, fieldNumber: 1) + } + if !self.entries.isEmpty { + try visitor.visitRepeatedMessageField(value: self.entries, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries) -> Bool { + if lhs.mappingSlot != rhs.mappingSlot {return false} + if lhs.entries != rhs.entries {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaMappingEntry" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "key"), + 2: .same(proto: "preimage"), + 3: .same(proto: "value"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Data? + try decoder.decodeSingularBytesField(value: &v) + if let v = v { + if self.entryKey != nil {try decoder.handleConflictingOneOf()} + self.entryKey = .key(v) + } + }() + case 2: try { + var v: Data? + try decoder.decodeSingularBytesField(value: &v) + if let v = v { + if self.entryKey != nil {try decoder.handleConflictingOneOf()} + self.entryKey = .preimage(v) + } + }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.value) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.entryKey { + case .key?: try { + guard case .key(let v)? = self.entryKey else { preconditionFailure() } + try visitor.visitSingularBytesField(value: v, fieldNumber: 1) + }() + case .preimage?: try { + guard case .preimage(let v)? = self.entryKey else { preconditionFailure() } + try visitor.visitSingularBytesField(value: v, fieldNumber: 2) + }() + case nil: break + } + if !self.value.isEmpty { + try visitor.visitSingularBytesField(value: self.value, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry) -> Bool { + if lhs.entryKey != rhs.entryKey {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaStorageSlot" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "key"), + 2: .same(proto: "value"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.key) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.value) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.key.isEmpty { + try visitor.visitSingularBytesField(value: self.key, fieldNumber: 1) + } + if !self.value.isEmpty { + try visitor.visitSingularBytesField(value: self.value, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot) -> Bool { + if lhs.key != rhs.key {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/HieroProtobufs/Generated/services/lambda_sstore.pb.swift b/Sources/HieroProtobufs/Generated/services/lambda_sstore.pb.swift new file mode 100644 index 00000000..6337bb44 --- /dev/null +++ b/Sources/HieroProtobufs/Generated/services/lambda_sstore.pb.swift @@ -0,0 +1,96 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: services/lambda_sstore.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +///* +/// Adds or removes key/value pairs in the storage of a lambda. The lambda's owning key must sign the transaction. +public struct Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + ///* + /// The id of the lambda EVM hook whose storage is being updated. + public var hookID: Proto_HookId { + get {return _hookID ?? Proto_HookId()} + set {_hookID = newValue} + } + /// Returns true if `hookID` has been explicitly set. + public var hasHookID: Bool {return self._hookID != nil} + /// Clears the value of `hookID`. Subsequent reads from it will return its default value. + public mutating func clearHookID() {self._hookID = nil} + + ///* + /// The updates to the storage of the lambda. + public var storageUpdates: [Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _hookID: Proto_HookId? = nil +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "com.hedera.hapi.node.hooks" + +extension Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".LambdaSStoreTransactionBody" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "hook_id"), + 2: .standard(proto: "storage_updates"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._hookID) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.storageUpdates) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._hookID { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.storageUpdates.isEmpty { + try visitor.visitRepeatedMessageField(value: self.storageUpdates, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody, rhs: Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody) -> Bool { + if lhs._hookID != rhs._hookID {return false} + if lhs.storageUpdates != rhs.storageUpdates {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/HieroProtobufs/Generated/services/node_update.pb.swift b/Sources/HieroProtobufs/Generated/services/node_update.pb.swift index 58e12046..d39d992d 100644 --- a/Sources/HieroProtobufs/Generated/services/node_update.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/node_update.pb.swift @@ -208,7 +208,9 @@ public struct Com_Hedera_Hapi_Node_Addressbook_NodeUpdateTransactionBody: Sendab /// This endpoint MUST use a valid port and SHALL be reachable over TLS.
/// This field MAY be omitted if the node does not support gRPC-Web access.
/// This field MUST be updated if the gRPC-Web endpoint changes.
- /// This field SHALL enable frontend clients to avoid hard-coded proxy endpoints. + /// This field SHALL enable frontend clients to avoid hard-coded proxy endpoints.
+ /// This field MAY be set to `ServiceEndpoint.DEFAULT` to remove a previously-valid + /// web proxy. public var grpcProxyEndpoint: Proto_ServiceEndpoint { get {return _grpcProxyEndpoint ?? Proto_ServiceEndpoint()} set {_grpcProxyEndpoint = newValue} diff --git a/Sources/HieroProtobufs/Generated/services/response_code.pb.swift b/Sources/HieroProtobufs/Generated/services/response_code.pb.swift index 23a7c499..1e93b190 100644 --- a/Sources/HieroProtobufs/Generated/services/response_code.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/response_code.pb.swift @@ -1546,6 +1546,72 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { /// The GRPC proxy endpoint is set in the NodeCreate or NodeUpdate transaction, /// which the network does not support. case grpcWebProxyNotSupported // = 399 + + ///* + /// An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. + case nftTransfersOnlyAllowedForNonFungibleUnique // = 400 + + ///* + /// A HAPI client cannot set the SignedTransaction#use_serialized_tx_message_hash_algorithm field. + case invalidSerializedTxMessageHashAlgorithm // = 401 + + ///* + /// An EVM hook execution was throttled due to high network gas utilization. + case evmHookGasThrottled // = 500 + + ///* + /// A user tried to create a hook with an id already in use. + case hookIDInUse // = 501 + + ///* + /// A transaction tried to execute a hook that did not match the specified + /// type or was malformed in some other way. + case badHookRequest // = 502 + + ///* + /// A CryptoTransfer relying on a ACCOUNT_ALLOWANCE hook was rejected. + case rejectedByAccountAllowanceHook // = 503 + + ///* + /// A hook id was not found. + case hookNotFound // = 504 + + ///* + /// A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + case lambdaStorageUpdateBytesTooLong // = 505 + + ///* + /// A lambda mapping slot, storage key, or storage value failed to use the + /// minimal representation (i.e., no leading zeros). + case lambdaStorageUpdateBytesMustUseMinimalRepresentation // = 506 + + ///* + /// A hook id was invalid. + case invalidHookID // = 507 + + ///* + /// A lambda storage update had no contents. + case emptyLambdaStorageUpdate // = 508 + + ///* + /// A user repeated the same hook id in a creation details list. + case hookIDRepeatedInCreationDetails // = 509 + + ///* + /// Hooks are not not enabled on the target Hiero network. + case hooksNotEnabled // = 510 + + ///* + /// The target hook is not a lambda. + case hookIsNotALambda // = 511 + + ///* + /// A hook was deleted. + case hookDeleted // = 512 + + ///* + /// The LambdaSStore tried to update too many storage slots in a single transaction. + case tooManyLambdaStorageUpdates // = 513 case UNRECOGNIZED(Int) public init() { @@ -1912,6 +1978,22 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { case 397: self = .throttleGroupLcmOverflow case 398: self = .airdropContainsMultipleSendersForAToken case 399: self = .grpcWebProxyNotSupported + case 400: self = .nftTransfersOnlyAllowedForNonFungibleUnique + case 401: self = .invalidSerializedTxMessageHashAlgorithm + case 500: self = .evmHookGasThrottled + case 501: self = .hookIDInUse + case 502: self = .badHookRequest + case 503: self = .rejectedByAccountAllowanceHook + case 504: self = .hookNotFound + case 505: self = .lambdaStorageUpdateBytesTooLong + case 506: self = .lambdaStorageUpdateBytesMustUseMinimalRepresentation + case 507: self = .invalidHookID + case 508: self = .emptyLambdaStorageUpdate + case 509: self = .hookIDRepeatedInCreationDetails + case 510: self = .hooksNotEnabled + case 511: self = .hookIsNotALambda + case 512: self = .hookDeleted + case 513: self = .tooManyLambdaStorageUpdates default: self = .UNRECOGNIZED(rawValue) } } @@ -2276,6 +2358,22 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { case .throttleGroupLcmOverflow: return 397 case .airdropContainsMultipleSendersForAToken: return 398 case .grpcWebProxyNotSupported: return 399 + case .nftTransfersOnlyAllowedForNonFungibleUnique: return 400 + case .invalidSerializedTxMessageHashAlgorithm: return 401 + case .evmHookGasThrottled: return 500 + case .hookIDInUse: return 501 + case .badHookRequest: return 502 + case .rejectedByAccountAllowanceHook: return 503 + case .hookNotFound: return 504 + case .lambdaStorageUpdateBytesTooLong: return 505 + case .lambdaStorageUpdateBytesMustUseMinimalRepresentation: return 506 + case .invalidHookID: return 507 + case .emptyLambdaStorageUpdate: return 508 + case .hookIDRepeatedInCreationDetails: return 509 + case .hooksNotEnabled: return 510 + case .hookIsNotALambda: return 511 + case .hookDeleted: return 512 + case .tooManyLambdaStorageUpdates: return 513 case .UNRECOGNIZED(let i): return i } } @@ -2640,6 +2738,22 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { .throttleGroupLcmOverflow, .airdropContainsMultipleSendersForAToken, .grpcWebProxyNotSupported, + .nftTransfersOnlyAllowedForNonFungibleUnique, + .invalidSerializedTxMessageHashAlgorithm, + .evmHookGasThrottled, + .hookIDInUse, + .badHookRequest, + .rejectedByAccountAllowanceHook, + .hookNotFound, + .lambdaStorageUpdateBytesTooLong, + .lambdaStorageUpdateBytesMustUseMinimalRepresentation, + .invalidHookID, + .emptyLambdaStorageUpdate, + .hookIDRepeatedInCreationDetails, + .hooksNotEnabled, + .hookIsNotALambda, + .hookDeleted, + .tooManyLambdaStorageUpdates, ] } @@ -3006,5 +3120,21 @@ extension Proto_ResponseCodeEnum: SwiftProtobuf._ProtoNameProviding { 397: .same(proto: "THROTTLE_GROUP_LCM_OVERFLOW"), 398: .same(proto: "AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN"), 399: .same(proto: "GRPC_WEB_PROXY_NOT_SUPPORTED"), + 400: .same(proto: "NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE"), + 401: .same(proto: "INVALID_SERIALIZED_TX_MESSAGE_HASH_ALGORITHM"), + 500: .same(proto: "EVM_HOOK_GAS_THROTTLED"), + 501: .same(proto: "HOOK_ID_IN_USE"), + 502: .same(proto: "BAD_HOOK_REQUEST"), + 503: .same(proto: "REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK"), + 504: .same(proto: "HOOK_NOT_FOUND"), + 505: .same(proto: "LAMBDA_STORAGE_UPDATE_BYTES_TOO_LONG"), + 506: .same(proto: "LAMBDA_STORAGE_UPDATE_BYTES_MUST_USE_MINIMAL_REPRESENTATION"), + 507: .same(proto: "INVALID_HOOK_ID"), + 508: .same(proto: "EMPTY_LAMBDA_STORAGE_UPDATE"), + 509: .same(proto: "HOOK_ID_REPEATED_IN_CREATION_DETAILS"), + 510: .same(proto: "HOOKS_NOT_ENABLED"), + 511: .same(proto: "HOOK_IS_NOT_A_LAMBDA"), + 512: .same(proto: "HOOK_DELETED"), + 513: .same(proto: "TOO_MANY_LAMBDA_STORAGE_UPDATES"), ] } diff --git a/Sources/HieroProtobufs/Generated/services/schedulable_transaction_body.pb.swift b/Sources/HieroProtobufs/Generated/services/schedulable_transaction_body.pb.swift index 3461ffcb..bc587e53 100644 --- a/Sources/HieroProtobufs/Generated/services/schedulable_transaction_body.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/schedulable_transaction_body.pb.swift @@ -602,6 +602,17 @@ public struct Proto_SchedulableTransactionBody: @unchecked Sendable { set {_uniqueStorage()._data = .tokenAirdrop(newValue)} } + ///* + /// A list of maximum custom fees that the users are willing to pay. + ///

+ /// This field is OPTIONAL.
+ /// If left empty, the users are accepting to pay any custom fee.
+ /// If used with a transaction type that does not support custom fee limits, the transaction will fail. + public var maxCustomFees: [Proto_CustomFeeLimit] { + get {return _storage._maxCustomFees} + set {_uniqueStorage()._maxCustomFees = newValue} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Data: Equatable, Sendable { @@ -878,12 +889,14 @@ extension Proto_SchedulableTransactionBody: SwiftProtobuf.Message, SwiftProtobuf 46: .same(proto: "tokenCancelAirdrop"), 47: .same(proto: "tokenClaimAirdrop"), 48: .same(proto: "tokenAirdrop"), + 1001: .standard(proto: "max_custom_fees"), ] fileprivate class _StorageClass { var _transactionFee: UInt64 = 0 var _memo: String = String() var _data: Proto_SchedulableTransactionBody.OneOf_Data? + var _maxCustomFees: [Proto_CustomFeeLimit] = [] #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -901,6 +914,7 @@ extension Proto_SchedulableTransactionBody: SwiftProtobuf.Message, SwiftProtobuf _transactionFee = source._transactionFee _memo = source._memo _data = source._data + _maxCustomFees = source._maxCustomFees } } @@ -1519,6 +1533,7 @@ extension Proto_SchedulableTransactionBody: SwiftProtobuf.Message, SwiftProtobuf _storage._data = .tokenAirdrop(v) } }() + case 1001: try { try decoder.decodeRepeatedMessageField(value: &_storage._maxCustomFees) }() default: break } } @@ -1724,6 +1739,9 @@ extension Proto_SchedulableTransactionBody: SwiftProtobuf.Message, SwiftProtobuf }() case nil: break } + if !_storage._maxCustomFees.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._maxCustomFees, fieldNumber: 1001) + } } try unknownFields.traverse(visitor: &visitor) } @@ -1736,6 +1754,7 @@ extension Proto_SchedulableTransactionBody: SwiftProtobuf.Message, SwiftProtobuf if _storage._transactionFee != rhs_storage._transactionFee {return false} if _storage._memo != rhs_storage._memo {return false} if _storage._data != rhs_storage._data {return false} + if _storage._maxCustomFees != rhs_storage._maxCustomFees {return false} return true } if !storagesAreEqual {return false} diff --git a/Sources/HieroProtobufs/Generated/services/smart_contract_service.grpc.swift b/Sources/HieroProtobufs/Generated/services/smart_contract_service.grpc.swift index 087ae242..6c852b43 100644 --- a/Sources/HieroProtobufs/Generated/services/smart_contract_service.grpc.swift +++ b/Sources/HieroProtobufs/Generated/services/smart_contract_service.grpc.swift @@ -81,6 +81,11 @@ public protocol Proto_SmartContractServiceClientProtocol: GRPCClient { _ request: Proto_Transaction, callOptions: CallOptions? ) -> UnaryCall + + func lambdaSStore( + _ request: Proto_Transaction, + callOptions: CallOptions? + ) -> UnaryCall } extension Proto_SmartContractServiceClientProtocol { @@ -397,6 +402,25 @@ extension Proto_SmartContractServiceClientProtocol { interceptors: self.interceptors?.makecallEthereumInterceptors() ?? [] ) } + + ///* + /// Update zero or more slots of a lambda. + /// + /// - Parameters: + /// - request: Request to send to lambdaSStore. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func lambdaSStore( + _ request: Proto_Transaction, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Proto_SmartContractServiceClientMetadata.Methods.lambdaSStore.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makelambdaSStoreInterceptors() ?? [] + ) + } } @available(*, deprecated) @@ -525,6 +549,11 @@ public protocol Proto_SmartContractServiceAsyncClientProtocol: GRPCClient { _ request: Proto_Transaction, callOptions: CallOptions? ) -> GRPCAsyncUnaryCall + + func makeLambdaSstoreCall( + _ request: Proto_Transaction, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -680,6 +709,28 @@ extension Proto_SmartContractServiceAsyncClientProtocol { interceptors: self.interceptors?.makecallEthereumInterceptors() ?? [] ) } + + public func makeLambdaSstoreCall( + _ request: Proto_Transaction, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Proto_SmartContractServiceClientMetadata.Methods.lambdaSStore.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makelambdaSStoreInterceptors() ?? [] + ) + } + + public func makeLambdaSStoreCall( + _ request: Proto_Transaction, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeLambdaSstoreCall( + request, + callOptions: callOptions + ) + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -827,6 +878,18 @@ extension Proto_SmartContractServiceAsyncClientProtocol { interceptors: self.interceptors?.makecallEthereumInterceptors() ?? [] ) } + + public func lambdaSStore( + _ request: Proto_Transaction, + callOptions: CallOptions? = nil + ) async throws -> Proto_TransactionResponse { + return try await self.performAsyncUnaryCall( + path: Proto_SmartContractServiceClientMetadata.Methods.lambdaSStore.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makelambdaSStoreInterceptors() ?? [] + ) + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -883,6 +946,9 @@ public protocol Proto_SmartContractServiceClientInterceptorFactoryProtocol: Send /// - Returns: Interceptors to use when invoking 'callEthereum'. func makecallEthereumInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'lambdaSStore'. + func makelambdaSStoreInterceptors() -> [ClientInterceptor] } public enum Proto_SmartContractServiceClientMetadata { @@ -902,6 +968,7 @@ public enum Proto_SmartContractServiceClientMetadata { Proto_SmartContractServiceClientMetadata.Methods.systemDelete, Proto_SmartContractServiceClientMetadata.Methods.systemUndelete, Proto_SmartContractServiceClientMetadata.Methods.callEthereum, + Proto_SmartContractServiceClientMetadata.Methods.lambdaSStore, ] ) @@ -977,6 +1044,12 @@ public enum Proto_SmartContractServiceClientMetadata { path: "/proto.SmartContractService/callEthereum", type: GRPCCallType.unary ) + + public static let lambdaSStore = GRPCMethodDescriptor( + name: "lambdaSStore", + path: "/proto.SmartContractService/lambdaSStore", + type: GRPCCallType.unary + ) } } @@ -1119,6 +1192,10 @@ public protocol Proto_SmartContractServiceProvider: CallHandlerProvider { /// data, but MAY be charged up to 80% of that value if the amount required /// is less than this "floor" amount. func callEthereum(request: Proto_Transaction, context: StatusOnlyCallContext) -> EventLoopFuture + + ///* + /// Update zero or more slots of a lambda. + func lambdaSStore(request: Proto_Transaction, context: StatusOnlyCallContext) -> EventLoopFuture } extension Proto_SmartContractServiceProvider { @@ -1241,6 +1318,15 @@ extension Proto_SmartContractServiceProvider { userFunction: self.callEthereum(request:context:) ) + case "lambdaSStore": + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makelambdaSStoreInterceptors() ?? [], + userFunction: self.lambdaSStore(request:context:) + ) + default: return nil } @@ -1424,6 +1510,13 @@ public protocol Proto_SmartContractServiceAsyncProvider: CallHandlerProvider, Se request: Proto_Transaction, context: GRPCAsyncServerCallContext ) async throws -> Proto_TransactionResponse + + ///* + /// Update zero or more slots of a lambda. + func lambdaSStore( + request: Proto_Transaction, + context: GRPCAsyncServerCallContext + ) async throws -> Proto_TransactionResponse } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -1553,6 +1646,15 @@ extension Proto_SmartContractServiceAsyncProvider { wrapping: { try await self.callEthereum(request: $0, context: $1) } ) + case "lambdaSStore": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makelambdaSStoreInterceptors() ?? [], + wrapping: { try await self.lambdaSStore(request: $0, context: $1) } + ) + default: return nil } @@ -1608,6 +1710,10 @@ public protocol Proto_SmartContractServiceServerInterceptorFactoryProtocol: Send /// - Returns: Interceptors to use when handling 'callEthereum'. /// Defaults to calling `self.makeInterceptors()`. func makecallEthereumInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'lambdaSStore'. + /// Defaults to calling `self.makeInterceptors()`. + func makelambdaSStoreInterceptors() -> [ServerInterceptor] } public enum Proto_SmartContractServiceServerMetadata { @@ -1627,6 +1733,7 @@ public enum Proto_SmartContractServiceServerMetadata { Proto_SmartContractServiceServerMetadata.Methods.systemDelete, Proto_SmartContractServiceServerMetadata.Methods.systemUndelete, Proto_SmartContractServiceServerMetadata.Methods.callEthereum, + Proto_SmartContractServiceServerMetadata.Methods.lambdaSStore, ] ) @@ -1702,5 +1809,11 @@ public enum Proto_SmartContractServiceServerMetadata { path: "/proto.SmartContractService/callEthereum", type: GRPCCallType.unary ) + + public static let lambdaSStore = GRPCMethodDescriptor( + name: "lambdaSStore", + path: "/proto.SmartContractService/lambdaSStore", + type: GRPCCallType.unary + ) } } diff --git a/Sources/HieroProtobufs/Generated/services/transaction.pb.swift b/Sources/HieroProtobufs/Generated/services/transaction.pb.swift index b5843bf9..6b3c446d 100644 --- a/Sources/HieroProtobufs/Generated/services/transaction.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/transaction.pb.swift @@ -965,6 +965,26 @@ public struct Proto_TransactionBody: @unchecked Sendable { set {_uniqueStorage()._data = .atomicBatch(newValue)} } + ///* + /// A transaction body for updating the storage of a EVM lambda hook. + public var lambdaSstore: Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody { + get { + if case .lambdaSstore(let v)? = _storage._data {return v} + return Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody() + } + set {_uniqueStorage()._data = .lambdaSstore(newValue)} + } + + ///* + /// An internal-only transaction body for dispatching a hook CRUD operation. + public var hookDispatch: Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody { + get { + if case .hookDispatch(let v)? = _storage._data {return v} + return Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody() + } + set {_uniqueStorage()._data = .hookDispatch(newValue)} + } + ///* /// A list of maximum custom fees that the users are willing to pay. ///

@@ -1265,6 +1285,12 @@ public struct Proto_TransactionBody: @unchecked Sendable { ///* /// A transaction body for handling a set of transactions atomically. case atomicBatch(Proto_AtomicBatchTransactionBody) + ///* + /// A transaction body for updating the storage of a EVM lambda hook. + case lambdaSstore(Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody) + ///* + /// An internal-only transaction body for dispatching a hook CRUD operation. + case hookDispatch(Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody) } @@ -1425,6 +1451,8 @@ extension Proto_TransactionBody: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 71: .standard(proto: "history_proof_vote"), 72: .standard(proto: "crs_publication"), 74: .standard(proto: "atomic_batch"), + 75: .standard(proto: "lambda_sstore"), + 76: .standard(proto: "hook_dispatch"), 1001: .standard(proto: "max_custom_fees"), ] @@ -2292,6 +2320,32 @@ extension Proto_TransactionBody: SwiftProtobuf.Message, SwiftProtobuf._MessageIm _storage._data = .atomicBatch(v) } }() + case 75: try { + var v: Com_Hedera_Hapi_Node_Hooks_LambdaSStoreTransactionBody? + var hadOneofValue = false + if let current = _storage._data { + hadOneofValue = true + if case .lambdaSstore(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._data = .lambdaSstore(v) + } + }() + case 76: try { + var v: Com_Hedera_Hapi_Node_Hooks_HookDispatchTransactionBody? + var hadOneofValue = false + if let current = _storage._data { + hadOneofValue = true + if case .hookDispatch(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._data = .hookDispatch(v) + } + }() case 1001: try { try decoder.decodeRepeatedMessageField(value: &_storage._maxCustomFees) }() default: break } @@ -2573,9 +2627,21 @@ extension Proto_TransactionBody: SwiftProtobuf.Message, SwiftProtobuf._MessageIm try { if let v = _storage._batchKey { try visitor.visitSingularMessageField(value: v, fieldNumber: 73) } }() - try { if case .atomicBatch(let v)? = _storage._data { + switch _storage._data { + case .atomicBatch?: try { + guard case .atomicBatch(let v)? = _storage._data else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 74) - } }() + }() + case .lambdaSstore?: try { + guard case .lambdaSstore(let v)? = _storage._data else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 75) + }() + case .hookDispatch?: try { + guard case .hookDispatch(let v)? = _storage._data else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 76) + }() + default: break + } if !_storage._maxCustomFees.isEmpty { try visitor.visitRepeatedMessageField(value: _storage._maxCustomFees, fieldNumber: 1001) } diff --git a/Sources/HieroProtobufs/Generated/services/transaction_contents.pb.swift b/Sources/HieroProtobufs/Generated/services/transaction_contents.pb.swift index 6150e82b..d21d7fe6 100644 --- a/Sources/HieroProtobufs/Generated/services/transaction_contents.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/transaction_contents.pb.swift @@ -67,6 +67,15 @@ public struct Proto_SignedTransaction: @unchecked Sendable { /// Clears the value of `sigMap`. Subsequent reads from it will return its default value. public mutating func clearSigMap() {self._sigMap = nil} + ///* + /// If false then the hash of this transaction is the SHA-384 hash of the + /// serialization of this SignedTransaction message as it arrived on the wire. + ///

+ /// If true then the hash of this transaction is the SHA-384 hash of the + /// ascending field order serialization of the Transaction whose `bodyBytes` + /// and sigMap fields are deserialized from the contents of this message. + public var useSerializedTxMessageHashAlgorithm: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -83,6 +92,7 @@ extension Proto_SignedTransaction: SwiftProtobuf.Message, SwiftProtobuf._Message public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "bodyBytes"), 2: .same(proto: "sigMap"), + 3: .standard(proto: "use_serialized_tx_message_hash_algorithm"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -93,6 +103,7 @@ extension Proto_SignedTransaction: SwiftProtobuf.Message, SwiftProtobuf._Message switch fieldNumber { case 1: try { try decoder.decodeSingularBytesField(value: &self.bodyBytes) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._sigMap) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.useSerializedTxMessageHashAlgorithm) }() default: break } } @@ -109,12 +120,16 @@ extension Proto_SignedTransaction: SwiftProtobuf.Message, SwiftProtobuf._Message try { if let v = self._sigMap { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + if self.useSerializedTxMessageHashAlgorithm != false { + try visitor.visitSingularBoolField(value: self.useSerializedTxMessageHashAlgorithm, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Proto_SignedTransaction, rhs: Proto_SignedTransaction) -> Bool { if lhs.bodyBytes != rhs.bodyBytes {return false} if lhs._sigMap != rhs._sigMap {return false} + if lhs.useSerializedTxMessageHashAlgorithm != rhs.useSerializedTxMessageHashAlgorithm {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/HieroProtobufs/Protos/services/basic_types.proto b/Sources/HieroProtobufs/Protos/services/basic_types.proto index df1ea135..a41d53ab 100644 --- a/Sources/HieroProtobufs/Protos/services/basic_types.proto +++ b/Sources/HieroProtobufs/Protos/services/basic_types.proto @@ -435,6 +435,83 @@ message TransactionID { int32 nonce = 4; } +/** + * Once a hook is created, its full id. + *

+ * A composite of its creating entity's id and an arbitrary 64-bit hook id + * (which need not be sequential). + */ +message HookId { + /** + * The hook's creating entity id. + */ + HookEntityId entity_id = 1; + + /** + * An arbitrary 64-bit identifier. + */ + int64 hook_id = 2; +} + +/** + * The id of an entity using a hook. + */ +message HookEntityId { + oneof entity_id { + /** + * An account using a hook. + */ + AccountID account_id = 1; + } +} + +/** + * Specifies a call to a hook from within a transaction. + *

+ * Often the hook's entity is implied by the nature of the call site. For example, when using an account allowance hook + * inside a crypto transfer, the hook's entity is necessarily the account whose authorization is required. + *

+ * For future extension points where the hook owner is not forced by the context, we include the option to fully + * specify the hook id for the call. + */ +message HookCall { + oneof id { + /** + * The full id of the hook to call, when the owning entity is not forced by the call site. + */ + HookId full_hook_id = 1; + /** + * The numeric id of the hook to call, when the owning entity is forced by the call site. + */ + int64 hook_id = 2; + } + + /** + * Specifies details of the call. + */ + oneof call_spec { + /** + * Specification of how to call an EVM hook. + */ + EvmHookCall evm_hook_call = 3; + } +} + +/** + * Specifies details of a call to an EVM hook. + */ +message EvmHookCall { + /** + * Call data to pass to the hook via the IHieroHook.HookContext#data field. + */ + bytes data = 1; + + /** + * The gas limit to use. + */ + uint64 gas_limit = 2; +} + /** * An account, and the amount that it sends or receives during a token transfer. * @@ -467,6 +544,28 @@ message AccountAmount { * The default value SHALL be false (unset). */ bool is_approval = 3; + + /** + * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` on scoped + * account; the hook's invoked methods must not revert and must return + * true for the containing CryptoTransfer to succeed. + *

+ * Cannot be set if `is_approval` is true. + */ + oneof hook_call { + /** + * A single call made before attempting the CryptoTransfer, to a + * method with logical signature allow(HookContext, ProposedTransfers) + */ + HookCall pre_tx_allowance_hook = 4; + /** + * Two calls, the first call before attempting the CryptoTransfer, to a + * method with logical signature allowPre(HookContext, ProposedTransfers); + * and the second call after attempting the CryptoTransfer, to a method + * with logical signature allowPost(HookContext, ProposedTransfers). + */ + HookCall pre_post_tx_allowance_hook = 5; + } } /** @@ -525,6 +624,51 @@ message NftTransfer { * The default value SHALL be false (unset). */ bool is_approval = 4; + + /** + * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + * senderAccountID that must succeed for the transaction to occur. + *

+ * Cannot be set if `is_approval` is true. + */ + oneof sender_allowance_hook_call { + /** + * A single call made before attempting the CryptoTransfer, to a + * method with logical signature allow(HookContext, ProposedTransfers) + */ + HookCall pre_tx_sender_allowance_hook = 5; + /** + * Two calls, the first call before attempting the CryptoTransfer, to a + * method with logical signature allowPre(HookContext, ProposedTransfers); + * and the second call after attempting the CryptoTransfer, to a method + * with logical signature allowPost(HookContext, ProposedTransfers). + */ + HookCall pre_post_tx_sender_allowance_hook = 6; + } + + /** + * If set, a call to a hook of type `ACCOUNT_ALLOWANCE_HOOK` installed on + * receiverAccountID that must succeed for the transaction to occur. + *

+ * May be set even if `is_approval` is true. In this case, the approval applies + * to the sender authorization, and the hook applies to the receiver authorization + * (if needed, e.g. because of a fallback royalty fee or receiver signature + * requirement). + */ + oneof receiver_allowance_hook_call { + /** + * A single call made before attempting the CryptoTransfer, to a + * method with logical signature allow(HookContext, ProposedTransfers) + */ + HookCall pre_tx_receiver_allowance_hook = 7; + /** + * Two calls, the first call before attempting the CryptoTransfer, to a + * method with logical signature allowPre(HookContext, ProposedTransfers); + * and the second call after attempting the CryptoTransfer, to a method + * with logical signature allowPost(HookContext, ProposedTransfers). + */ + HookCall pre_post_tx_receiver_allowance_hook = 8; + } } /** @@ -1689,6 +1833,16 @@ enum HederaFunctionality { * Submit a batch of transactions to run atomically */ AtomicBatch = 108; + + /** + * Update one or more storage slots in an lambda EVM hook. + */ + LambdaSStore = 109; + + /** + * (Internal-only) Dispatch a hook action. + */ + HookDispatch = 110; } /** diff --git a/Sources/HieroProtobufs/Protos/services/contract_create.proto b/Sources/HieroProtobufs/Protos/services/contract_create.proto index 14f05d27..abb03f93 100644 --- a/Sources/HieroProtobufs/Protos/services/contract_create.proto +++ b/Sources/HieroProtobufs/Protos/services/contract_create.proto @@ -45,6 +45,7 @@ option java_multiple_files = true; import "services/basic_types.proto"; import "services/duration.proto"; +import "services/hook_types.proto"; /** * Create a new smart contract. @@ -272,4 +273,9 @@ message ContractCreateTransactionBody { * without reward. */ bool decline_reward = 19; + + /** + * Details of hooks to add immediately after creating this contract. + */ + repeated com.hedera.hapi.node.hooks.HookCreationDetails hook_creation_details = 20; } diff --git a/Sources/HieroProtobufs/Protos/services/contract_types.proto b/Sources/HieroProtobufs/Protos/services/contract_types.proto index 7202ea7b..fcc48211 100644 --- a/Sources/HieroProtobufs/Protos/services/contract_types.proto +++ b/Sources/HieroProtobufs/Protos/services/contract_types.proto @@ -23,6 +23,9 @@ import "google/protobuf/wrappers.proto"; /** * Context of an internal call in an EVM transaction that is not otherwise externalized.
+ * This message does not say anything about whether an EVM transaction is itself a logical + * transaction in a Hiero transactional unit. It simply provides context on an internal + * message call within an EVM transaction. */ message InternalCallContext { /** @@ -42,7 +45,7 @@ message InternalCallContext { } /** - * Results of executing EVM transaction.
+ * Results of executing a EVM transaction.
*/ message EvmTransactionResult { /** @@ -73,7 +76,8 @@ message EvmTransactionResult { uint64 gas_used = 5; /** - * If not already externalized, the context of the internal call producing this result. + * If not already externalized in a transaction body, the context of the + * internal call producing this result. */ InternalCallContext internal_call_context = 6; } diff --git a/Sources/HieroProtobufs/Protos/services/contract_update.proto b/Sources/HieroProtobufs/Protos/services/contract_update.proto index deb4cd1e..3d699023 100644 --- a/Sources/HieroProtobufs/Protos/services/contract_update.proto +++ b/Sources/HieroProtobufs/Protos/services/contract_update.proto @@ -22,6 +22,7 @@ option java_multiple_files = true; import "services/basic_types.proto"; import "services/duration.proto"; +import "services/hook_types.proto"; import "services/timestamp.proto"; import "google/protobuf/wrappers.proto"; @@ -178,4 +179,14 @@ message ContractUpdateTransactionBody { * without reward. */ google.protobuf.BoolValue decline_reward = 15; + + /** + * The ids the hooks to delete from the contract. + */ + repeated int64 hook_ids_to_delete = 16; + + /** + * The hooks to create for the contract. + */ + repeated com.hedera.hapi.node.hooks.HookCreationDetails hook_creation_details = 17; } diff --git a/Sources/HieroProtobufs/Protos/services/crypto_create.proto b/Sources/HieroProtobufs/Protos/services/crypto_create.proto index af46c47b..108b8b0d 100644 --- a/Sources/HieroProtobufs/Protos/services/crypto_create.proto +++ b/Sources/HieroProtobufs/Protos/services/crypto_create.proto @@ -20,6 +20,7 @@ option java_multiple_files = true; import "services/basic_types.proto"; import "services/duration.proto"; +import "services/hook_types.proto"; /* * Create a new account. @@ -198,4 +199,9 @@ message CryptoCreateTransactionBody { * Once set, an account alias is immutable and MUST NOT be changed. */ bytes alias = 18; + + /** + * Details of hooks to add immediately after creating this account. + */ + repeated com.hedera.hapi.node.hooks.HookCreationDetails hook_creation_details = 19; } diff --git a/Sources/HieroProtobufs/Protos/services/crypto_update.proto b/Sources/HieroProtobufs/Protos/services/crypto_update.proto index f551a42d..539b7126 100644 --- a/Sources/HieroProtobufs/Protos/services/crypto_update.proto +++ b/Sources/HieroProtobufs/Protos/services/crypto_update.proto @@ -20,6 +20,7 @@ option java_multiple_files = true; import "services/basic_types.proto"; import "services/duration.proto"; +import "services/hook_types.proto"; import "services/timestamp.proto"; import "google/protobuf/wrappers.proto"; @@ -216,4 +217,13 @@ message CryptoUpdateTransactionBody { */ google.protobuf.BoolValue decline_reward = 18; + /** + * The ids the hooks to delete from the account. + */ + repeated int64 hook_ids_to_delete = 19; + + /** + * The hooks to create for the account. + */ + repeated com.hedera.hapi.node.hooks.HookCreationDetails hook_creation_details = 20; } diff --git a/Sources/HieroProtobufs/Protos/services/hook_dispatch.proto b/Sources/HieroProtobufs/Protos/services/hook_dispatch.proto new file mode 100644 index 00000000..bfe8cc2f --- /dev/null +++ b/Sources/HieroProtobufs/Protos/services/hook_dispatch.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +package com.hedera.hapi.node.hooks; + +// SPDX-License-Identifier: Apache-2.0 +option java_package = "com.hedera.hapi.node.hooks.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +import "services/basic_types.proto"; +import "services/hook_types.proto"; + +/** + * Dispatches a hook action to an appropriate service. + */ +message HookDispatchTransactionBody { + oneof action { + /** + * The id of the hook to delete. + */ + proto.HookId hook_id_to_delete = 1; + + /** + * Creation details for a new hook. + */ + HookCreation creation = 2; + + /** + * Execution details of an existing hook. + */ + HookExecution execution = 3; + } +} + +/** + * Details the execution of a hook. + */ +message HookExecution { + /** + * The id of the hook's entity. + */ + proto.HookEntityId hook_entity_id = 1; + + /** + * The details of the call, including which hook id to call. + */ + proto.HookCall call = 2; +} + diff --git a/Sources/HieroProtobufs/Protos/services/hook_types.proto b/Sources/HieroProtobufs/Protos/services/hook_types.proto new file mode 100644 index 00000000..3b52d52b --- /dev/null +++ b/Sources/HieroProtobufs/Protos/services/hook_types.proto @@ -0,0 +1,215 @@ +syntax = "proto3"; + +package com.hedera.hapi.node.hooks; + +import "services/basic_types.proto"; +import "google/protobuf/wrappers.proto"; + +// SPDX-License-Identifier: Apache-2.0 +option java_package = "com.hedera.hapi.node.hooks.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +/*** + * The Hiero extension points that accept a hook. + */ +enum HookExtensionPoint { + /** + * Used to customize an account's allowances during a CryptoTransfer transaction. + */ + ACCOUNT_ALLOWANCE_HOOK = 0; +} + +/** + * Specifies the creation of a new hook at the given id for the given entity. + */ +message HookCreation { + /** + * The id of the hook's entity. + */ + proto.HookEntityId entity_id = 1; + + /** + * The creation details. + */ + HookCreationDetails details = 2; + + /** + * If set, the id of the hook following this one in the owner's + * doubly-linked list of hooks. + */ + google.protobuf.Int64Value next_hook_id = 9; +} + +/*** + * The details of a hook's creation. + */ +message HookCreationDetails { + /** + * The extension point for the hook. + */ + HookExtensionPoint extension_point = 1; + + /** + * The id to create the hook at. + */ + int64 hook_id = 2; + + /** + * The hook implementation. + */ + oneof hook { + /** + * A hook programmed in EVM bytecode that does not require access to state + * or interactions with external contracts. + */ + PureEvmHook pure_evm_hook = 3; + /** + * A hook programmed in EVM bytecode that may access state or interact with + * external contracts. + */ + LambdaEvmHook lambda_evm_hook = 4; + } + + /** + * If set, a key that that can be used to remove or replace the hook; or (if + * applicable, as with a lambda EVM hook) perform transactions that customize + * the hook. + */ + proto.Key admin_key = 5; +} + +/** + * Definition of a lambda EVM hook. + */ +message PureEvmHook { + /** + * The specification for the hook. + */ + EvmHookSpec spec = 1; +} + +/** + * Definition of a lambda EVM hook. + */ +message LambdaEvmHook { + /** + * The specification for the hook. + */ + EvmHookSpec spec = 1; + + /** + * Initial storage updates for the lambda, if any. + */ + repeated LambdaStorageUpdate storage_updates = 2; +} + +/** + * Shared specifications for an EVM hook. May be used for any extension point. + */ +message EvmHookSpec { + /** + * The source of the EVM bytecode for the hook. + */ + oneof bytecode_source { + /** + * The id of a contract that implements the extension point API with EVM bytecode. + */ + proto.ContractID contract_id = 1; + } +} + +/** + * Specifies a key/value pair in the storage of a lambda, either by the explicit storage + * slot contents; or by a combination of a Solidity mapping's slot key and the key into + * that mapping. + */ +message LambdaStorageUpdate { + oneof update { + /** + * An explicit storage slot update. + */ + LambdaStorageSlot storage_slot = 1; + /** + * Implicit storage slot updates specified as Solidity mapping entries. + */ + LambdaMappingEntries mapping_entries = 2; + } +} + +/** + * Specifies storage slot updates via indirection into a Solidity mapping. + *

+ * Concretely, if the Solidity mapping is itself at slot `mapping_slot`, then + * the * storage slot for key `key` in the mapping is defined by the relationship + * `key_storage_slot = keccak256(abi.encodePacked(mapping_slot, key))`. + *

+ * This message lets a metaprotocol be specified in terms of changes to a + * Solidity mapping's entries. If only raw slots could be updated, then a block + * stream consumer following the metaprotocol would have to invert the Keccak256 + * hash to determine which mapping entry was being updated, which is not possible. + */ +message LambdaMappingEntries { + /** + * The slot that corresponds to the Solidity mapping itself. Must use a + * minimal byte representation (no leading zeros). + */ + bytes mapping_slot = 1; + + /** + * The entries in the mapping at the given slot. + */ + repeated LambdaMappingEntry entries = 2; +} + +/** + * An entry in a Solidity mapping. Very helpful for protocols that apply + * `LambdaSStore` to manage the entries of a hook contract's mapping instead + * its raw storage slots. + *

+ * This is especially attractive when the mapping value itself fits in a single + * word; for more complicated value storage layouts it becomes necessary to + * combine the mapping update with additional `LambdaStorageSlot` updates that + * specify the complete storage slots of the value type. + */ +message LambdaMappingEntry { + oneof entry_key { + /** + * The explicit bytes of the mapping entry. Must use a minimal byte representation; + * may not exceed 32 bytes in length. + */ + bytes key = 1; + /** + * The bytes that are the preimage of the Keccak256 hash that forms the mapping key. + * May be longer or shorter than 32 bytes and may have leading zeros, since Solidity + * supports variable-length keys in mappings. + */ + bytes preimage = 2; + } + + /** + * If the mapping entry is present and non-zero, its value. May not be longer than + * 32 bytes in length; must use a minimal byte representation (no leading zeros). + * Leaving this field empty in an update removes the entry from the mapping. + */ + bytes value = 3; +} + +/** + * A slot in the storage of a lambda EVM hook. + */ +message LambdaStorageSlot { + /** + * The key of the slot. Must use a minimal byte representation (no + * leading zeros); may not exceed 32 bytes in length. + */ + bytes key = 1; + + /** + * If the slot is present and non-zero, its value. Must use a minimal + * byte representation (no leading zeros); may not exceed 32 bytes in + * length Leaving this field empty in an update removes the slot from + * storage. + */ + bytes value = 2; +} diff --git a/Sources/HieroProtobufs/Protos/services/lambda_sstore.proto b/Sources/HieroProtobufs/Protos/services/lambda_sstore.proto new file mode 100644 index 00000000..f67676cb --- /dev/null +++ b/Sources/HieroProtobufs/Protos/services/lambda_sstore.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package com.hedera.hapi.node.hooks; + +// SPDX-License-Identifier: Apache-2.0 +option java_package = "com.hedera.hapi.node.hooks.legacy"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +import "services/basic_types.proto"; +import "services/hook_types.proto"; + +/** + * Adds or removes key/value pairs in the storage of a lambda. The lambda's owning key must sign the transaction. + */ +message LambdaSStoreTransactionBody { + /** + * The id of the lambda EVM hook whose storage is being updated. + */ + proto.HookId hook_id = 1; + + /** + * The updates to the storage of the lambda. + */ + repeated LambdaStorageUpdate storage_updates = 2; +} diff --git a/Sources/HieroProtobufs/Protos/services/node_update.proto b/Sources/HieroProtobufs/Protos/services/node_update.proto index d2a80350..7a0ceb28 100644 --- a/Sources/HieroProtobufs/Protos/services/node_update.proto +++ b/Sources/HieroProtobufs/Protos/services/node_update.proto @@ -162,7 +162,9 @@ message NodeUpdateTransactionBody { * This endpoint MUST use a valid port and SHALL be reachable over TLS.
* This field MAY be omitted if the node does not support gRPC-Web access.
* This field MUST be updated if the gRPC-Web endpoint changes.
- * This field SHALL enable frontend clients to avoid hard-coded proxy endpoints. + * This field SHALL enable frontend clients to avoid hard-coded proxy endpoints.
+ * This field MAY be set to `ServiceEndpoint.DEFAULT` to remove a previously-valid + * web proxy. */ proto.ServiceEndpoint grpc_proxy_endpoint = 10; } diff --git a/Sources/HieroProtobufs/Protos/services/response_code.proto b/Sources/HieroProtobufs/Protos/services/response_code.proto index 6607588d..9daa7cdf 100644 --- a/Sources/HieroProtobufs/Protos/services/response_code.proto +++ b/Sources/HieroProtobufs/Protos/services/response_code.proto @@ -1760,4 +1760,86 @@ enum ResponseCodeEnum { * which the network does not support. */ GRPC_WEB_PROXY_NOT_SUPPORTED = 399; + + /** + * An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. + */ + NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE = 400; + + /** + * A HAPI client cannot set the SignedTransaction#use_serialized_tx_message_hash_algorithm field. + */ + INVALID_SERIALIZED_TX_MESSAGE_HASH_ALGORITHM = 401; + + /** + * An EVM hook execution was throttled due to high network gas utilization. + */ + EVM_HOOK_GAS_THROTTLED = 500; + + /** + * A user tried to create a hook with an id already in use. + */ + HOOK_ID_IN_USE = 501; + + /** + * A transaction tried to execute a hook that did not match the specified + * type or was malformed in some other way. + */ + BAD_HOOK_REQUEST = 502; + + /** + * A CryptoTransfer relying on a ACCOUNT_ALLOWANCE hook was rejected. + */ + REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK = 503; + + /** + * A hook id was not found. + */ + HOOK_NOT_FOUND = 504; + + /** + * A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + */ + LAMBDA_STORAGE_UPDATE_BYTES_TOO_LONG = 505; + + /** + * A lambda mapping slot, storage key, or storage value failed to use the + * minimal representation (i.e., no leading zeros). + */ + LAMBDA_STORAGE_UPDATE_BYTES_MUST_USE_MINIMAL_REPRESENTATION = 506; + + /** + * A hook id was invalid. + */ + INVALID_HOOK_ID = 507; + + /** + * A lambda storage update had no contents. + */ + EMPTY_LAMBDA_STORAGE_UPDATE = 508; + + /** + * A user repeated the same hook id in a creation details list. + */ + HOOK_ID_REPEATED_IN_CREATION_DETAILS = 509; + + /** + * Hooks are not not enabled on the target Hiero network. + */ + HOOKS_NOT_ENABLED = 510; + + /** + * The target hook is not a lambda. + */ + HOOK_IS_NOT_A_LAMBDA = 511; + + /** + * A hook was deleted. + */ + HOOK_DELETED = 512; + + /** + * The LambdaSStore tried to update too many storage slots in a single transaction. + */ + TOO_MANY_LAMBDA_STORAGE_UPDATES = 513; } diff --git a/Sources/HieroProtobufs/Protos/services/schedulable_transaction_body.proto b/Sources/HieroProtobufs/Protos/services/schedulable_transaction_body.proto index 7928e9a4..e9f6851e 100644 --- a/Sources/HieroProtobufs/Protos/services/schedulable_transaction_body.proto +++ b/Sources/HieroProtobufs/Protos/services/schedulable_transaction_body.proto @@ -70,6 +70,7 @@ import "services/token_airdrop.proto"; import "services/schedule_delete.proto"; import "services/util_prng.proto"; +import "services/custom_fees.proto"; import "services/node_create.proto"; import "services/node_update.proto"; @@ -403,4 +404,13 @@ message SchedulableTransactionBody { */ TokenAirdropTransactionBody tokenAirdrop = 48; } + + /** + * A list of maximum custom fees that the users are willing to pay. + *

+ * This field is OPTIONAL.
+ * If left empty, the users are accepting to pay any custom fee.
+ * If used with a transaction type that does not support custom fee limits, the transaction will fail. + */ + repeated CustomFeeLimit max_custom_fees = 1001; } diff --git a/Sources/HieroProtobufs/Protos/services/smart_contract_service.proto b/Sources/HieroProtobufs/Protos/services/smart_contract_service.proto index cf64ced4..208034ac 100644 --- a/Sources/HieroProtobufs/Protos/services/smart_contract_service.proto +++ b/Sources/HieroProtobufs/Protos/services/smart_contract_service.proto @@ -171,4 +171,9 @@ service SmartContractService { * is less than this "floor" amount. */ rpc callEthereum (Transaction) returns (TransactionResponse); + + /** + * Update zero or more slots of a lambda. + */ + rpc lambdaSStore (Transaction) returns (TransactionResponse); } diff --git a/Sources/HieroProtobufs/Protos/services/transaction.proto b/Sources/HieroProtobufs/Protos/services/transaction.proto index 42e994d2..fe031736 100644 --- a/Sources/HieroProtobufs/Protos/services/transaction.proto +++ b/Sources/HieroProtobufs/Protos/services/transaction.proto @@ -98,6 +98,9 @@ import "services/auxiliary/history/history_proof_signature.proto"; import "services/auxiliary/history/history_proof_key_publication.proto"; import "services/auxiliary/history/history_proof_vote.proto"; +import "services/lambda_sstore.proto"; +import "services/hook_dispatch.proto"; + /** * A wrapper around signed transaction bytes.
* This was originally a transaction with body, signatures, and/or bytes, @@ -648,6 +651,16 @@ message TransactionBody { * A transaction body for handling a set of transactions atomically. */ AtomicBatchTransactionBody atomic_batch = 74; + + /** + * A transaction body for updating the storage of a EVM lambda hook. + */ + com.hedera.hapi.node.hooks.LambdaSStoreTransactionBody lambda_sstore = 75; + + /** + * An internal-only transaction body for dispatching a hook CRUD operation. + */ + com.hedera.hapi.node.hooks.HookDispatchTransactionBody hook_dispatch = 76; } /** diff --git a/Sources/HieroProtobufs/Protos/services/transaction_contents.proto b/Sources/HieroProtobufs/Protos/services/transaction_contents.proto index 1d9198ea..52f7e88a 100644 --- a/Sources/HieroProtobufs/Protos/services/transaction_contents.proto +++ b/Sources/HieroProtobufs/Protos/services/transaction_contents.proto @@ -47,4 +47,14 @@ message SignedTransaction { * This set MAY contain additional signatures. */ SignatureMap sigMap = 2; + + /** + * If false then the hash of this transaction is the SHA-384 hash of the + * serialization of this SignedTransaction message as it arrived on the wire. + *

+ * If true then the hash of this transaction is the SHA-384 hash of the + * ascending field order serialization of the Transaction whose `bodyBytes` + * and sigMap fields are deserialized from the contents of this message. + */ + bool use_serialized_tx_message_hash_algorithm = 3; } diff --git a/Sources/HieroProtobufs/update_protos.py b/Sources/HieroProtobufs/update_protos.py index f2802798..50da30ed 100644 --- a/Sources/HieroProtobufs/update_protos.py +++ b/Sources/HieroProtobufs/update_protos.py @@ -177,6 +177,9 @@ def run_protoc_grpc(proto_files: List[str]): "services/get_account_details.proto", "services/get_by_key.proto", "services/get_by_solidity_id.proto", + "services/hook_dispatch.proto", + "services/hook_types.proto", + "services/lambda_sstore.proto", "services/network_get_execution_time.proto", "services/network_get_version_info.proto", "services/network_service.proto", diff --git a/protobufs b/protobufs index c4f62d04..6cb044ab 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c4f62d047ec898519710feab6882beebe70a124d +Subproject commit 6cb044abed16f1c7859ae45e0cbdc567ce2c9a74 From 4040d2bfc02abc0d409f20cb1b1754edd1e13afe Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 14 Oct 2025 09:38:51 -0400 Subject: [PATCH 2/7] add unit tests Signed-off-by: Rob Walworth --- Sources/Hiero/Hooks/HookCall.swift | 2 + Sources/Hiero/Hooks/HookCreationDetails.swift | 26 ++- Sources/Hiero/Hooks/HookEntityId.swift | 4 +- Sources/Hiero/Hooks/HookId.swift | 2 +- Sources/Hiero/Hooks/LambdaMappingEntry.swift | 39 +++-- Sources/Hiero/Hooks/LambdaStorageSlot.swift | 4 +- .../Transaction+FromProtobuf.swift | 25 +-- Tests/HieroTests/EvmHookCallTests.swift | 64 +++++++ Tests/HieroTests/EvmHookSpecTests.swift | 48 ++++++ Tests/HieroTests/HookCallTests.swift | 146 ++++++++++++++++ .../HieroTests/HookCreationDetailsTests.swift | 156 ++++++++++++++++++ Tests/HieroTests/HookEntityIdTests.swift | 46 ++++++ Tests/HieroTests/HookIdTests.swift | 66 ++++++++ Tests/HieroTests/LambdaEvmHookTests.swift | 135 +++++++++++++++ .../LambdaMappingEntriesTests.swift | 122 ++++++++++++++ .../HieroTests/LambdaMappingEntryTests.swift | 119 +++++++++++++ Tests/HieroTests/LambdaStorageSlotTests.swift | 63 +++++++ .../HieroTests/LambdaStorageUpdateTests.swift | 149 +++++++++++++++++ 18 files changed, 1166 insertions(+), 50 deletions(-) create mode 100644 Tests/HieroTests/EvmHookCallTests.swift create mode 100644 Tests/HieroTests/EvmHookSpecTests.swift create mode 100644 Tests/HieroTests/HookCallTests.swift create mode 100644 Tests/HieroTests/HookCreationDetailsTests.swift create mode 100644 Tests/HieroTests/HookEntityIdTests.swift create mode 100644 Tests/HieroTests/HookIdTests.swift create mode 100644 Tests/HieroTests/LambdaEvmHookTests.swift create mode 100644 Tests/HieroTests/LambdaMappingEntriesTests.swift create mode 100644 Tests/HieroTests/LambdaMappingEntryTests.swift create mode 100644 Tests/HieroTests/LambdaStorageSlotTests.swift create mode 100644 Tests/HieroTests/LambdaStorageUpdateTests.swift diff --git a/Sources/Hiero/Hooks/HookCall.swift b/Sources/Hiero/Hooks/HookCall.swift index d9d9ade8..474dfcc1 100644 --- a/Sources/Hiero/Hooks/HookCall.swift +++ b/Sources/Hiero/Hooks/HookCall.swift @@ -8,6 +8,8 @@ public struct HookCall { public var hookId: Int64? public var evmHookCall: EvmHookCall? + public init() {} + public init(fullHookId: HookId? = nil, evmHookCall: EvmHookCall? = nil) { self.fullHookId = fullHookId self.hookId = nil diff --git a/Sources/Hiero/Hooks/HookCreationDetails.swift b/Sources/Hiero/Hooks/HookCreationDetails.swift index db3742e6..25a2c1b3 100644 --- a/Sources/Hiero/Hooks/HookCreationDetails.swift +++ b/Sources/Hiero/Hooks/HookCreationDetails.swift @@ -4,31 +4,43 @@ import Foundation import HieroProtobufs public struct HookCreationDetails { - public var extensionPoint: HookExtensionPoint + public var hookExtensionPoint: HookExtensionPoint public var hookId: Int64 public var lambdaEvmHook: LambdaEvmHook? public var adminKey: Key? public init( - extensionPoint: HookExtensionPoint, + hookExtensionPoint: HookExtensionPoint, hookId: Int64 = 0, lambdaEvmHook: LambdaEvmHook? = nil, adminKey: Key? = nil ) { - self.extensionPoint = extensionPoint + self.hookExtensionPoint = hookExtensionPoint self.hookId = hookId self.lambdaEvmHook = lambdaEvmHook self.adminKey = adminKey } @discardableResult - public mutating func setLambdaEvmHook(_ hook: LambdaEvmHook) -> Self { + public mutating func hookExtensionPoint(_ hookExtensionPoint: HookExtensionPoint) -> Self { + self.hookExtensionPoint = hookExtensionPoint + return self + } + + @discardableResult + public mutating func hookId(_ hookId: Int64) -> Self { + self.hookId = hookId + return self + } + + @discardableResult + public mutating func lambdaEvmHook(_ hook: LambdaEvmHook) -> Self { self.lambdaEvmHook = hook return self } @discardableResult - public mutating func setAdminKey(_ key: Key?) -> Self { + public mutating func adminKey(_ key: Key?) -> Self { self.adminKey = key return self } @@ -38,7 +50,7 @@ extension HookCreationDetails: TryProtobufCodable { internal typealias Protobuf = Com_Hedera_Hapi_Node_Hooks_HookCreationDetails internal init(protobuf proto: Protobuf) throws { - self.extensionPoint = try HookExtensionPoint(protobuf: proto.extensionPoint) + self.hookExtensionPoint = try HookExtensionPoint(protobuf: proto.extensionPoint) self.hookId = proto.hookID // Only accept lambda; anything else => nil @@ -54,7 +66,7 @@ extension HookCreationDetails: TryProtobufCodable { internal func toProtobuf() -> Protobuf { var proto = Protobuf() - proto.extensionPoint = extensionPoint.toProtobuf() + proto.extensionPoint = hookExtensionPoint.toProtobuf() proto.hookID = hookId // Only encode lambda; otherwise leave the oneof unset (nil) diff --git a/Sources/Hiero/Hooks/HookEntityId.swift b/Sources/Hiero/Hooks/HookEntityId.swift index 7faec7ef..a3b945c2 100644 --- a/Sources/Hiero/Hooks/HookEntityId.swift +++ b/Sources/Hiero/Hooks/HookEntityId.swift @@ -4,7 +4,9 @@ import HieroProtobufs public final class HookEntityId { /// ID of the account that owns a hook. - public var accountId: AccountId? + public var accountId: AccountId? = nil + + public init() {} public init(_ accountId: AccountId) { self.accountId = accountId diff --git a/Sources/Hiero/Hooks/HookId.swift b/Sources/Hiero/Hooks/HookId.swift index 1098a1f7..6b2f0245 100644 --- a/Sources/Hiero/Hooks/HookId.swift +++ b/Sources/Hiero/Hooks/HookId.swift @@ -11,7 +11,7 @@ public struct HookId { /// The ID for the hook. public var hookId: Int64 - public init(entityId: HookEntityId, hookId: Int64 = 0) { + public init(entityId: HookEntityId = HookEntityId(), hookId: Int64 = 0) { self.entityId = entityId self.hookId = hookId } diff --git a/Sources/Hiero/Hooks/LambdaMappingEntry.swift b/Sources/Hiero/Hooks/LambdaMappingEntry.swift index 6b9e5fc1..790be020 100644 --- a/Sources/Hiero/Hooks/LambdaMappingEntry.swift +++ b/Sources/Hiero/Hooks/LambdaMappingEntry.swift @@ -5,38 +5,38 @@ import HieroProtobufs /// An implicit storage slot specified as a Solidity mapping entry. public struct LambdaMappingEntry { - /// The slot corresponding to the Solidity mapping. - public var mappingSlot: Data - /// The 32-byte key of the mapping entry. - public var key: Data + public var key: Data? + + /// The slot corresponding to the Solidity mapping. + public var preimage: Data? /// The 32-byte value of the mapping entry (leave empty to delete). public var value: Data - public init(mappingSlot: Data = Data(), key: Data = Data(), value: Data = Data()) { - self.mappingSlot = mappingSlot + public init(key: Data = Data(), preimage: Data = Data(), value: Data = Data()) { self.key = key + self.preimage = preimage self.value = value } - /// Set the Solidity mapping slot. + /// Set the key for the mapping entry. @discardableResult - public mutating func setMappingSlot(_ mappingSlot: Data) -> Self { - self.mappingSlot = mappingSlot + public mutating func key(_ key: Data) -> Self { + self.key = key return self } - /// Set the key for the mapping entry. + /// Set the Solidity preimage. @discardableResult - public mutating func setKey(_ key: Data) -> Self { - self.key = key + public mutating func preimage(_ preimage: Data) -> Self { + self.preimage = preimage return self } /// Set the value for the mapping entry. @discardableResult - public mutating func setValue(_ value: Data) -> Self { + public mutating func value(_ value: Data) -> Self { self.value = value return self } @@ -47,16 +47,23 @@ extension LambdaMappingEntry: TryProtobufCodable { /// Construct from protobuf. internal init(protobuf proto: Protobuf) throws { - self.mappingSlot = proto.mappingSlot self.key = proto.key + self.preimage = proto.preimage self.value = proto.value } /// Convert to protobuf. internal func toProtobuf() -> Protobuf { var proto = Protobuf() - proto.mappingSlot = mappingSlot - proto.key = key + + if let key = key { + proto.key = key + } + + if let preimage = preimage { + proto.preimage = preimage + } + proto.value = value return proto } diff --git a/Sources/Hiero/Hooks/LambdaStorageSlot.swift b/Sources/Hiero/Hooks/LambdaStorageSlot.swift index 6cfcdca6..3f4a995f 100644 --- a/Sources/Hiero/Hooks/LambdaStorageSlot.swift +++ b/Sources/Hiero/Hooks/LambdaStorageSlot.swift @@ -18,14 +18,14 @@ public struct LambdaStorageSlot { /// Set the storage slot key. @discardableResult - public mutating func setKey(_ key: Data) -> Self { + public mutating func key(_ key: Data) -> Self { self.key = key return self } /// Set the storage slot value. @discardableResult - public mutating func setValue(_ value: Data) -> Self { + public mutating func value(_ value: Data) -> Self { self.value = value return self } diff --git a/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift b/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift index c4938db9..b75d97f4 100644 --- a/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift +++ b/Sources/Hiero/Transaction/Transaction+FromProtobuf.swift @@ -220,29 +220,8 @@ extension Transaction { let value = try intoOnlyValue(value) return try LambdaSStoreTransaction(protobuf: firstBody, value) - case .stateSignatureTransaction(let code): - throw HError.fromProtobuf("unrecognized: stateSignatureTransaction `\(code)`") - - case .hintsPreprocessingVote(let code): - throw HError.fromProtobuf("unrecognized: hintsPreprocessingVote `\(code)`") - - case .hintsKeyPublication(let code): - throw HError.fromProtobuf("unrecognized: hintsKeyPublication `\(code)`") - - case .hintsPartialSignature(let code): - throw HError.fromProtobuf("unrecognized: hintsPartialSignature `\(code)`") - - case .historyProofSignature(let code): - throw HError.fromProtobuf("unrecognized: historyProofSignature `\(code)`") - - case .historyProofKeyPublication(let code): - throw HError.fromProtobuf("unrecognized: historyProofKeyPublication `\(code)`") - - case .historyProofVote(let code): - throw HError.fromProtobuf("unrecognized: historyProofVote `\(code)`") - - case .crsPublication(let code): - throw HError.fromProtobuf("unrecognized: crsPublication `\(code)`") + default: + throw HError.fromProtobuf("unrecognized code") } } } diff --git a/Tests/HieroTests/EvmHookCallTests.swift b/Tests/HieroTests/EvmHookCallTests.swift new file mode 100644 index 00000000..6817d015 --- /dev/null +++ b/Tests/HieroTests/EvmHookCallTests.swift @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +import HieroProtobufs +@testable import Hiero + +final class EvmHookCallUnitTests: XCTestCase { + + private let testCallData = Data([0x01, 0x23, 0x45]) + private let testGasLimit: UInt64 = 1_000_000 + + func test_GetSetCallData() { + // Given + var evmHookCall = EvmHookCall() + + // When + evmHookCall.data = testCallData + + // Then + XCTAssertEqual(evmHookCall.data, testCallData) + } + + func test_GetSetGasLimit() { + // Given + var evmHookCall = EvmHookCall() + + // When + evmHookCall.gasLimit = testGasLimit + + // Then + XCTAssertEqual(evmHookCall.gasLimit, testGasLimit) + } + + // MARK: - TEST_F(EvmHookCallUnitTests, FromProtobuf) + func test_FromProtobuf() throws { + // Given + var protoMsg = Proto_EvmHookCall() + protoMsg.data = testCallData + protoMsg.gasLimit = testGasLimit + + // When + let evmHookCall = try EvmHookCall.fromProtobuf(protoMsg) + + // Then + XCTAssertEqual(evmHookCall.data, testCallData) + XCTAssertEqual(evmHookCall.gasLimit, testGasLimit) + } + + // MARK: - TEST_F(EvmHookCallUnitTests, ToProtobuf) + func test_ToProtobuf() { + // Given + var evmHookCall = EvmHookCall() + evmHookCall.data = testCallData + evmHookCall.gasLimit = testGasLimit + + // When + let protoMsg = evmHookCall.toProtobuf() + + // Then + XCTAssertEqual(protoMsg.data, testCallData) + XCTAssertEqual(protoMsg.gasLimit, testGasLimit) + } +} diff --git a/Tests/HieroTests/EvmHookSpecTests.swift b/Tests/HieroTests/EvmHookSpecTests.swift new file mode 100644 index 00000000..eb47c3ca --- /dev/null +++ b/Tests/HieroTests/EvmHookSpecTests.swift @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +@testable import Hiero +import HieroProtobufs + +final class EvmHookSpecUnitTests: XCTestCase { + + private let testContractId = ContractId(shard: 1, realm: 2, num: 3) + + func test_GetSetContractId() { + // Given + var evmHookSpec = EvmHookSpec() + + // When + evmHookSpec.contractId = testContractId + + // Then + XCTAssertEqual(evmHookSpec.contractId, testContractId) + } + + // TEST_F(EvmHookSpecUnitTests, FromProtobuf) + func test_FromProtobuf() throws { + // Given + var protoSpec = Com_Hedera_Hapi_Node_Hooks_EvmHookSpec() + protoSpec.contractID = testContractId.toProtobuf() + + // When + let evmHookSpec = try EvmHookSpec.fromProtobuf(protoSpec) + + // Then + XCTAssertEqual(evmHookSpec.contractId, testContractId) + } + + func test_ToProtobuf() { + // Given + var evmHookSpec = EvmHookSpec() + evmHookSpec.contractId = testContractId + + // When + let protoSpec = evmHookSpec.toProtobuf() + + // Then + XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.shardNum), testContractId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.realmNum), testContractId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.contractNum), testContractId.num) + } +} diff --git a/Tests/HieroTests/HookCallTests.swift b/Tests/HieroTests/HookCallTests.swift new file mode 100644 index 00000000..66565214 --- /dev/null +++ b/Tests/HieroTests/HookCallTests.swift @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class HookCallUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let testAccountId = AccountId(shard: 1, realm: 2, num: 3) + private let testHookId: Int64 = 4 + private let testCallData = Data([0x56, 0x78, 0x9A]) + private let testGasLimit: UInt64 = 11 + + private var testHookEntityId: HookEntityId { + return HookEntityId(testAccountId) + } + + private var testFullHookId: HookId { + return HookId(entityId: testHookEntityId, hookId: testHookId) + } + + private var testEvmHookCall: EvmHookCall { + var c = EvmHookCall() + c.data = testCallData + c.gasLimit = testGasLimit + return c + } + + func test_GetSetFullHookId() { + // Given + var hookCall = HookCall() + + // When + hookCall.fullHookId(testFullHookId) + + // Then + XCTAssertNotNil(hookCall.fullHookId?.entityId.accountId) + XCTAssertEqual(hookCall.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(hookCall.fullHookId?.hookId, testHookId) + } + + func test_GetSetFullHookIdResetsHookId() { + // Given + var hookCall = HookCall() + + // When + hookCall.hookId(testHookId) + hookCall.fullHookId(testFullHookId) + + // Then + XCTAssertNil(hookCall.hookId) + } + + func test_GetSetHookId() { + // Given + var hookCall = HookCall() + + // When + hookCall.hookId(testHookId) + + // Then + XCTAssertEqual(hookCall.hookId, testHookId) + } + + func test_GetSetHookIdResetsFullHookId() { + // Given + var hookCall = HookCall() + + // When + hookCall.fullHookId(testFullHookId) + hookCall.hookId(testHookId) + + // Then + XCTAssertNil(hookCall.fullHookId) + } + + func test_GetSetEvmHookCall() { + // Given + var hookCall = HookCall() + + // When + hookCall.evmHookCall(testEvmHookCall) + + // Then + XCTAssertNotNil(hookCall.evmHookCall) + XCTAssertEqual(hookCall.evmHookCall?.data, testCallData) + XCTAssertEqual(hookCall.evmHookCall?.gasLimit, testGasLimit) + } + + func test_FromProtobuf() throws { + // Given + var protoFull = Proto_HookCall() + var protoHookIdOnly = Proto_HookCall() + + // full_hook_id + evm_hook_call + protoFull.fullHookID = testFullHookId.toProtobuf() + protoFull.evmHookCall = testEvmHookCall.toProtobuf() + + // hook_id only + protoHookIdOnly.hookID = testHookId + + // When + let hookCallFull = try HookCall.fromProtobuf(protoFull) + let hookCallHookOnly = try HookCall.fromProtobuf(protoHookIdOnly) + + // Then + XCTAssertNotNil(hookCallFull.fullHookId?.entityId.accountId) + XCTAssertEqual(hookCallFull.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(hookCallFull.fullHookId?.hookId, testHookId) + + XCTAssertNotNil(hookCallFull.evmHookCall) + XCTAssertEqual(hookCallFull.evmHookCall?.data, testCallData) + XCTAssertEqual(hookCallFull.evmHookCall?.gasLimit, testGasLimit) + + XCTAssertEqual(hookCallHookOnly.hookId, testHookId) + } + + func test_ToProtobuf() { + // Given + var hookCallFull = HookCall() + var hookCallHookOnly = HookCall() + + hookCallFull.fullHookId = testFullHookId + hookCallFull.evmHookCall = testEvmHookCall + + hookCallHookOnly.hookId = testHookId + + // When + let protoFull = hookCallFull.toProtobuf() + let protoHookOnly = hookCallHookOnly.toProtobuf() + + // Then + XCTAssertTrue(protoFull.fullHookID.hasEntityID) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.accountNum), testAccountId.num) + XCTAssertEqual(protoFull.fullHookID.hookID, testHookId) + + XCTAssertEqual(protoFull.evmHookCall.data, testCallData) + XCTAssertEqual(protoFull.evmHookCall.gasLimit, testGasLimit) + + XCTAssertEqual(protoHookOnly.hookID, testHookId) + } +} diff --git a/Tests/HieroTests/HookCreationDetailsTests.swift b/Tests/HieroTests/HookCreationDetailsTests.swift new file mode 100644 index 00000000..904b233c --- /dev/null +++ b/Tests/HieroTests/HookCreationDetailsTests.swift @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class HookCreationDetailsUnitTests: XCTestCase { + + private func makeStorageSlot(_ key: Data, _ value: Data) -> LambdaStorageSlot { + var s = LambdaStorageSlot() + s.key(key) + s.value(value) + return s + } + + private func makeEntryWithKey(_ key: Data, _ value: Data) -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key) + e.value(value) + return e + } + + private func makeEntryWithPreimage(_ preimage: Data, _ value: Data) -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.preimage(preimage) + e.value(value) + return e + } + + private func makeMappingEntries(_ mappingSlot: Data, _ entries: [LambdaMappingEntry]) -> LambdaMappingEntries { + var me = LambdaMappingEntries() + me.setEntries(entries) + me.mappingSlot(mappingSlot) + return me + } + + private func makeStorageUpdateStorageSlot(_ slot: LambdaStorageSlot) -> LambdaStorageUpdate { + var u = LambdaStorageUpdate() + u.setStorageSlot(slot) + return u + } + + private func makeStorageUpdateMappingEntries(_ entries: LambdaMappingEntries) -> LambdaStorageUpdate { + var u = LambdaStorageUpdate() + u.setMappingEntries(entries) + return u + } + + private func makeLambdaEvmHook() -> LambdaEvmHook { + let key1 = Data([0x01, 0x23, 0x45]) + let key2 = Data([0x67, 0x89, 0xAB]) + let preimage = Data([0xCD, 0xEF, 0x02]) + + let value1 = Data([0x04, 0x06, 0x08]) + let value2 = Data([0x0A, 0x0C, 0x0E]) + let value3 = Data([0x11, 0x13, 0x15]) + + let mappingSlot = Data([0x17, 0x19, 0x1B]) + + let slot = makeStorageSlot(key1, value1) + let update1 = makeStorageUpdateStorageSlot(slot) + + let e1 = makeEntryWithKey(key2, value2) + let e2 = makeEntryWithPreimage(preimage, value3) + let me = makeMappingEntries(mappingSlot, [e1, e2]) + let update2 = makeStorageUpdateMappingEntries(me) + + var hook = LambdaEvmHook() + hook.addStorageUpdate(update1) + hook.addStorageUpdate(update2) + return hook + } + + private func makeAdminKey() throws -> Key { + let priv = PrivateKey.generateEcdsa() + return .single(priv.publicKey) + } + + func test_GetSetHookId() throws { + // Given + var details = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + + // When + details.hookId(1) + + // Then + XCTAssertEqual(details.hookId, 1) + } + + func test_GetSetLambdaEvmHook() { + // Given + var details = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + let lambda = makeLambdaEvmHook() + + // When + details.lambdaEvmHook(lambda) + + // Then + XCTAssertNotNil(details.lambdaEvmHook) + XCTAssertEqual(details.lambdaEvmHook?.storageUpdates.count, lambda.storageUpdates.count) + } + + func test_GetSetAdminKey() throws { + // Given + var details = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + let key = try makeAdminKey() + + // When + details.adminKey(key) + + // Then + // Round-trip bytes check if your Key exposes them; otherwise just assert non-nil. + XCTAssertNotNil(details.adminKey) + XCTAssertEqual(details.adminKey?.toBytes(), key.toBytes()) + } + + func test_FromProtobuf() throws { + // Given + var proto = Com_Hedera_Hapi_Node_Hooks_HookCreationDetails() + proto.extensionPoint = HookExtensionPoint.accountAllowanceHook.toProtobuf() + proto.hookID = 1 + proto.lambdaEvmHook = makeLambdaEvmHook().toProtobuf() + + let key = try makeAdminKey() + proto.adminKey = key.toProtobuf() + + // When + let decoded = try HookCreationDetails.fromProtobuf(proto) + + // Then + XCTAssertEqual(decoded.hookExtensionPoint, .accountAllowanceHook) + XCTAssertEqual(decoded.hookId, 1) + XCTAssertNotNil(decoded.lambdaEvmHook) + XCTAssertNotNil(decoded.adminKey) + } + + func test_ToProtobuf() throws { + // Given + var details = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + details.hookId(1) + details.lambdaEvmHook(makeLambdaEvmHook()) + + let key = try makeAdminKey() + details.adminKey(key) + + // When + let proto = details.toProtobuf() + + // Then + XCTAssertEqual(proto.extensionPoint, HookExtensionPoint.accountAllowanceHook.toProtobuf()) + XCTAssertEqual(proto.hookID, 1) + XCTAssertEqual(proto.hook, .lambdaEvmHook(proto.lambdaEvmHook)) + XCTAssertTrue(proto.hasAdminKey) + } +} diff --git a/Tests/HieroTests/HookEntityIdTests.swift b/Tests/HieroTests/HookEntityIdTests.swift new file mode 100644 index 00000000..f0785e08 --- /dev/null +++ b/Tests/HieroTests/HookEntityIdTests.swift @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +@testable import Hiero // TODO: Replace with your Swift target/module name +import HieroProtobufs + +final class HookEntityIdUnitTests: XCTestCase { + + private let testAccountId = AccountId(shard: 1, realm: 2, num: 3) + + func test_GetSetAccountId() { + // Given + let hookEntityId = HookEntityId(AccountId(num: 0)) + + // When + hookEntityId.accountId(testAccountId) + + // Then + XCTAssertEqual(hookEntityId.accountId, testAccountId) + } + + func test_FromProtobuf() throws { + // Given + var protoMsg = Proto_HookEntityId() + protoMsg.accountID = testAccountId.toProtobuf() + + // When + let hookEntityId = try HookEntityId.fromProtobuf(protoMsg) + + // Then + XCTAssertEqual(hookEntityId.accountId, testAccountId) + } + + func test_ToProtobuf() { + // Given + let hookEntityId = HookEntityId(testAccountId) + + // When + let protoMsg = hookEntityId.toProtobuf() + + // Then + XCTAssertEqual(UInt64(truncatingIfNeeded: protoMsg.accountID.shardNum), testAccountId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoMsg.accountID.realmNum), testAccountId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoMsg.accountID.accountNum), testAccountId.num) + } +} diff --git a/Tests/HieroTests/HookIdTests.swift b/Tests/HieroTests/HookIdTests.swift new file mode 100644 index 00000000..0706533e --- /dev/null +++ b/Tests/HieroTests/HookIdTests.swift @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +@testable import Hiero +import HieroProtobufs + +final class HookIdUnitTests: XCTestCase { + + private let testAccountId = AccountId(shard: 1, realm: 2, num: 3) + private let testHookId: Int64 = 4 + private var testHookEntityId: HookEntityId { + return HookEntityId(testAccountId) + } + + func test_GetSetEntityId() { + // Given + var hookId = HookId() + + // When + hookId.entityId(testHookEntityId) + + // Then + XCTAssertNotNil(hookId.entityId.accountId) + XCTAssertEqual(hookId.entityId.accountId, testAccountId) + } + + func test_GetSetHookId() { + // Given + var hookId = HookId() + + // When + hookId.hookId(testHookId) + + // Then + XCTAssertEqual(hookId.hookId, testHookId) + } + + func test_FromProtobuf() throws { + // Given + var proto = Proto_HookId() + proto.entityID = testHookEntityId.toProtobuf() + proto.hookID = testHookId + + // When + let hookId = try HookId.fromProtobuf(proto) + + // Then + XCTAssertNotNil(hookId.entityId.accountId) + XCTAssertEqual(hookId.entityId.accountId, testAccountId) + XCTAssertEqual(hookId.hookId, testHookId) + } + + func test_ToProtobuf() { + // Given + let hookId = HookId(entityId: testHookEntityId, hookId: testHookId) + + // When + let proto = hookId.toProtobuf() + + // Then + XCTAssertTrue(proto.hasEntityID) + XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.accountNum), testAccountId.num) + } +} diff --git a/Tests/HieroTests/LambdaEvmHookTests.swift b/Tests/HieroTests/LambdaEvmHookTests.swift new file mode 100644 index 00000000..ef70c4f2 --- /dev/null +++ b/Tests/HieroTests/LambdaEvmHookTests.swift @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class LambdaEvmHookUnitTests: XCTestCase { + + private func makeStorageSlot(key: Data, value: Data) -> LambdaStorageSlot { + var s = LambdaStorageSlot() + s.key(key) + s.value(value) + return s + } + + private func makeEntryWithKey(_ key: Data, value: Data) -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key) + e.value(value) + return e + } + + private func makeEntryWithPreimage(_ preimage: Data, value: Data) -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.preimage(preimage) + e.value(value) + return e + } + + private func makeMappingEntries(mappingSlot: Data, _ entries: [LambdaMappingEntry]) -> LambdaMappingEntries { + var me = LambdaMappingEntries() + me.mappingSlot(mappingSlot) + me.setEntries(entries) + return me + } + + private func makeStorageUpdateStorageSlot(_ slot: LambdaStorageSlot) -> LambdaStorageUpdate { + var u = LambdaStorageUpdate() + u.setStorageSlot(slot) + return u + } + + private func makeStorageUpdateMappingEntries(_ entries: LambdaMappingEntries) -> LambdaStorageUpdate { + var u = LambdaStorageUpdate() + u.setMappingEntries(entries) + return u + } + + private func makeUpdates() -> [LambdaStorageUpdate] { + // test bytes + let key1 = Data([0x01, 0x23, 0x45]) + let key2 = Data([0x67, 0x89, 0xAB]) + let preimage = Data([0xCD, 0xEF, 0x02]) + + let value1 = Data([0x04, 0x06, 0x08]) + let value2 = Data([0x0A, 0x0C, 0x0E]) + let value3 = Data([0x11, 0x13, 0x15]) + + let mappingSlot = Data([0x17, 0x19, 0x1B]) + + let slot = makeStorageSlot(key: key1, value: value1) + let update1 = makeStorageUpdateStorageSlot(slot) + + let e1 = makeEntryWithKey(key2, value: value2) + let e2 = makeEntryWithPreimage(preimage, value: value3) + let me = makeMappingEntries(mappingSlot: mappingSlot, [e1, e2]) + let update2 = makeStorageUpdateMappingEntries(me) + + return [update1, update2] + } + + func test_GetSetStorageUpdates() { + // Given + var hook = LambdaEvmHook() + let updates = makeUpdates() + + // When + hook.setStorageUpdates(updates) + + // Then + XCTAssertEqual(hook.storageUpdates.count, updates.count) + } + + func test_AddStorageUpdate() { + // Given + var hook = LambdaEvmHook() + let updates = makeUpdates() + + // When + for u in updates { + hook.addStorageUpdate(u) + } + + // Then + XCTAssertEqual(hook.storageUpdates.count, updates.count) + } + + func test_ClearStorageUpdates() { + // Given + var hook = LambdaEvmHook() + hook.setStorageUpdates(makeUpdates()) + + // When + hook.clearStorageUpdates() + + // Then + XCTAssertTrue(hook.storageUpdates.isEmpty) + } + + func test_FromProtobuf() throws { + // Given + var proto = Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook() + proto.storageUpdates = makeUpdates().map { $0.toProtobuf() } + + // When + let decoded = try LambdaEvmHook.fromProtobuf(proto) + + // Then + XCTAssertEqual(decoded.storageUpdates.count, proto.storageUpdates.count) + } + + func test_ToProtobuf() { + // Given + var hook = LambdaEvmHook() + let updates = makeUpdates() + hook.setStorageUpdates(updates) + + // When + let proto = hook.toProtobuf() + + // Then + XCTAssertEqual(proto.storageUpdates.count, updates.count) + } +} diff --git a/Tests/HieroTests/LambdaMappingEntriesTests.swift b/Tests/HieroTests/LambdaMappingEntriesTests.swift new file mode 100644 index 00000000..ebfc25cc --- /dev/null +++ b/Tests/HieroTests/LambdaMappingEntriesTests.swift @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class LambdaMappingEntriesUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let mappingSlot = Data([0x17, 0x19, 0x1B]) + + private let key1 = Data([0x01, 0x23, 0x45]) + private let key3 = Data([0x67, 0x89, 0xAB]) + private let preimage2 = Data([0xCD, 0xEF, 0x02]) + + private let value1 = Data([0x04, 0x06, 0x08]) + private let value2 = Data([0x0A, 0x0C, 0x0E]) + private let value3 = Data([0x11, 0x13, 0x15]) + + private func makeEntry1() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key1) + e.value(value1) + return e + } + + private func makeEntry2() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.preimage(preimage2) + e.value(value2) + return e + } + + private func makeEntry3() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key3) + e.value(value3) + return e + } + + private func makeEntries() -> [LambdaMappingEntry] { + [makeEntry1(), makeEntry2(), makeEntry3()] + } + + func test_GetSetMappingSlot() { + // Given + var entries = LambdaMappingEntries() + + // When + entries.mappingSlot(mappingSlot) + + // Then + XCTAssertEqual(entries.mappingSlot, mappingSlot) + } + + func test_GetSetEntries() { + // Given + var entries = LambdaMappingEntries() + + // When + entries.setEntries(makeEntries()) + + // Then + XCTAssertEqual(entries.entries.count, makeEntries().count) + } + + func test_AddEntry() { + // Given + var entries = LambdaMappingEntries() + let e1 = makeEntry1() + + // When + entries.addEntry(e1) + + // Then + XCTAssertEqual(entries.entries.count, 1) + XCTAssertNotNil(entries.entries[0].key) + XCTAssertEqual(entries.entries[0].key, key1) + XCTAssertEqual(entries.entries[0].value, value1) + } + + func test_ClearEntries() { + // Given + var entries = LambdaMappingEntries() + entries.setEntries(makeEntries()) + + // When + entries.clearEntries() + + // Then + XCTAssertTrue(entries.entries.isEmpty) + } + + func test_FromProtobuf() throws { + // Given + var proto = Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntries() + proto.mappingSlot = mappingSlot + proto.entries = makeEntries().map { $0.toProtobuf() } + + // When + let decoded = try LambdaMappingEntries.fromProtobuf(proto) + + // Then + XCTAssertEqual(decoded.mappingSlot, mappingSlot) + XCTAssertEqual(decoded.entries.count, makeEntries().count) + } + + func test_ToProtobuf() { + // Given + var entries = LambdaMappingEntries() + entries.mappingSlot(mappingSlot) + entries.setEntries(makeEntries()) + + // When + let proto = entries.toProtobuf() + + // Then + XCTAssertEqual(proto.mappingSlot, mappingSlot) + XCTAssertEqual(proto.entries.count, makeEntries().count) + } +} diff --git a/Tests/HieroTests/LambdaMappingEntryTests.swift b/Tests/HieroTests/LambdaMappingEntryTests.swift new file mode 100644 index 00000000..c410be72 --- /dev/null +++ b/Tests/HieroTests/LambdaMappingEntryTests.swift @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class LambdaMappingEntryUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let testKey = Data([0x01, 0x23, 0x45]) + private let testPreimage = Data([0x67, 0x89, 0xAB]) + private let testValue = Data([0xCD, 0xEF, 0x02]) + + func test_GetSetKey() { + // Given + var entry = LambdaMappingEntry() + + // When + entry.key(testKey) + + // Then + XCTAssertNotNil(entry.key) + XCTAssertEqual(entry.key, testKey) + } + + func test_GetSetKeyResetPreimage() { + // Given + var entry = LambdaMappingEntry() + + // When + entry.key(testKey) + entry.preimage(testPreimage) + + // Then + XCTAssertNil(entry.key) + } + + func test_GetSetPreimage() { + // Given + var entry = LambdaMappingEntry() + + // When + entry.preimage(testPreimage) + + // Then + XCTAssertNotNil(entry.preimage) + XCTAssertEqual(entry.preimage, testPreimage) + } + + func test_GetSetPreimageResetsKey() { + // Given + var entry = LambdaMappingEntry() + + // When + entry.preimage(testPreimage) + entry.key(testKey) + + // Then + XCTAssertNil(entry.preimage) + } + + func test_GetSetValue() { + // Given + var entry = LambdaMappingEntry() + + // When + entry.value(testValue) + + // Then + XCTAssertEqual(entry.value, testValue) + } + + func test_FromProtobuf() throws { + // Given + var protoKey = Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry() + var protoPreimage = Com_Hedera_Hapi_Node_Hooks_LambdaMappingEntry() + + protoKey.key = testKey + protoKey.value = testValue + + protoPreimage.preimage = testPreimage + + // When + let entryKey = try LambdaMappingEntry.fromProtobuf(protoKey) + let entryPreimage = try LambdaMappingEntry.fromProtobuf(protoPreimage) + + // Then + XCTAssertNotNil(entryKey.key) + XCTAssertEqual(entryKey.key, testKey) + XCTAssertEqual(entryKey.value, testValue) + + XCTAssertNotNil(entryPreimage.preimage) + XCTAssertEqual(entryPreimage.preimage, testPreimage) + } + + func test_ToProtobuf() { + // Given + var entryKey = LambdaMappingEntry() + var entryPreimage = LambdaMappingEntry() + + entryKey.key(testKey) + entryKey.value(testValue) + + entryPreimage.preimage(testPreimage) + + // When + let protoKey = entryKey.toProtobuf() + let protoPreimage = entryPreimage.toProtobuf() + + // Then + // key path + XCTAssertEqual(protoKey.key, testKey) + XCTAssertEqual(protoKey.value, testValue) + + // preimage path + XCTAssertEqual(protoPreimage.preimage, testPreimage) + } +} diff --git a/Tests/HieroTests/LambdaStorageSlotTests.swift b/Tests/HieroTests/LambdaStorageSlotTests.swift new file mode 100644 index 00000000..140cf7ae --- /dev/null +++ b/Tests/HieroTests/LambdaStorageSlotTests.swift @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class LambdaStorageSlotUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let testKey = Data([0x01, 0x23, 0x45]) + private let testValue = Data([0x67, 0x89, 0xAB]) + + func test_GetSetKey() { + // Given + var slot = LambdaStorageSlot() + + // When + slot.key(testKey) + + // Then + XCTAssertEqual(slot.key, testKey) + } + + func test_GetSetValue() { + // Given + var slot = LambdaStorageSlot() + + // When + slot.value(testValue) + + // Then + XCTAssertEqual(slot.value, testValue) + } + + func test_FromProtobuf() throws { + // Given + var proto = Com_Hedera_Hapi_Node_Hooks_LambdaStorageSlot() + proto.key = testKey + proto.value = testValue + + // When + let slot = try LambdaStorageSlot.fromProtobuf(proto) + + // Then + XCTAssertEqual(slot.key, testKey) + XCTAssertEqual(slot.value, testValue) + } + + func test_ToProtobuf() { + // Given + var slot = LambdaStorageSlot() + slot.key(testKey) + slot.value(testValue) + + // When + let proto = slot.toProtobuf() + + // Then + XCTAssertEqual(proto.key, testKey) + XCTAssertEqual(proto.value, testValue) + } +} diff --git a/Tests/HieroTests/LambdaStorageUpdateTests.swift b/Tests/HieroTests/LambdaStorageUpdateTests.swift new file mode 100644 index 00000000..01a1ad8b --- /dev/null +++ b/Tests/HieroTests/LambdaStorageUpdateTests.swift @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 + +import XCTest +import Foundation +@testable import Hiero +import HieroProtobufs + +final class LambdaStorageUpdateUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let mappingSlot = Data([0x17, 0x19, 0x1B]) + + private let key1 = Data([0x01, 0x23, 0x45]) + private let key3 = Data([0x67, 0x89, 0xAB]) + + private let preimage2 = Data([0xCD, 0xEF, 0x02]) + + private let value1 = Data([0x04, 0x06, 0x08]) + private let value2 = Data([0x0A, 0x0C, 0x0E]) + private let value3 = Data([0x11, 0x13, 0x15]) + + private func makeEntry1() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key1) + e.value(value1) + return e + } + + private func makeEntry2() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.preimage(preimage2) + e.value(value2) + return e + } + + private func makeEntry3() -> LambdaMappingEntry { + var e = LambdaMappingEntry() + e.key(key3) + e.value(value3) + return e + } + + private func makeEntries() -> LambdaMappingEntries { + var le = LambdaMappingEntries() + le.setEntries([makeEntry1(), makeEntry2(), makeEntry3()]) + le.mappingSlot(mappingSlot) + return le + } + + private func makeStorageSlot() -> LambdaStorageSlot { + var s = LambdaStorageSlot() + s.key(key1) + s.value(value1) + return s + } + + func test_GetSetStorageSlot() { + // Given + var update = LambdaStorageUpdate() + + // When + update.setStorageSlot(makeStorageSlot()) + + // Then + XCTAssertNotNil(update.storageSlot) + XCTAssertEqual(update.storageSlot?.key, key1) + XCTAssertEqual(update.storageSlot?.value, value1) + } + + func test_GetSetMappingEntries() { + // Given + var update = LambdaStorageUpdate() + let entries = makeEntries() + + // When + update.setMappingEntries(entries) + + // Then + XCTAssertNotNil(update.mappingEntries) + XCTAssertEqual(update.mappingEntries?.entries.count, entries.entries.count) + } + + func test_SetStorageSlotResetsMappingEntries() { + // Given + var update = LambdaStorageUpdate() + + // When + update.setMappingEntries(makeEntries()) + update.setStorageSlot(makeStorageSlot()) + + // Then + XCTAssertNil(update.mappingEntries) + } + + func test_SetMappingEntriesResetsStorageSlot() { + // Given + var update = LambdaStorageUpdate() + + // When + update.setStorageSlot(makeStorageSlot()) + update.setMappingEntries(makeEntries()) + + // Then + XCTAssertNil(update.storageSlot) + } + + func test_FromProtobuf() throws { + // Given + var protoSlot = Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate() + protoSlot.storageSlot = makeStorageSlot().toProtobuf() + + var protoEntries = Com_Hedera_Hapi_Node_Hooks_LambdaStorageUpdate() + protoEntries.mappingEntries = makeEntries().toProtobuf() + + // When + let decodedSlot = try LambdaStorageUpdate.fromProtobuf(protoSlot) + let decodedEntries = try LambdaStorageUpdate.fromProtobuf(protoEntries) + + // Then + XCTAssertNotNil(decodedSlot.storageSlot) + XCTAssertEqual(decodedSlot.storageSlot?.key, key1) + XCTAssertEqual(decodedSlot.storageSlot?.value, value1) + + XCTAssertNotNil(decodedEntries.mappingEntries) + XCTAssertEqual(decodedEntries.mappingEntries?.entries.count, 3) + } + + func test_ToProtobuf() { + // Given + var updateSlot = LambdaStorageUpdate() + var updateEntries = LambdaStorageUpdate() + + updateSlot.setStorageSlot(makeStorageSlot()) + updateEntries.setMappingEntries(makeEntries()) + + // When + let protoSlot = updateSlot.toProtobuf() + let protoEntries = updateEntries.toProtobuf() + + // Then + // storageSlot path + XCTAssertEqual(protoSlot.storageSlot.key, key1) + XCTAssertEqual(protoSlot.storageSlot.value, value1) + + // mappingEntries path + XCTAssertEqual(protoEntries.mappingEntries.mappingSlot, mappingSlot) + XCTAssertEqual(protoEntries.mappingEntries.entries.count, 3) + } +} From 4e0409a6d6c38a9d3ed75f9d74e0b527ae83bb20 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Thu, 16 Oct 2025 14:07:24 -0400 Subject: [PATCH 3/7] feat: hooks Signed-off-by: Rob Walworth --- .../Account/AccountCreateTransaction.swift | 24 ++ .../Account/AccountUpdateTransaction.swift | 50 +++ .../Contract/ContractCreateTransaction.swift | 23 ++ .../Contract/ContractUpdateTransaction.swift | 47 +++ Sources/Hiero/Hooks/HookCreationDetails.swift | 2 +- Sources/Hiero/Hooks/LambdaMappingEntry.swift | 2 +- Sources/Hiero/Status.swift | 192 ++++++++++ .../HieroE2ETests/Account/AccountCreate.swift | 171 +++++++-- .../HieroE2ETests/Account/AccountUpdate.swift | 348 ++++++++++++++++++ Tests/HieroE2ETests/Contract/Contract.swift | 2 +- .../Contract/ContractCreate.swift | 183 +++++++++ .../Contract/ContractUpdate.swift | 341 +++++++++++++++++ Tests/HieroTests/EvmHookCallTests.swift | 3 +- Tests/HieroTests/EvmHookSpecTests.swift | 7 +- Tests/HieroTests/HookCallTests.swift | 14 +- .../HieroTests/HookCreationDetailsTests.swift | 5 +- Tests/HieroTests/HookEntityIdTests.swift | 3 +- Tests/HieroTests/HookIdTests.swift | 7 +- Tests/HieroTests/LambdaEvmHookTests.swift | 17 +- .../LambdaMappingEntriesTests.swift | 9 +- .../HieroTests/LambdaMappingEntryTests.swift | 9 +- Tests/HieroTests/LambdaStorageSlotTests.swift | 7 +- .../HieroTests/LambdaStorageUpdateTests.swift | 5 +- 23 files changed, 1397 insertions(+), 74 deletions(-) diff --git a/Sources/Hiero/Account/AccountCreateTransaction.swift b/Sources/Hiero/Account/AccountCreateTransaction.swift index c113b78b..453f90f8 100644 --- a/Sources/Hiero/Account/AccountCreateTransaction.swift +++ b/Sources/Hiero/Account/AccountCreateTransaction.swift @@ -31,6 +31,7 @@ public final class AccountCreateTransaction: Transaction { self.stakedAccountId = stakedAccountId self.stakedNodeId = stakedNodeId self.declineStakingReward = declineStakingReward + self.hookCreationDetails = [] super.init() } @@ -56,6 +57,7 @@ public final class AccountCreateTransaction: Transaction { self.declineStakingReward = data.declineReward self.alias = !data.alias.isEmpty ? try EvmAddress(data.alias) : nil + self.hookCreationDetails = try data.hookCreationDetails.map { try HookCreationDetails.fromProtobuf($0) } try super.init(protobuf: proto) } @@ -289,6 +291,26 @@ public final class AccountCreateTransaction: Transaction { return self } + public var hookCreationDetails: [HookCreationDetails] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHook(_ hook: HookCreationDetails) -> Self { + self.hookCreationDetails.append(hook) + + return self + } + + @discardableResult + public func setHooks(_ hooks: [HookCreationDetails]) -> Self { + self.hookCreationDetails = hooks + + return self + } + internal override func validateChecksums(on ledgerId: LedgerId) throws { try stakedAccountId?.validateChecksums(on: ledgerId) try autoRenewAccountId?.validateChecksums(on: ledgerId) @@ -336,6 +358,8 @@ extension AccountCreateTransaction: ToProtobuf { } proto.declineReward = declineStakingReward + + proto.hookCreationDetails = hookCreationDetails.map { $0.toProtobuf() } } } } diff --git a/Sources/Hiero/Account/AccountUpdateTransaction.swift b/Sources/Hiero/Account/AccountUpdateTransaction.swift index 354f2d27..40533d67 100644 --- a/Sources/Hiero/Account/AccountUpdateTransaction.swift +++ b/Sources/Hiero/Account/AccountUpdateTransaction.swift @@ -15,6 +15,8 @@ import SwiftProtobuf public final class AccountUpdateTransaction: Transaction { /// Create a new `AccountCreateTransaction` ready for configuration. public override init() { + self.hookCreationDetails = [] + self.hooksToDelete = [] super.init() } @@ -59,6 +61,8 @@ public final class AccountUpdateTransaction: Transaction { self.stakedAccountId = stakedAccountId self.stakedNodeId = stakedNodeId self.declineStakingReward = data.hasDeclineReward ? data.declineReward.value : nil + self.hookCreationDetails = try data.hookCreationDetails.map { try HookCreationDetails.fromProtobuf($0) } + self.hooksToDelete = data.hookIdsToDelete try super.init(protobuf: proto) } @@ -288,6 +292,46 @@ public final class AccountUpdateTransaction: Transaction { return self } + public var hookCreationDetails: [HookCreationDetails] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHookToCreate(_ hook: HookCreationDetails) -> Self { + self.hookCreationDetails.append(hook) + + return self + } + + @discardableResult + public func setHooksToCreate(_ hooks: [HookCreationDetails]) -> Self { + self.hookCreationDetails = hooks + + return self + } + + public var hooksToDelete: [Int64] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHookToDelete(_ hook: Int64) -> Self { + self.hooksToDelete.append(hook) + + return self + } + + @discardableResult + public func setHooksToDelete(_ hooks: [Int64]) -> Self { + self.hooksToDelete = hooks + + return self + } + internal override func validateChecksums(on ledgerId: LedgerId) throws { try accountId?.validateChecksums(on: ledgerId) try stakedAccountId?.validateChecksums(on: ledgerId) @@ -347,6 +391,12 @@ extension AccountUpdateTransaction: ToProtobuf { if let declineStakingReward = declineStakingReward { proto.declineReward = Google_Protobuf_BoolValue(declineStakingReward) } + + for hook in hookCreationDetails { + proto.hookCreationDetails.append(hook.toProtobuf()) + } + + proto.hookIdsToDelete = hooksToDelete } } } diff --git a/Sources/Hiero/Contract/ContractCreateTransaction.swift b/Sources/Hiero/Contract/ContractCreateTransaction.swift index 468b6673..8b0d6d50 100644 --- a/Sources/Hiero/Contract/ContractCreateTransaction.swift +++ b/Sources/Hiero/Contract/ContractCreateTransaction.swift @@ -72,6 +72,7 @@ public final class ContractCreateTransaction: Transaction { self.stakedNodeId = stakedNodeId } self.declineStakingReward = declineStakingReward + self.hookCreationDetails = [] super.init() } @@ -104,6 +105,7 @@ public final class ContractCreateTransaction: Transaction { self.stakedAccountId = stakedAccountId self.stakedNodeId = stakedNodeId self.declineStakingReward = data.declineReward + self.hookCreationDetails = try data.hookCreationDetails.map { try HookCreationDetails.fromProtobuf($0) } try super.init(protobuf: proto) } @@ -343,6 +345,26 @@ public final class ContractCreateTransaction: Transaction { return self } + public var hookCreationDetails: [HookCreationDetails] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHook(_ hook: HookCreationDetails) -> Self { + self.hookCreationDetails.append(hook) + + return self + } + + @discardableResult + public func setHooks(_ hooks: [HookCreationDetails]) -> Self { + self.hookCreationDetails = hooks + + return self + } + internal override func validateChecksums(on ledgerId: LedgerId) throws { try bytecodeFileId?.validateChecksums(on: ledgerId) try autoRenewAccountId?.validateChecksums(on: ledgerId) @@ -391,6 +413,7 @@ extension ContractCreateTransaction: ToProtobuf { } proto.declineReward = declineStakingReward + proto.hookCreationDetails = hookCreationDetails.map { $0.toProtobuf() } } } } diff --git a/Sources/Hiero/Contract/ContractUpdateTransaction.swift b/Sources/Hiero/Contract/ContractUpdateTransaction.swift index 3bb62099..ef28f5ed 100644 --- a/Sources/Hiero/Contract/ContractUpdateTransaction.swift +++ b/Sources/Hiero/Contract/ContractUpdateTransaction.swift @@ -32,6 +32,8 @@ public final class ContractUpdateTransaction: Transaction { self.stakedAccountId = stakedAccountId self.stakedNodeId = stakedNodeId self.declineStakingReward = declineStakingReward + self.hookCreationDetails = [] + self.hooksToDelete = [] super.init() } @@ -75,6 +77,8 @@ public final class ContractUpdateTransaction: Transaction { self.stakedAccountId = stakedAccountId self.stakedNodeId = stakedNodeId self.declineStakingReward = data.hasDeclineReward ? data.declineReward.value : nil + self.hookCreationDetails = try data.hookCreationDetails.map { try HookCreationDetails.fromProtobuf($0) } + self.hooksToDelete = data.hookIdsToDelete try super.init(protobuf: proto) } @@ -289,6 +293,46 @@ public final class ContractUpdateTransaction: Transaction { return self } + public var hookCreationDetails: [HookCreationDetails] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHookToCreate(_ hook: HookCreationDetails) -> Self { + self.hookCreationDetails.append(hook) + + return self + } + + @discardableResult + public func setHooksToCreate(_ hooks: [HookCreationDetails]) -> Self { + self.hookCreationDetails = hooks + + return self + } + + public var hooksToDelete: [Int64] { + willSet { + ensureNotFrozen() + } + } + + @discardableResult + public func addHookToDelete(_ hook: Int64) -> Self { + self.hooksToDelete.append(hook) + + return self + } + + @discardableResult + public func setHooksToDelete(_ hooks: [Int64]) -> Self { + self.hooksToDelete = hooks + + return self + } + internal override func transactionExecute(_ channel: GRPCChannel, _ request: Proto_Transaction) async throws -> Proto_TransactionResponse { @@ -344,6 +388,9 @@ extension ContractUpdateTransaction: ToProtobuf { if let contractMemo = contractMemo { proto.memoWrapper = .init(contractMemo) } + + proto.hookCreationDetails = hookCreationDetails.map { $0.toProtobuf() } + proto.hookIdsToDelete = hooksToDelete } } } diff --git a/Sources/Hiero/Hooks/HookCreationDetails.swift b/Sources/Hiero/Hooks/HookCreationDetails.swift index 25a2c1b3..927ecd5c 100644 --- a/Sources/Hiero/Hooks/HookCreationDetails.swift +++ b/Sources/Hiero/Hooks/HookCreationDetails.swift @@ -78,7 +78,7 @@ extension HookCreationDetails: TryProtobufCodable { if let key = adminKey { proto.adminKey = key.toProtobuf() - } // else: leave unset + } // else: leave unset return proto } diff --git a/Sources/Hiero/Hooks/LambdaMappingEntry.swift b/Sources/Hiero/Hooks/LambdaMappingEntry.swift index 790be020..78018595 100644 --- a/Sources/Hiero/Hooks/LambdaMappingEntry.swift +++ b/Sources/Hiero/Hooks/LambdaMappingEntry.swift @@ -63,7 +63,7 @@ extension LambdaMappingEntry: TryProtobufCodable { if let preimage = preimage { proto.preimage = preimage } - + proto.value = value return proto } diff --git a/Sources/Hiero/Status.swift b/Sources/Hiero/Status.swift index da1dcdb2..dd0813bd 100644 --- a/Sources/Hiero/Status.swift +++ b/Sources/Hiero/Status.swift @@ -1160,6 +1160,84 @@ public enum Status: Equatable { /// The GRPC proxy endpoint is set in the NodeCreate or NodeUpdate transaction, which the network does not support. case grpcWebProxyNotSupported // = 399 + /// An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE. + case nftTransersOnlyAllowedForNonFungibleUnique // = 400 + + /// A HAPI client cannot set the SignedTransaction#use_serialized_tx_message_hash_algorithm field. + case invalidSerializedTxMessageHashAlgorithm // = 401 + + /// An EVM hook execution was throttled due to high network gas utilization. + case evmHookGasThrottled // = 500 + + /// A user tried to create a hook with an id already in use. + case hookIdInUse // = 501 + + /// A transaction tried to execute a hook that did not match the specified type or was malformed in some other way. + case badHookRequest // = 502 + + /// A CryptoTransfer relying on a ACCOUNT_ALLOWANCE hook was rejected. + case rejectedByAccountAllowanceHook // = 503 + + /// A hook id was not found. + case hookNotFound // = 504 + + /// A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + case lambdaStorageUpdateBytesTooLong // = 505 + + /// A lambda mapping slot, storage key, or storage value failed to use the minimal representation (i.e., no leading zeros). + case lambdaStorageUpdateBytesMustUseMinimalRepresentation // = 506 + + /// A hook id was invalid. + case invalidHookId // = 507 + + /// A lambda storage update had no contents. + case emptyLambdaStorageUpdate // = 508 + + /// A user repeated the same hook id in a creation details list. + case hookIdRepeatedInCreationDetails // = 509 + + /// Hooks are not not enabled on the target Hiero network. + case hooksNotEnabled // = 510 + + /// The target hook is not a lambda. + case hookIsNotALambda // = 511 + + /// A hook was deleted. + case hookDeleted // = 512 + + /// The LambdaSStore tried to update too many storage slots in a single transaction. + case tooManyLambdaStorageUpdates // = 513 + + /// A lambda mapping slot, storage key, or storage value failed to use the minimal representation (i.e., no leading zeros). + case hookCreationBytesNotUsingMinimalRepresenation // = 514 + + /// A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + case hookCreationBytesTooLong // = 515 + + /// A hook creation spec was not found. + case invalidHookCreationSpec // = 516 + + /// A hook extension point was empty. + case hookExtensionEmpty // = 517 + + /// A hook admin key was invalid. + case invalidHookAdminKey // = 518 + + /// The hook deletion requires the hook to have zero storage slots. + case hookDeletionRequiresZeroStorageSlots // = 519 + + /// Cannot set both a hook call and an approval on the same AccountAmount or NftTransfer message. + case cannotSetHooksAndApproval // = 520 + + /// The attempted operation is invalid until all the target entity's hooks have been deleted. + case transactionRequiresZeroHooks // = 521 + + /// The HookCall set in the transaction is invalid. + case invalidHookCall // = 522 + + /// Hooks are not supported to be used in TokenAirdrop transactions + case hooksAreNotSupportedInAirdrops // = 523 + /// swift-format-ignore: AlwaysUseLowerCamelCase case unrecognized(Int32) @@ -1522,6 +1600,32 @@ public enum Status: Equatable { case 397: self = .throttleGroupLcmOverflow case 398: self = .airdropContainsMultipleSendersForAToken case 399: self = .grpcWebProxyNotSupported + case 400: self = .nftTransersOnlyAllowedForNonFungibleUnique + case 401: self = .invalidSerializedTxMessageHashAlgorithm + case 500: self = .evmHookGasThrottled + case 501: self = .hookIdInUse + case 502: self = .badHookRequest + case 503: self = .rejectedByAccountAllowanceHook + case 504: self = .hookNotFound + case 505: self = .lambdaStorageUpdateBytesTooLong + case 506: self = .lambdaStorageUpdateBytesMustUseMinimalRepresentation + case 507: self = .invalidHookId + case 508: self = .emptyLambdaStorageUpdate + case 509: self = .hookIdRepeatedInCreationDetails + case 510: self = .hooksNotEnabled + case 511: self = .hookIsNotALambda + case 512: self = .hookDeleted + case 513: self = .tooManyLambdaStorageUpdates + case 514: self = .hookCreationBytesNotUsingMinimalRepresenation + case 515: self = .hookCreationBytesTooLong + case 516: self = .invalidHookCreationSpec + case 517: self = .hookExtensionEmpty + case 518: self = .invalidHookAdminKey + case 519: self = .hookDeletionRequiresZeroStorageSlots + case 520: self = .cannotSetHooksAndApproval + case 521: self = .transactionRequiresZeroHooks + case 522: self = .invalidHookCall + case 523: self = .hooksAreNotSupportedInAirdrops default: self = .unrecognized(rawValue) } } @@ -1886,6 +1990,32 @@ public enum Status: Equatable { case .throttleGroupLcmOverflow: return 397 case .airdropContainsMultipleSendersForAToken: return 398 case .grpcWebProxyNotSupported: return 399 + case .nftTransersOnlyAllowedForNonFungibleUnique: return 400 + case .invalidSerializedTxMessageHashAlgorithm: return 401 + case .evmHookGasThrottled: return 500 + case .hookIdInUse: return 501 + case .badHookRequest: return 502 + case .rejectedByAccountAllowanceHook: return 503 + case .hookNotFound: return 504 + case .lambdaStorageUpdateBytesTooLong: return 505 + case .lambdaStorageUpdateBytesMustUseMinimalRepresentation: return 506 + case .invalidHookId: return 507 + case .emptyLambdaStorageUpdate: return 508 + case .hookIdRepeatedInCreationDetails: return 509 + case .hooksNotEnabled: return 510 + case .hookIsNotALambda: return 511 + case .hookDeleted: return 512 + case .tooManyLambdaStorageUpdates: return 513 + case .hookCreationBytesNotUsingMinimalRepresenation: return 514 + case .hookCreationBytesTooLong: return 515 + case .invalidHookCreationSpec: return 516 + case .hookExtensionEmpty: return 517 + case .invalidHookAdminKey: return 518 + case .hookDeletionRequiresZeroStorageSlots: return 519 + case .cannotSetHooksAndApproval: return 520 + case .transactionRequiresZeroHooks: return 521 + case .invalidHookCall: return 522 + case .hooksAreNotSupportedInAirdrops: return 523 case .unrecognized(let i): return i } } @@ -2248,6 +2378,37 @@ extension Status: CaseIterable { .missingBatchKey, .batchKeySetOnNonInnerTransaction, .invalidBatchKey, + .scheduleExpiryNotConfigurable, + .creatingSystemEntities, + .throttleGroupLcmOverflow, + .airdropContainsMultipleSendersForAToken, + .grpcWebProxyNotSupported, + .nftTransersOnlyAllowedForNonFungibleUnique, + .invalidSerializedTxMessageHashAlgorithm, + .evmHookGasThrottled, + .hookIdInUse, + .badHookRequest, + .rejectedByAccountAllowanceHook, + .hookNotFound, + .lambdaStorageUpdateBytesTooLong, + .lambdaStorageUpdateBytesMustUseMinimalRepresentation, + .invalidHookId, + .emptyLambdaStorageUpdate, + .hookIdRepeatedInCreationDetails, + .hooksNotEnabled, + .hookIsNotALambda, + .hookDeleted, + .tooManyLambdaStorageUpdates, + .hookCreationBytesNotUsingMinimalRepresenation, + .hookCreationBytesTooLong, + .invalidHookCreationSpec, + .hookExtensionEmpty, + .invalidHookAdminKey, + .hookDeletionRequiresZeroStorageSlots, + .cannotSetHooksAndApproval, + .transactionRequiresZeroHooks, + .invalidHookCall, + .hooksAreNotSupportedInAirdrops, ] } @@ -2608,6 +2769,37 @@ extension Status { 392: "MISSING_BATCH_KEY", 393: "BATCH_KEY_SET_ON_NON_INNER_TRANSACTION", 394: "INVALID_BATCH_KEY", + 395: "SCHEDULE_EXPIRY_NOT_CONFIGURABLE", + 396: "CREATING_SYSTEM_ENTITIES", + 397: "THROTTLE_GROUP_LCM_OVERFLOW", + 398: "AIRDROP_CONTAINS_MULTIPLE_SENDERS_FOR_A_TOKEN", + 399: "GRPC_WEB_PROXY_NOT_SUPPORTED", + 400: "NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE", + 401: "INVALID_SERIALIZED_TX_MESSAGE_HASH_ALGORITHM", + 500: "EVM_HOOK_GAS_THROTTLED", + 501: "HOOK_ID_IN_USE", + 502: "BAD_HOOK_REQUEST", + 503: "REJECTED_BY_ACCOUNT_ALLOWANCE_HOOK", + 504: "HOOK_NOT_FOUND", + 505: "LAMBDA_STORAGE_UPDATE_BYTES_TOO_LONG", + 506: "LAMBDA_STORAGE_UPDATE_BYTES_MUST_USE_MINIMAL_REPRESENTATION", + 507: "INVALID_HOOK_ID", + 508: "EMPTY_LAMBDA_STORAGE_UPDATE", + 509: "HOOK_ID_REPEATED_IN_CREATION_DETAILS", + 510: "HOOKS_NOT_ENABLED", + 511: "HOOK_IS_NOT_A_LAMBDA", + 512: "HOOK_DELETED", + 513: "TOO_MANY_LAMBDA_STORAGE_UPDATES", + 514: "HOOK_CREATION_BYTES_MUST_USE_MINIMAL_REPRESENTATION", + 515: "HOOK_CREATION_BYTES_TOO_LONG", + 516: "INVALID_HOOK_CREATION_SPEC", + 517: "HOOK_EXTENSION_EMPTY", + 518: "INVALID_HOOK_ADMIN_KEY", + 519: "HOOK_DELETION_REQUIRES_ZERO_STORAGE_SLOTS", + 520: "CANNOT_SET_HOOKS_AND_APPROVAL", + 521: "TRANSACTION_REQUIRES_ZERO_HOOKS", + 522: "INVALID_HOOK_CALL", + 523: "HOOKS_ARE_NOT_SUPPORTED_IN_AIRDROPS", ] } diff --git a/Tests/HieroE2ETests/Account/AccountCreate.swift b/Tests/HieroE2ETests/Account/AccountCreate.swift index 8bf4fc65..db9820fb 100644 --- a/Tests/HieroE2ETests/Account/AccountCreate.swift +++ b/Tests/HieroE2ETests/Account/AccountCreate.swift @@ -4,6 +4,9 @@ import Hiero import XCTest internal final class AccountCreate: XCTestCase { + + private let testContractId = ContractId(shard: 1, realm: 2, num: 3) + internal func testInitialBalanceAndKey() async throws { let testEnv = try TestEnvironment.nonFree @@ -98,41 +101,6 @@ internal final class AccountCreate: XCTestCase { XCTAssertEqual(info.aliasKey, key.publicKey) } - // there's a disagreement between Java and Swift here. - // internal func testManagesExpiration() async throws { - // let testEnv = try TestEnvironment.nonFree - - // let key = PrivateKey.generateEd25519() - - // let receipt = try await AccountCreateTransaction() - // .keyWithoutAlias(.single(key.publicKey)) - // .transactionId( - // .withValidStart( - // testEnv.operator.accountId, - // .now - .seconds(40) - // ) - // ) - // .transactionValidDuration(.seconds(30)) - // .freezeWith(testEnv.client) - // .execute(testEnv.client) - // .getReceipt(testEnv.client) - - // let accountId = try XCTUnwrap(receipt.accountId) - - // addTeardownBlock { try await Account(id: accountId, key: key).delete(testEnv) } - - // let info = try await AccountInfoQuery(accountId: accountId).execute(testEnv.client) - - // XCTAssertEqual(info.accountId, accountId) - // XCTAssertFalse(info.isDeleted) - // XCTAssertEqual(info.key, .single(key.publicKey)) - // XCTAssertEqual(info.balance, 0) - // XCTAssertEqual(info.autoRenewPeriod, .days(90)) - // // fixme: ensure no warning gets emitted. - // // XCTAssertNil(info.proxyAccountId) - // XCTAssertEqual(info.proxyReceived, 0) - // } - internal func testAliasFromAdminKey() async throws { // Tests the third row of this table // https://github.com/hashgraph/hedera-improvement-proposal/blob/d39f740021d7da592524cffeaf1d749803798e9a/HIP/hip-583.md#signatures @@ -473,4 +441,137 @@ internal final class AccountCreate: XCTestCase { return true } + internal func test_CreateTransactionWithLambdaHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let ecdsaPrivateKey = PrivateKey.generateEcdsa() + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = testContractId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) + + // When / Then + var txReceipt: Hiero.TransactionReceipt! + do { + txReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(ecdsaPrivateKey.publicKey)) + .addHook(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(ecdsaPrivateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + + XCTAssertNotNil(txReceipt.accountId) + } + + func test_CreateTransactionWithLambdaHookAndStorageUpdates() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let ecdsaPrivateKey = PrivateKey.generateEcdsa() + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = testContractId + + var lambdaStorageSlot = LambdaStorageSlot() + lambdaStorageSlot.key = Data([0x01, 0x23, 0x45]) + lambdaStorageSlot.value = Data([0x67, 0x89, 0xAB]) + + var lambdaStorageUpdate = LambdaStorageUpdate() + lambdaStorageUpdate.storageSlot = lambdaStorageSlot + + lambdaEvmHook.addStorageUpdate(lambdaStorageUpdate) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) + + // When / Then + var txReceipt: Hiero.TransactionReceipt! + do { + txReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(ecdsaPrivateKey.publicKey)) + .addHook(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(ecdsaPrivateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + + XCTAssertNotNil(txReceipt.accountId) + } + + func test_CreateTransactionWithLambdaHookWithNoContractId() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let ecdsaPrivateKey = PrivateKey.generateEcdsa() + + var lambdaEvmHook = LambdaEvmHook() + + var lambdaStorageSlot = LambdaStorageSlot() + lambdaStorageSlot.key = Data([0x01, 0x23, 0x45]) + lambdaStorageSlot.value = Data([0x67, 0x89, 0xAB]) + + var lambdaStorageUpdate = LambdaStorageUpdate() + lambdaStorageUpdate.storageSlot = lambdaStorageSlot + + lambdaEvmHook.addStorageUpdate(lambdaStorageUpdate) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) + + // When / Then (expecting a precheck failure) + await assertThrowsHErrorAsync( + try await AccountCreateTransaction() + .keyWithoutAlias(.single(ecdsaPrivateKey.publicKey)) + .addHook(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client), "expected error creating account" + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + + XCTAssertEqual(status, .invalidHookId) + } + } + + func test_CreateTransactionWithSameLambdaHookIds() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let ecdsaPrivateKey = PrivateKey.generateEcdsa() + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = testContractId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) + + // When / Then — expect precheck error when duplicate hook IDs supplied + await assertThrowsHErrorAsync( + try await AccountCreateTransaction() + .addHook(hookCreationDetails) + .addHook(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + + XCTAssertEqual(status, .hookIdRepeatedInCreationDetails) + } + } + } diff --git a/Tests/HieroE2ETests/Account/AccountUpdate.swift b/Tests/HieroE2ETests/Account/AccountUpdate.swift index 573a7a15..383160b7 100644 --- a/Tests/HieroE2ETests/Account/AccountUpdate.swift +++ b/Tests/HieroE2ETests/Account/AccountUpdate.swift @@ -122,4 +122,352 @@ internal final class AccountUpdate: XCTestCase { XCTAssertEqual(status, .existingAutomaticAssociationsExceedGivenLimit) } } + + internal func test_CanaddHookToCreateToAccount() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + do { + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CannotUpdateWithMultipleOfSameHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + await assertThrowsHErrorAsync( + try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + return + } + XCTAssertEqual(status, .hookIdRepeatedInCreationDetails) + } + } + + internal func test_CannotUpdateWithHookAlreadyInUse() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // Add once + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Add again should fail + await assertThrowsHErrorAsync( + try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + return + } + XCTAssertEqual(status, .hookIdInUse) + } + } + + internal func test_CanaddHookToCreateToAccountWithStorageUpdates() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + var slot = LambdaStorageSlot() + slot.key = Data([0x01, 0x23, 0x45]) + slot.value = Data([0x67, 0x89, 0xAB]) + + var update = LambdaStorageUpdate() + update.storageSlot = slot + + lambdaEvmHook.addStorageUpdate(update) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + do { + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CanDeleteHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + // Add + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Delete + do { + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToDelete(hookId) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CannotDeleteNonExistentHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // Add + _ = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Delete non-existent hook + await assertThrowsHErrorAsync( + try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToDelete(999) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + return + } + XCTAssertEqual(status, .hookNotFound) + } + } + + internal func test_CannotAddAndDeleteSameHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let privateKey = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + addTeardownBlock { + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(testEnv.operator.accountId) + .sign(privateKey) + .execute(testEnv.client) + } + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = ContractId(shard: 1, realm: 2, num: 3) + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + await assertThrowsHErrorAsync( + try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails) + .addHookToDelete(hookId) + .freezeWith(testEnv.client) + .sign(privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + return + } + XCTAssertEqual(status, .hookNotFound) + } + } } diff --git a/Tests/HieroE2ETests/Contract/Contract.swift b/Tests/HieroE2ETests/Contract/Contract.swift index 170a756f..365dd8b8 100644 --- a/Tests/HieroE2ETests/Contract/Contract.swift +++ b/Tests/HieroE2ETests/Contract/Contract.swift @@ -47,7 +47,7 @@ extension ContractHelpers { let receipt = try await ContractCreateTransaction( bytecodeFileId: file.fileId, adminKey: operatorAdminKey ? .single(testEnv.operator.privateKey.publicKey) : nil, - gas: 200000, + gas: 300000, constructorParameters: ContractFunctionParameters().addString("Hello from Hiero.").toBytes(), contractMemo: "[e2e::ContractCreateTransaction]" ) diff --git a/Tests/HieroE2ETests/Contract/ContractCreate.swift b/Tests/HieroE2ETests/Contract/ContractCreate.swift index 7e64d011..66c757b7 100644 --- a/Tests/HieroE2ETests/Contract/ContractCreate.swift +++ b/Tests/HieroE2ETests/Contract/ContractCreate.swift @@ -4,6 +4,7 @@ import Hiero import XCTest internal final class ContractCreate: XCTestCase { + internal func testBasic() async throws { let testEnv = try TestEnvironment.nonFree @@ -129,4 +130,186 @@ internal final class ContractCreate: XCTestCase { XCTAssertEqual(status, .invalidFileID) } } + + internal func test_CreateContractWithHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let lambdaId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When + let txResponse = try await ContractCreateTransaction() + .bytecode(ContractHelpers.bytecode) + .gas(300_000) + .addHook(hookCreationDetails) + .execute(testEnv.client) + + // Then + let txReceipt = try await txResponse.getReceipt(testEnv.client) + XCTAssertNotNil(txReceipt.contractId) + } + + internal func test_CreateContractWithHookWithStorageUpdates() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let lambdaId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + var slot = LambdaStorageSlot() + slot.key = Data([0x01, 0x23, 0x45]) + slot.value = Data([0x67, 0x89, 0xAB]) + + var update = LambdaStorageUpdate() + update.storageSlot = slot + + lambdaEvmHook.addStorageUpdate(update) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When + let txResponse = try await ContractCreateTransaction() + .bytecode(ContractHelpers.bytecode) + .gas(300_000) + .addHook(hookCreationDetails) + .execute(testEnv.client) + + // Then + let txReceipt = try await txResponse.getReceipt(testEnv.client) + XCTAssertNotNil(txReceipt.contractId) + } + + internal func test_CannotCreateContractWithNoContractIdForHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaEvmHook = LambdaEvmHook() + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractCreateTransaction() + .bytecode(ContractHelpers.bytecode) + .gas(300_000) + .addHook(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") + return + } + XCTAssertEqual(status, .invalidHookCreationSpec) + } + } + + internal func test_CannotCreateContractWithDuplicateHookId() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let lambdaId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractCreateTransaction() + .bytecode(ContractHelpers.bytecode) + .gas(300_000) + .addHook(hookCreationDetails) + .addHook(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookIdRepeatedInCreationDetails) + } + } + + internal func test_CreateContractWithHookWithAdminKey() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let adminKey = PrivateKey.generateEcdsa() + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let lambdaId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook, + adminKey: .single(adminKey.publicKey) + ) + + // When + let txResponse = try await ContractCreateTransaction() + .bytecode(ContractHelpers.bytecode) + .gas(300_000) + .addHook(hookCreationDetails) + .execute(testEnv.client) + + // Then + let txReceipt = try await txResponse.getReceipt(testEnv.client) + XCTAssertNotNil(txReceipt.contractId) + } } diff --git a/Tests/HieroE2ETests/Contract/ContractUpdate.swift b/Tests/HieroE2ETests/Contract/ContractUpdate.swift index cf21597d..826be859 100644 --- a/Tests/HieroE2ETests/Contract/ContractUpdate.swift +++ b/Tests/HieroE2ETests/Contract/ContractUpdate.swift @@ -4,6 +4,7 @@ import Hiero import XCTest internal final class ContractUpdate: XCTestCase { + internal func testBasic() async throws { let testEnv = try TestEnvironment.nonFree @@ -65,4 +66,344 @@ internal final class ContractUpdate: XCTestCase { XCTAssertEqual(status, .modifyingImmutableContract) } } + + internal func test_CanAddHookToContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + do { + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CannotAddDuplicateHooksToContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .addHookToCreate(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookIdRepeatedInCreationDetails) + } + } + + internal func test_CannotAddHookToContractThatAlreadyExists() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(testEnv.operator.privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookIdInUse) + } + } + + internal func test_CanAddHookToContractWithStorageUpdates() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + var slot = LambdaStorageSlot() + slot.key = Data([0x01, 0x23, 0x45]) + slot.value = Data([0x67, 0x89, 0xAB]) + + var update = LambdaStorageUpdate() + update.storageSlot = slot + lambdaEvmHook.addStorageUpdate(update) + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + do { + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CanDeleteHookFromContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(testEnv.operator.privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // When / Then + do { + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToDelete(hookId) + .freezeWith(testEnv.client) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } catch { + XCTFail("Unexpected throw: \(error)") + } + } + + internal func test_CannotDeleteNonExistentHookFromContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + // When / Then + await assertThrowsHErrorAsync( + try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToDelete(999) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookNotFound) + } + } + + internal func test_CannotAddAndDeleteSameHookFromContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .addHookToDelete(hookId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookNotFound) + } + } + + internal func test_CannotDeleteAlreadyDeletedHookFromContract() async throws { + let testEnv = try TestEnvironment.nonFree + + // Given + let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) + + let contractId = try await ContractCreateTransaction() + .bytecodeFileId(bytecode.fileId) + .gas(300_000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(testEnv.operator.privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToDelete(hookId) + .freezeWith(testEnv.client) + .sign(testEnv.operator.privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // When / Then + await assertThrowsHErrorAsync( + try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToDelete(hookId) + .freezeWith(testEnv.client) + .sign(testEnv.operator.privateKey) + .execute(testEnv.client) + .getReceipt(testEnv.client) + ) { error in + guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + return + } + XCTAssertEqual(status, .hookNotFound) + } + } } diff --git a/Tests/HieroTests/EvmHookCallTests.swift b/Tests/HieroTests/EvmHookCallTests.swift index 6817d015..a5a541fb 100644 --- a/Tests/HieroTests/EvmHookCallTests.swift +++ b/Tests/HieroTests/EvmHookCallTests.swift @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation import HieroProtobufs +import XCTest + @testable import Hiero final class EvmHookCallUnitTests: XCTestCase { diff --git a/Tests/HieroTests/EvmHookSpecTests.swift b/Tests/HieroTests/EvmHookSpecTests.swift index eb47c3ca..6787b6ea 100644 --- a/Tests/HieroTests/EvmHookSpecTests.swift +++ b/Tests/HieroTests/EvmHookSpecTests.swift @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +import HieroProtobufs import XCTest + @testable import Hiero -import HieroProtobufs final class EvmHookSpecUnitTests: XCTestCase { @@ -41,8 +42,8 @@ final class EvmHookSpecUnitTests: XCTestCase { let protoSpec = evmHookSpec.toProtobuf() // Then - XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.shardNum), testContractId.shard) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.realmNum), testContractId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.shardNum), testContractId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.realmNum), testContractId.realm) XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.contractNum), testContractId.num) } } diff --git a/Tests/HieroTests/HookCallTests.swift b/Tests/HieroTests/HookCallTests.swift index 66565214..83df9af5 100644 --- a/Tests/HieroTests/HookCallTests.swift +++ b/Tests/HieroTests/HookCallTests.swift @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class HookCallUnitTests: XCTestCase { @@ -133,9 +134,12 @@ final class HookCallUnitTests: XCTestCase { // Then XCTAssertTrue(protoFull.fullHookID.hasEntityID) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.shardNum), testAccountId.shard) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.realmNum), testAccountId.realm) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.accountNum), testAccountId.num) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.accountNum), testAccountId.num) XCTAssertEqual(protoFull.fullHookID.hookID, testHookId) XCTAssertEqual(protoFull.evmHookCall.data, testCallData) diff --git a/Tests/HieroTests/HookCreationDetailsTests.swift b/Tests/HieroTests/HookCreationDetailsTests.swift index 904b233c..d9ea628c 100644 --- a/Tests/HieroTests/HookCreationDetailsTests.swift +++ b/Tests/HieroTests/HookCreationDetailsTests.swift @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class HookCreationDetailsUnitTests: XCTestCase { diff --git a/Tests/HieroTests/HookEntityIdTests.swift b/Tests/HieroTests/HookEntityIdTests.swift index f0785e08..e9437ab7 100644 --- a/Tests/HieroTests/HookEntityIdTests.swift +++ b/Tests/HieroTests/HookEntityIdTests.swift @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +import HieroProtobufs import XCTest + @testable import Hiero // TODO: Replace with your Swift target/module name -import HieroProtobufs final class HookEntityIdUnitTests: XCTestCase { diff --git a/Tests/HieroTests/HookIdTests.swift b/Tests/HieroTests/HookIdTests.swift index 0706533e..f4dbb746 100644 --- a/Tests/HieroTests/HookIdTests.swift +++ b/Tests/HieroTests/HookIdTests.swift @@ -1,8 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +import HieroProtobufs import XCTest + @testable import Hiero -import HieroProtobufs final class HookIdUnitTests: XCTestCase { @@ -59,8 +60,8 @@ final class HookIdUnitTests: XCTestCase { // Then XCTAssertTrue(proto.hasEntityID) - XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.shardNum), testAccountId.shard) - XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.realmNum), testAccountId.realm) XCTAssertEqual(UInt64(truncatingIfNeeded: proto.entityID.accountID.accountNum), testAccountId.num) } } diff --git a/Tests/HieroTests/LambdaEvmHookTests.swift b/Tests/HieroTests/LambdaEvmHookTests.swift index ef70c4f2..fa802713 100644 --- a/Tests/HieroTests/LambdaEvmHookTests.swift +++ b/Tests/HieroTests/LambdaEvmHookTests.swift @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class LambdaEvmHookUnitTests: XCTestCase { @@ -49,13 +50,13 @@ final class LambdaEvmHookUnitTests: XCTestCase { private func makeUpdates() -> [LambdaStorageUpdate] { // test bytes - let key1 = Data([0x01, 0x23, 0x45]) - let key2 = Data([0x67, 0x89, 0xAB]) - let preimage = Data([0xCD, 0xEF, 0x02]) + let key1 = Data([0x01, 0x23, 0x45]) + let key2 = Data([0x67, 0x89, 0xAB]) + let preimage = Data([0xCD, 0xEF, 0x02]) - let value1 = Data([0x04, 0x06, 0x08]) - let value2 = Data([0x0A, 0x0C, 0x0E]) - let value3 = Data([0x11, 0x13, 0x15]) + let value1 = Data([0x04, 0x06, 0x08]) + let value2 = Data([0x0A, 0x0C, 0x0E]) + let value3 = Data([0x11, 0x13, 0x15]) let mappingSlot = Data([0x17, 0x19, 0x1B]) diff --git a/Tests/HieroTests/LambdaMappingEntriesTests.swift b/Tests/HieroTests/LambdaMappingEntriesTests.swift index ebfc25cc..95152f02 100644 --- a/Tests/HieroTests/LambdaMappingEntriesTests.swift +++ b/Tests/HieroTests/LambdaMappingEntriesTests.swift @@ -1,17 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class LambdaMappingEntriesUnitTests: XCTestCase { // Fixture-equivalent constants private let mappingSlot = Data([0x17, 0x19, 0x1B]) - private let key1 = Data([0x01, 0x23, 0x45]) - private let key3 = Data([0x67, 0x89, 0xAB]) + private let key1 = Data([0x01, 0x23, 0x45]) + private let key3 = Data([0x67, 0x89, 0xAB]) private let preimage2 = Data([0xCD, 0xEF, 0x02]) private let value1 = Data([0x04, 0x06, 0x08]) diff --git a/Tests/HieroTests/LambdaMappingEntryTests.swift b/Tests/HieroTests/LambdaMappingEntryTests.swift index c410be72..c9a9b278 100644 --- a/Tests/HieroTests/LambdaMappingEntryTests.swift +++ b/Tests/HieroTests/LambdaMappingEntryTests.swift @@ -1,16 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class LambdaMappingEntryUnitTests: XCTestCase { // Fixture-equivalent constants - private let testKey = Data([0x01, 0x23, 0x45]) + private let testKey = Data([0x01, 0x23, 0x45]) private let testPreimage = Data([0x67, 0x89, 0xAB]) - private let testValue = Data([0xCD, 0xEF, 0x02]) + private let testValue = Data([0xCD, 0xEF, 0x02]) func test_GetSetKey() { // Given diff --git a/Tests/HieroTests/LambdaStorageSlotTests.swift b/Tests/HieroTests/LambdaStorageSlotTests.swift index 140cf7ae..97efcaac 100644 --- a/Tests/HieroTests/LambdaStorageSlotTests.swift +++ b/Tests/HieroTests/LambdaStorageSlotTests.swift @@ -1,14 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class LambdaStorageSlotUnitTests: XCTestCase { // Fixture-equivalent constants - private let testKey = Data([0x01, 0x23, 0x45]) + private let testKey = Data([0x01, 0x23, 0x45]) private let testValue = Data([0x67, 0x89, 0xAB]) func test_GetSetKey() { diff --git a/Tests/HieroTests/LambdaStorageUpdateTests.swift b/Tests/HieroTests/LambdaStorageUpdateTests.swift index 01a1ad8b..a2913c60 100644 --- a/Tests/HieroTests/LambdaStorageUpdateTests.swift +++ b/Tests/HieroTests/LambdaStorageUpdateTests.swift @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 -import XCTest import Foundation -@testable import Hiero import HieroProtobufs +import XCTest + +@testable import Hiero final class LambdaStorageUpdateUnitTests: XCTestCase { From 51093b14f13a0487fa1b75efe9d3262f73fb1d32 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Mon, 27 Oct 2025 19:01:39 -0400 Subject: [PATCH 4/7] feat: add tests and examples Signed-off-by: Rob Walworth --- Examples/AccountHooks/main.swift | 107 +++++ Examples/ContractHooks/main.swift | 120 +++++ Examples/LambdaSStore/main.swift | 160 +++++++ Examples/TransferWithHooks/main.swift | 270 ++++++++++++ Package.swift | 4 + Sources/Hiero/Hooks/EvmHookCall.swift | 6 + Sources/Hiero/Hooks/EvmHookSpec.swift | 7 +- Sources/Hiero/Hooks/HookCall.swift | 7 + Sources/Hiero/Hooks/HookEntityId.swift | 6 + Sources/Hiero/Hooks/HookId.swift | 6 + Sources/Hiero/Hooks/LambdaMappingEntry.swift | 2 + .../AbstractTokenTransferTransaction.swift | 259 ++++++++++- Sources/Hiero/TransferTransaction.swift | 56 +++ .../TransferTransactionHooks.swift | 415 ++++++++++++++++++ 14 files changed, 1419 insertions(+), 6 deletions(-) create mode 100644 Examples/AccountHooks/main.swift create mode 100644 Examples/ContractHooks/main.swift create mode 100644 Examples/LambdaSStore/main.swift create mode 100644 Examples/TransferWithHooks/main.swift create mode 100644 Tests/HieroE2ETests/TransferTransactionHooks.swift diff --git a/Examples/AccountHooks/main.swift b/Examples/AccountHooks/main.swift new file mode 100644 index 00000000..63457787 --- /dev/null +++ b/Examples/AccountHooks/main.swift @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Hiero +import HieroProtobufs + +@main +struct AccountHooksExample { + static func main() async throws { + // Initialize the client + let client = Client.forTestnet() + + // Create operator account + let operatorKey = PrivateKey.generateEd25519() + let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID + + client.setOperator(operatorId, operatorKey) + + print("🚀 Account Hooks Example") + print("=======================") + + // Create a contract that will serve as our hook + print("\n📝 Creating hook contract...") + + let contractKey = PrivateKey.generateEd25519() + let contractResponse = try await ContractCreateTransaction() + .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .adminKey(.single(contractKey.publicKey)) + .gas(100000) + .execute(client) + let contractReceipt = try await contractResponse.getReceipt(client) + let contractId = contractReceipt.contractId! + + print("✅ Created hook contract: \(contractId)") + + // Create Lambda EVM Hook specification + print("\n🔧 Creating Lambda EVM Hook specification...") + + var evmHookSpec = EvmHookSpec() + evmHookSpec = evmHookSpec.contractId(contractId) + + let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) + + // Create hook creation details + var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) + hookCreationDetails = hookCreationDetails.adminKey(Key.single(contractKey.publicKey)) + + // Example 1: Create account with hooks + print("\n👤 Example 1: Creating account with hooks") + + let accountKey = PrivateKey.generateEd25519() + let accountResponse = try await AccountCreateTransaction() + .keyWithoutAlias(.single(accountKey.publicKey)) + .initialBalance(10) + .addHook(hookCreationDetails) + .execute(client) + let accountReceipt = try await accountResponse.getReceipt(client) + let accountId = accountReceipt.accountId! + + print("✅ Created account with hooks: \(accountId)") + + // Example 2: Update account to add more hooks + print("\n🔄 Example 2: Updating account to add more hooks") + + var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) + hookCreationDetails2 = hookCreationDetails2.adminKey(Key.single(contractKey.publicKey)) + + let updateResponse = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToCreate(hookCreationDetails2) + .execute(client) + let updateReceipt = try await updateResponse.getReceipt(client) + + print("✅ Updated account with additional hooks: \(updateReceipt.status)") + + // Example 3: Update account to delete hooks + print("\n🗑️ Example 3: Updating account to delete hooks") + + let deleteResponse = try await AccountUpdateTransaction() + .accountId(accountId) + .addHookToDelete(1) // Delete hook with ID 1 + .execute(client) + let deleteReceipt = try await deleteResponse.getReceipt(client) + + print("✅ Updated account to delete hooks: \(deleteReceipt.status)") + + // Cleanup + print("\n🧹 Cleaning up...") + + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + _ = try await ContractDeleteTransaction() + .contractId(contractId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + print("✅ Cleanup completed") + print("\n🎉 Account Hooks Example completed successfully!") + } +} diff --git a/Examples/ContractHooks/main.swift b/Examples/ContractHooks/main.swift new file mode 100644 index 00000000..75946b3d --- /dev/null +++ b/Examples/ContractHooks/main.swift @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Hiero +import HieroProtobufs + +@main +struct ContractHooksExample { + static func main() async throws { + // Initialize the client + let client = Client.forTestnet() + + // Create operator account + let operatorKey = PrivateKey.generateEd25519() + let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID + + client.setOperator(operatorId, operatorKey) + + print("🚀 Contract Hooks Example") + print("========================") + + // Create a contract that will serve as our hook + print("\n📝 Creating hook contract...") + + let hookContractKey = PrivateKey.generateEd25519() + let hookContractResponse = try await ContractCreateTransaction() + .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .adminKey(.single(hookContractKey.publicKey)) + .gas(100000) + .execute(client) + let hookContractReceipt = try await hookContractResponse.getReceipt(client) + let hookContractId = hookContractReceipt.contractId! + + print("✅ Created hook contract: \(hookContractId)") + + // Create Lambda EVM Hook specification + print("\n🔧 Creating Lambda EVM Hook specification...") + + var evmHookSpec = EvmHookSpec() + evmHookSpec = evmHookSpec.contractId(hookContractId) + + let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) + + // Create hook creation details + var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) + hookCreationDetails = hookCreationDetails.adminKey(Key.single(hookContractKey.publicKey)) + + // Example 1: Create contract with hooks + print("\n📄 Example 1: Creating contract with hooks") + + let contractKey = PrivateKey.generateEd25519() + let contractResponse = try await ContractCreateTransaction() + .bytecodeFileId(FileId.fromString("0.0.1236")) // Replace with your contract file ID + .adminKey(.single(contractKey.publicKey)) + .gas(200000) + .addHook(hookCreationDetails) + .execute(client) + let contractReceipt = try await contractResponse.getReceipt(client) + let contractId = contractReceipt.contractId! + + print("✅ Created contract with hooks: \(contractId)") + + // Example 2: Update contract to add more hooks + print("\n🔄 Example 2: Updating contract to add more hooks") + + var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) + hookCreationDetails2 = hookCreationDetails2.adminKey(Key.single(hookContractKey.publicKey)) + + let updateResponse = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails2) + .execute(client) + let updateReceipt = try await updateResponse.getReceipt(client) + + print("✅ Updated contract with additional hooks: \(updateReceipt.status)") + + // Example 3: Update contract to delete hooks + print("\n🗑️ Example 3: Updating contract to delete hooks") + + let deleteResponse = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToDelete(1) // Delete hook with ID 1 + .execute(client) + let deleteReceipt = try await deleteResponse.getReceipt(client) + + print("✅ Updated contract to delete hooks: \(deleteReceipt.status)") + + // Example 4: Execute contract with hooks + print("\n⚡ Example 4: Executing contract with hooks") + + let executeResponse = try await ContractExecuteTransaction() + .contractId(contractId) + .gas(100000) + .function("someFunction") + .execute(client) + let executeReceipt = try await executeResponse.getReceipt(client) + + print("✅ Executed contract with hooks: \(executeReceipt.status)") + + // Cleanup + print("\n🧹 Cleaning up...") + + _ = try await ContractDeleteTransaction() + .contractId(contractId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + _ = try await ContractDeleteTransaction() + .contractId(hookContractId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + print("✅ Cleanup completed") + print("\n🎉 Contract Hooks Example completed successfully!") + } +} diff --git a/Examples/LambdaSStore/main.swift b/Examples/LambdaSStore/main.swift new file mode 100644 index 00000000..affbdecf --- /dev/null +++ b/Examples/LambdaSStore/main.swift @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Hiero +import HieroProtobufs + +@main +struct LambdaSStoreExample { + static func main() async throws { + // Initialize the client + let client = Client.forTestnet() + + // Create operator account + let operatorKey = PrivateKey.generateEd25519() + let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID + + client.setOperator(operatorId, operatorKey) + + print("🚀 Lambda SSTORE Example") + print("========================") + + // Create a contract that will serve as our hook + print("\n📝 Creating hook contract...") + + let contractKey = PrivateKey.generateEd25519() + let contractResponse = try await ContractCreateTransaction() + .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .adminKey(.single(contractKey.publicKey)) + .gas(100000) + .execute(client) + let contractReceipt = try await contractResponse.getReceipt(client) + let contractId = contractReceipt.contractId! + + print("✅ Created hook contract: \(contractId)") + + // Create account with hooks + print("\n👤 Creating account with hooks...") + + var evmHookSpec = EvmHookSpec() + evmHookSpec = evmHookSpec.contractId(contractId) + + let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) + + var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) + hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) + hookCreationDetails = hookCreationDetails.adminKey(Key.single(contractKey.publicKey)) + + let accountKey = PrivateKey.generateEd25519() + let accountResponse = try await AccountCreateTransaction() + .keyWithoutAlias(.single(accountKey.publicKey)) + .initialBalance(10) + .addHook(hookCreationDetails) + .execute(client) + let accountReceipt = try await accountResponse.getReceipt(client) + let accountId = accountReceipt.accountId! + + print("✅ Created account with hooks: \(accountId)") + + // Create HookId for the Lambda SSTORE transaction + print("\n🔧 Creating HookId...") + + var hookEntityId = HookEntityId() + hookEntityId = hookEntityId.accountId(accountId) + + let hookId = HookId(entityId: hookEntityId, hookId: 1) + + print("✅ Created HookId: \(hookId)") + + // Example 1: Update storage slot + print("\n💾 Example 1: Updating storage slot") + + var storageSlot = LambdaStorageSlot() + storageSlot = storageSlot.key(Data([0x01, 0x02, 0x03, 0x04])) // 32-byte key + storageSlot = storageSlot.value(Data([0x05, 0x06, 0x07, 0x08])) // 32-byte value + + var storageUpdate = LambdaStorageUpdate() + storageUpdate = storageUpdate.setStorageSlot(storageSlot) + + let sstoreResponse1 = try await LambdaSStoreTransaction() + .hookId(hookId) + .addStorageUpdate(storageUpdate) + .execute(client) + let sstoreReceipt1 = try await sstoreResponse1.getReceipt(client) + + print("✅ Updated storage slot: \(sstoreReceipt1.status)") + + // Example 2: Update mapping entries + print("\n🗺️ Example 2: Updating mapping entries") + + var mappingEntry1 = LambdaMappingEntry() + mappingEntry1 = mappingEntry1.key(Data([0x09, 0x0A, 0x0B, 0x0C])) // 32-byte key + mappingEntry1 = mappingEntry1.value(Data([0x0D, 0x0E, 0x0F, 0x10])) // 32-byte value + + var mappingEntry2 = LambdaMappingEntry() + mappingEntry2 = mappingEntry2.preimage(Data([0x11, 0x12, 0x13, 0x14])) // Preimage + mappingEntry2 = mappingEntry2.value(Data([0x15, 0x16, 0x17, 0x18])) // 32-byte value + + var mappingEntries = LambdaMappingEntries() + mappingEntries = mappingEntries.mappingSlot(Data([0x19, 0x1A, 0x1B, 0x1C])) // Mapping slot + mappingEntries = mappingEntries.setEntries([mappingEntry1, mappingEntry2]) + + var mappingUpdate = LambdaStorageUpdate() + mappingUpdate = mappingUpdate.setMappingEntries(mappingEntries) + + let sstoreResponse2 = try await LambdaSStoreTransaction() + .hookId(hookId) + .addStorageUpdate(mappingUpdate) + .execute(client) + let sstoreReceipt2 = try await sstoreResponse2.getReceipt(client) + + print("✅ Updated mapping entries: \(sstoreReceipt2.status)") + + // Example 3: Multiple storage updates + print("\n📦 Example 3: Multiple storage updates") + + var storageSlot2 = LambdaStorageSlot() + storageSlot2 = storageSlot2.key(Data([0x1D, 0x1E, 0x1F, 0x20])) + storageSlot2 = storageSlot2.value(Data([0x21, 0x22, 0x23, 0x24])) + + var storageUpdate2 = LambdaStorageUpdate() + storageUpdate2 = storageUpdate2.setStorageSlot(storageSlot2) + + let sstoreResponse3 = try await LambdaSStoreTransaction() + .hookId(hookId) + .storageUpdates([storageUpdate, storageUpdate2]) // Multiple updates + .execute(client) + let sstoreReceipt3 = try await sstoreResponse3.getReceipt(client) + + print("✅ Applied multiple storage updates: \(sstoreReceipt3.status)") + + // Example 4: Clear storage updates + print("\n🧹 Example 4: Clearing storage updates") + + let sstoreResponse4 = try await LambdaSStoreTransaction() + .hookId(hookId) + .clearStorageUpdates() // Clear all updates + .execute(client) + let sstoreReceipt4 = try await sstoreResponse4.getReceipt(client) + + print("✅ Cleared storage updates: \(sstoreReceipt4.status)") + + // Cleanup + print("\n🧹 Cleaning up...") + + _ = try await AccountDeleteTransaction() + .accountId(accountId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + _ = try await ContractDeleteTransaction() + .contractId(contractId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + print("✅ Cleanup completed") + print("\n🎉 Lambda SSTORE Example completed successfully!") + } +} diff --git a/Examples/TransferWithHooks/main.swift b/Examples/TransferWithHooks/main.swift new file mode 100644 index 00000000..9103abf7 --- /dev/null +++ b/Examples/TransferWithHooks/main.swift @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Hiero + +/// Example demonstrating how to use hooks with transfer transactions. +/// +/// This example shows how to: +/// 1. Create accounts +/// 2. Create a hook call +/// 3. Execute transfers with hooks +/// 4. Handle different types of hook calls +@main +struct TransferWithHooksExample { + static func main() async throws { + // Initialize the client + let client = Client.forTestnet() + + // Create operator account + let operatorKey = PrivateKey.generateEd25519() + let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID + + client.setOperator(operatorId, operatorKey) + + print("🚀 Transfer with Hooks Example") + print("==============================") + + // Create test accounts + print("\n📝 Creating test accounts...") + + let senderKey = PrivateKey.generateEd25519() + let senderResponse = try await AccountCreateTransaction() + .keyWithoutAlias(.single(senderKey.publicKey)) + .initialBalance(10) + .execute(client) + let senderReceipt = try await senderResponse.getReceipt(client) + let senderAccountId = senderReceipt.accountId! + + let receiverKey = PrivateKey.generateEd25519() + let receiverResponse = try await AccountCreateTransaction() + .keyWithoutAlias(.single(receiverKey.publicKey)) + .initialBalance(5) + .execute(client) + let receiverReceipt = try await receiverResponse.getReceipt(client) + let receiverAccountId = receiverReceipt.accountId! + + print("✅ Created sender account: \(senderAccountId)") + print("✅ Created receiver account: \(receiverAccountId)") + + // Example 1: HBAR transfer with pre-transaction hook + print("\n💰 Example 1: HBAR transfer with pre-transaction hook") + + var preTxHook = HookCall() + preTxHook = preTxHook.hookId(1) + var evmHookCall1 = EvmHookCall() + evmHookCall1 = evmHookCall1.data(Data([0x01, 0x02, 0x03])) // Hook call data + evmHookCall1 = evmHookCall1.gasLimit(100000) // Gas limit for hook execution + preTxHook = preTxHook.evmHookCall(evmHookCall1) + + let hbarTransferTx = TransferTransaction() + .hbarTransferWithPreTxHook(senderAccountId, 2, preTxHook) + + let hbarResponse = try await hbarTransferTx.execute(client) + let hbarReceipt = try await hbarResponse.getReceipt(client) + + print("✅ HBAR transfer with hook completed: \(hbarReceipt.status)") + + // Example 2: HBAR transfer with pre-post-transaction hook + print("\n💰 Example 2: HBAR transfer with pre-post-transaction hook") + + var prePostTxHook = HookCall() + prePostTxHook = prePostTxHook.hookId(2) + var evmHookCall2 = EvmHookCall() + evmHookCall2 = evmHookCall2.data(Data([0x04, 0x05, 0x06])) + evmHookCall2 = evmHookCall2.gasLimit(150000) + prePostTxHook = prePostTxHook.evmHookCall(evmHookCall2) + + let hbarPrePostTx = TransferTransaction() + .hbarTransferWithPrePostTxHook(senderAccountId, 1, prePostTxHook) + + let hbarPrePostResponse = try await hbarPrePostTx.execute(client) + let hbarPrePostReceipt = try await hbarPrePostResponse.getReceipt(client) + + print("✅ HBAR transfer with pre-post hook completed: \(hbarPrePostReceipt.status)") + + // Example 3: Create a token and transfer with hooks + print("\n🪙 Example 3: Token transfer with hooks") + + let tokenId = try await TokenCreateTransaction() + .name("Hook Test Token") + .symbol("HTT") + .decimals(2) + .initialSupply(1000) + .treasuryAccountId(operatorId) + .adminKey(.single(operatorKey.publicKey)) + .execute(client) + .getReceipt(client) + .tokenId! + + print("✅ Created token: \(tokenId)") + + // Associate accounts with the token + _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) + .execute(client) + .getReceipt(client) + + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) + .execute(client) + .getReceipt(client) + + print("✅ Associated accounts with token") + + // Transfer tokens with hook + var tokenHook = HookCall() + tokenHook = tokenHook.hookId(3) + var evmHookCall3 = EvmHookCall() + evmHookCall3 = evmHookCall3.data(Data([0x07, 0x08, 0x09])) + evmHookCall3 = evmHookCall3.gasLimit(200000) + tokenHook = tokenHook.evmHookCall(evmHookCall3) + + let tokenTransferTx = TransferTransaction() + .tokenTransferWithPreTxHook(tokenId, senderAccountId, 100, tokenHook) + + let tokenResponse = try await tokenTransferTx.execute(client) + let tokenReceipt = try await tokenResponse.getReceipt(client) + + print("✅ Token transfer with hook completed: \(tokenReceipt.status)") + + // Example 4: NFT transfer with hooks + print("\n🎨 Example 4: NFT transfer with hooks") + + let nftTokenId = try await TokenCreateTransaction() + .name("Hook Test NFT") + .symbol("HTNFT") + .tokenType(.nonFungibleUnique) + .treasuryAccountId(operatorId) + .adminKey(.single(operatorKey.publicKey)) + .supplyKey(.single(operatorKey.publicKey)) + .execute(client) + .getReceipt(client) + .tokenId! + + print("✅ Created NFT token: \(nftTokenId)") + + // Associate accounts with the NFT token + _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [nftTokenId]) + .execute(client) + .getReceipt(client) + + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [nftTokenId]) + .execute(client) + .getReceipt(client) + + // Mint an NFT + let nftSerial = try await TokenMintTransaction(tokenId: nftTokenId) + .metadata([Data("Hook Test NFT Metadata".utf8)]) + .execute(client) + .getReceipt(client) + .serials?.first! + + print("✅ Minted NFT with serial: \(nftSerial!)") + + // Transfer NFT to sender first + _ = try await TransferTransaction() + .nftTransfer(NftId(tokenId: nftTokenId, serial: nftSerial!), operatorId, senderAccountId) + .execute(client) + .getReceipt(client) + + // Transfer NFT with sender hook + var senderHook = HookCall() + senderHook = senderHook.hookId(4) + var evmHookCall4 = EvmHookCall() + evmHookCall4 = evmHookCall4.data(Data([0x0A, 0x0B, 0x0C])) + evmHookCall4 = evmHookCall4.gasLimit(250000) + senderHook = senderHook.evmHookCall(evmHookCall4) + + let nftTransferTx = TransferTransaction() + .nftTransferWithSenderHooks( + NftId(tokenId: nftTokenId, serial: nftSerial!), + senderAccountId, + receiverAccountId, + preTxSenderHook: senderHook + ) + + let nftResponse = try await nftTransferTx.execute(client) + let nftReceipt = try await nftResponse.getReceipt(client) + + print("✅ NFT transfer with sender hook completed: \(nftReceipt.status)") + + // Example 5: NFT transfer with receiver hook + print("\n🎨 Example 5: NFT transfer with receiver hook") + + var receiverHook = HookCall() + receiverHook = receiverHook.hookId(5) + var evmHookCall5 = EvmHookCall() + evmHookCall5 = evmHookCall5.data(Data([0x0D, 0x0E, 0x0F])) + evmHookCall5 = evmHookCall5.gasLimit(300000) + receiverHook = receiverHook.evmHookCall(evmHookCall5) + + let nftReceiverTx = TransferTransaction() + .nftTransferWithReceiverHooks( + NftId(tokenId: nftTokenId, serial: nftSerial!), + receiverAccountId, + senderAccountId, + preTxReceiverHook: receiverHook + ) + + let nftReceiverResponse = try await nftReceiverTx.execute(client) + let nftReceiverReceipt = try await nftReceiverResponse.getReceipt(client) + + print("✅ NFT transfer with receiver hook completed: \(nftReceiverReceipt.status)") + + // Example 6: NFT transfer with all hooks + print("\n🎨 Example 6: NFT transfer with all hooks") + + var allSenderHook = HookCall() + allSenderHook = allSenderHook.hookId(6) + var evmHookCall6 = EvmHookCall() + evmHookCall6 = evmHookCall6.data(Data([0x10, 0x11, 0x12])) + evmHookCall6 = evmHookCall6.gasLimit(350000) + allSenderHook = allSenderHook.evmHookCall(evmHookCall6) + + var allReceiverHook = HookCall() + allReceiverHook = allReceiverHook.hookId(7) + var evmHookCall7 = EvmHookCall() + evmHookCall7 = evmHookCall7.data(Data([0x13, 0x14, 0x15])) + evmHookCall7 = evmHookCall7.gasLimit(400000) + allReceiverHook = allReceiverHook.evmHookCall(evmHookCall7) + + let nftAllHooksTx = TransferTransaction() + .nftTransferWithAllHooks( + NftId(tokenId: nftTokenId, serial: nftSerial!), + senderAccountId, + receiverAccountId, + preTxSenderHook: allSenderHook, + preTxReceiverHook: allReceiverHook + ) + + let nftAllHooksResponse = try await nftAllHooksTx.execute(client) + let nftAllHooksReceipt = try await nftAllHooksResponse.getReceipt(client) + + print("✅ NFT transfer with all hooks completed: \(nftAllHooksReceipt.status)") + + // Cleanup + print("\n🧹 Cleaning up...") + + _ = try await AccountDeleteTransaction() + .accountId(senderAccountId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + _ = try await AccountDeleteTransaction() + .accountId(receiverAccountId) + .transferAccountId(operatorId) + .execute(client) + .getReceipt(client) + + _ = try await TokenDeleteTransaction(tokenId: tokenId) + .execute(client) + .getReceipt(client) + + _ = try await TokenDeleteTransaction(tokenId: nftTokenId) + .execute(client) + .getReceipt(client) + + print("✅ Cleanup completed") + print("\n🎉 Transfer with Hooks Example completed successfully!") + } +} diff --git a/Package.swift b/Package.swift index f3c9b322..abd29b9e 100644 --- a/Package.swift +++ b/Package.swift @@ -25,11 +25,13 @@ import PackageDescription let exampleTargets = [ "AccountAlias", "AccountAllowance", + "AccountHooks", "AddNftAllowance", "BatchTransaction", "ConsensusPubSub", "ConsensusPubSubChunked", "ConsensusPubSubWithSubmitKey", + "ContractHooks", "CreateAccount", "CreateAccountThresholdKey", "CreateAccountWithAlias", @@ -50,6 +52,7 @@ let exampleTargets = [ "GetExchangeRates", "GetFileContents", "InitializeClientWithMirrorNetwork", + "LambdaSStore", "LongTermScheduledTransaction", "MirrorNodeContractQuery", "ModifyTokenKeys", @@ -66,6 +69,7 @@ let exampleTargets = [ "TopicWithAdminKey", "TransferCrypto", "TransferTokens", + "TransferWithHooks", "UpdateAccountPublicKey", "ValidateChecksum", "TokenUpdateMetadata", diff --git a/Sources/Hiero/Hooks/EvmHookCall.swift b/Sources/Hiero/Hooks/EvmHookCall.swift index d14170bb..1f7d7701 100644 --- a/Sources/Hiero/Hooks/EvmHookCall.swift +++ b/Sources/Hiero/Hooks/EvmHookCall.swift @@ -48,3 +48,9 @@ extension EvmHookCall: TryProtobufCodable { } } } + +extension EvmHookCall: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + // EvmHookCall only contains Data and UInt64, no checksums to validate + } +} diff --git a/Sources/Hiero/Hooks/EvmHookSpec.swift b/Sources/Hiero/Hooks/EvmHookSpec.swift index a253d740..bd36ff0c 100644 --- a/Sources/Hiero/Hooks/EvmHookSpec.swift +++ b/Sources/Hiero/Hooks/EvmHookSpec.swift @@ -25,7 +25,12 @@ extension EvmHookSpec: TryProtobufCodable { /// Construct from protobuf. internal init(protobuf proto: Protobuf) throws { - self.contractId = try ContractId.fromProtobuf(proto.contractID) + // Handle the case where contractID might not be set + if case .contractID(let contractID)? = proto.bytecodeSource { + self.contractId = try ContractId.fromProtobuf(contractID) + } else { + self.contractId = nil + } } /// Convert to protobuf. diff --git a/Sources/Hiero/Hooks/HookCall.swift b/Sources/Hiero/Hooks/HookCall.swift index 474dfcc1..76e79014 100644 --- a/Sources/Hiero/Hooks/HookCall.swift +++ b/Sources/Hiero/Hooks/HookCall.swift @@ -91,3 +91,10 @@ extension HookCall: TryProtobufCodable { return proto } } + +extension HookCall: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + try fullHookId?.validateChecksums(on: ledgerId) + try evmHookCall?.validateChecksums(on: ledgerId) + } +} diff --git a/Sources/Hiero/Hooks/HookEntityId.swift b/Sources/Hiero/Hooks/HookEntityId.swift index a3b945c2..23b173df 100644 --- a/Sources/Hiero/Hooks/HookEntityId.swift +++ b/Sources/Hiero/Hooks/HookEntityId.swift @@ -41,3 +41,9 @@ extension HookEntityId: TryProtobufCodable { } } } + +extension HookEntityId: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + try accountId?.validateChecksums(on: ledgerId) + } +} diff --git a/Sources/Hiero/Hooks/HookId.swift b/Sources/Hiero/Hooks/HookId.swift index 6b2f0245..3cb961ae 100644 --- a/Sources/Hiero/Hooks/HookId.swift +++ b/Sources/Hiero/Hooks/HookId.swift @@ -48,3 +48,9 @@ extension HookId: TryProtobufCodable { } } } + +extension HookId: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + try entityId.validateChecksums(on: ledgerId) + } +} diff --git a/Sources/Hiero/Hooks/LambdaMappingEntry.swift b/Sources/Hiero/Hooks/LambdaMappingEntry.swift index 78018595..99840245 100644 --- a/Sources/Hiero/Hooks/LambdaMappingEntry.swift +++ b/Sources/Hiero/Hooks/LambdaMappingEntry.swift @@ -24,6 +24,7 @@ public struct LambdaMappingEntry { @discardableResult public mutating func key(_ key: Data) -> Self { self.key = key + self.preimage = nil // Reset preimage when setting key return self } @@ -31,6 +32,7 @@ public struct LambdaMappingEntry { @discardableResult public mutating func preimage(_ preimage: Data) -> Self { self.preimage = preimage + self.key = nil // Reset key when setting preimage return self } diff --git a/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift b/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift index 8ea0df71..faebe254 100644 --- a/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift +++ b/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift @@ -12,9 +12,13 @@ public class AbstractTokenTransferTransaction: Transaction { let accountId: AccountId var amount: Int64 let isApproval: Bool + var preTxAllowanceHook: HookCall? + var prePostTxAllowanceHook: HookCall? internal func validateChecksums(on ledgerId: LedgerId) throws { try accountId.validateChecksums(on: ledgerId) + try preTxAllowanceHook?.validateChecksums(on: ledgerId) + try prePostTxAllowanceHook?.validateChecksums(on: ledgerId) } } @@ -36,10 +40,18 @@ public class AbstractTokenTransferTransaction: Transaction { let receiverAccountId: AccountId let serial: UInt64 let isApproval: Bool + var preTxSenderAllowanceHook: HookCall? + var prePostTxSenderAllowanceHook: HookCall? + var preTxReceiverAllowanceHook: HookCall? + var prePostTxReceiverAllowanceHook: HookCall? internal func validateChecksums(on ledgerId: LedgerId) throws { try senderAccountId.validateChecksums(on: ledgerId) try receiverAccountId.validateChecksums(on: ledgerId) + try preTxSenderAllowanceHook?.validateChecksums(on: ledgerId) + try prePostTxSenderAllowanceHook?.validateChecksums(on: ledgerId) + try preTxReceiverAllowanceHook?.validateChecksums(on: ledgerId) + try prePostTxReceiverAllowanceHook?.validateChecksums(on: ledgerId) } } @@ -169,7 +181,7 @@ public class AbstractTokenTransferTransaction: Transaction { _ amount: Int64, _ approved: Bool ) -> Self { - let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved) + let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: nil, prePostTxAllowanceHook: nil) if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId }) { @@ -203,7 +215,7 @@ public class AbstractTokenTransferTransaction: Transaction { _ approved: Bool, _ expectedDecimals: UInt32 ) -> Self { - let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved) + let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: nil, prePostTxAllowanceHook: nil) if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId }) { @@ -239,6 +251,67 @@ public class AbstractTokenTransferTransaction: Transaction { return self } + /// Add a non-approved token transfer with a pre-transaction allowance hook to the transaction. + @discardableResult + public func tokenTransferWithPreTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { + doTokenTransferWithHook(tokenId, accountId, amount, false, hookCall, nil) + } + + /// Add a non-approved token transfer with a pre-post-transaction allowance hook to the transaction. + @discardableResult + public func tokenTransferWithPrePostTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { + doTokenTransferWithHook(tokenId, accountId, amount, false, nil, hookCall) + } + + /// Add an approved token transfer with a pre-transaction allowance hook to the transaction. + @discardableResult + public func approvedTokenTransferWithPreTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { + doTokenTransferWithHook(tokenId, accountId, amount, true, hookCall, nil) + } + + /// Add an approved token transfer with a pre-post-transaction allowance hook to the transaction. + @discardableResult + public func approvedTokenTransferWithPrePostTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { + doTokenTransferWithHook(tokenId, accountId, amount, true, nil, hookCall) + } + + private func doTokenTransferWithHook( + _ tokenId: TokenId, + _ accountId: AccountId, + _ amount: Int64, + _ approved: Bool, + _ preTxHook: HookCall?, + _ prePostTxHook: HookCall? + ) -> Self { + let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: preTxHook, prePostTxAllowanceHook: prePostTxHook) + + if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId + }) { + let existingTransfers = tokenTransfersInner[firstIndex].transfers + + if let existingTransferIndex = existingTransfers.firstIndex(where: { + $0.accountId == accountId && $0.isApproval == approved + }) { + tokenTransfersInner[firstIndex].transfers[existingTransferIndex].amount += amount + tokenTransfersInner[firstIndex].transfers[existingTransferIndex].preTxAllowanceHook = preTxHook + tokenTransfersInner[firstIndex].transfers[existingTransferIndex].prePostTxAllowanceHook = prePostTxHook + } else { + tokenTransfersInner[firstIndex].expectedDecimals = nil + tokenTransfersInner[firstIndex].transfers.append(transfer) + } + } else { + tokenTransfersInner.append( + TokenTransfer( + tokenId: tokenId, + transfers: [transfer], + nftTransfers: [], + expectedDecimals: nil + )) + } + + return self + } + private func doNftTransfer( _ nftId: NftId, _ senderAccountId: AccountId, @@ -249,7 +322,126 @@ public class AbstractTokenTransferTransaction: Transaction { senderAccountId: senderAccountId, receiverAccountId: receiverAccountId, serial: nftId.serial, - isApproval: approved + isApproval: approved, + preTxSenderAllowanceHook: nil, + prePostTxSenderAllowanceHook: nil, + preTxReceiverAllowanceHook: nil, + prePostTxReceiverAllowanceHook: nil + ) + + if let index = tokenTransfersInner.firstIndex(where: { transfer in transfer.tokenId == nftId.tokenId }) { + var tmp = tokenTransfersInner[index] + tmp.nftTransfers.append(transfer) + tokenTransfersInner[index] = tmp + } else { + tokenTransfersInner.append( + TokenTransfer( + tokenId: nftId.tokenId, + transfers: [], + nftTransfers: [transfer], + expectedDecimals: nil + ) + ) + } + + return self + } + + /// Add a non-approved NFT transfer with sender allowance hooks to the transaction. + @discardableResult + public func nftTransferWithSenderHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxSenderHook: HookCall? = nil, + prePostTxSenderHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, preTxSenderHook, prePostTxSenderHook, nil, nil) + } + + /// Add a non-approved NFT transfer with receiver allowance hooks to the transaction. + @discardableResult + public func nftTransferWithReceiverHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxReceiverHook: HookCall? = nil, + prePostTxReceiverHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, nil, nil, preTxReceiverHook, prePostTxReceiverHook) + } + + /// Add a non-approved NFT transfer with both sender and receiver allowance hooks to the transaction. + @discardableResult + public func nftTransferWithAllHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxSenderHook: HookCall? = nil, + prePostTxSenderHook: HookCall? = nil, + preTxReceiverHook: HookCall? = nil, + prePostTxReceiverHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, preTxSenderHook, prePostTxSenderHook, preTxReceiverHook, prePostTxReceiverHook) + } + + /// Add an approved NFT transfer with sender allowance hooks to the transaction. + @discardableResult + public func approvedNftTransferWithSenderHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxSenderHook: HookCall? = nil, + prePostTxSenderHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, preTxSenderHook, prePostTxSenderHook, nil, nil) + } + + /// Add an approved NFT transfer with receiver allowance hooks to the transaction. + @discardableResult + public func approvedNftTransferWithReceiverHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxReceiverHook: HookCall? = nil, + prePostTxReceiverHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, nil, nil, preTxReceiverHook, prePostTxReceiverHook) + } + + /// Add an approved NFT transfer with both sender and receiver allowance hooks to the transaction. + @discardableResult + public func approvedNftTransferWithAllHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + preTxSenderHook: HookCall? = nil, + prePostTxSenderHook: HookCall? = nil, + preTxReceiverHook: HookCall? = nil, + prePostTxReceiverHook: HookCall? = nil + ) -> Self { + doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, preTxSenderHook, prePostTxSenderHook, preTxReceiverHook, prePostTxReceiverHook) + } + + private func doNftTransferWithHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + _ approved: Bool, + _ preTxSenderHook: HookCall?, + _ prePostTxSenderHook: HookCall?, + _ preTxReceiverHook: HookCall?, + _ prePostTxReceiverHook: HookCall? + ) -> Self { + let transfer = NftTransfer( + senderAccountId: senderAccountId, + receiverAccountId: receiverAccountId, + serial: nftId.serial, + isApproval: approved, + preTxSenderAllowanceHook: preTxSenderHook, + prePostTxSenderAllowanceHook: prePostTxSenderHook, + preTxReceiverAllowanceHook: preTxReceiverHook, + prePostTxReceiverAllowanceHook: prePostTxReceiverHook ) if let index = tokenTransfersInner.firstIndex(where: { transfer in transfer.tokenId == nftId.tokenId }) { @@ -319,8 +511,20 @@ extension AbstractTokenTransferTransaction.Transfer: TryProtobufCodable { self.init( accountId: try .fromProtobuf(proto.accountID), amount: proto.amount, - isApproval: proto.isApproval + isApproval: proto.isApproval, + preTxAllowanceHook: nil, + prePostTxAllowanceHook: nil ) + + // Handle hook calls + switch proto.hookCall { + case .preTxAllowanceHook(let hookCall): + self.preTxAllowanceHook = try HookCall(protobuf: hookCall) + case .prePostTxAllowanceHook(let hookCall): + self.prePostTxAllowanceHook = try HookCall(protobuf: hookCall) + case nil: + break + } } internal func toProtobuf() -> Protobuf { @@ -328,6 +532,13 @@ extension AbstractTokenTransferTransaction.Transfer: TryProtobufCodable { proto.accountID = accountId.toProtobuf() proto.amount = amount proto.isApproval = isApproval + + // Handle hook calls + if let preTxHook = preTxAllowanceHook { + proto.hookCall = .preTxAllowanceHook(preTxHook.toProtobuf()) + } else if let prePostTxHook = prePostTxAllowanceHook { + proto.hookCall = .prePostTxAllowanceHook(prePostTxHook.toProtobuf()) + } } } } @@ -366,8 +577,32 @@ extension AbstractTokenTransferTransaction.NftTransfer: TryProtobufCodable { senderAccountId: try .fromProtobuf(proto.senderAccountID), receiverAccountId: try .fromProtobuf(proto.receiverAccountID), serial: UInt64(proto.serialNumber), - isApproval: proto.isApproval + isApproval: proto.isApproval, + preTxSenderAllowanceHook: nil, + prePostTxSenderAllowanceHook: nil, + preTxReceiverAllowanceHook: nil, + prePostTxReceiverAllowanceHook: nil ) + + // Handle sender allowance hook calls + switch proto.senderAllowanceHookCall { + case .preTxSenderAllowanceHook(let hookCall): + self.preTxSenderAllowanceHook = try HookCall(protobuf: hookCall) + case .prePostTxSenderAllowanceHook(let hookCall): + self.prePostTxSenderAllowanceHook = try HookCall(protobuf: hookCall) + case nil: + break + } + + // Handle receiver allowance hook calls + switch proto.receiverAllowanceHookCall { + case .preTxReceiverAllowanceHook(let hookCall): + self.preTxReceiverAllowanceHook = try HookCall(protobuf: hookCall) + case .prePostTxReceiverAllowanceHook(let hookCall): + self.prePostTxReceiverAllowanceHook = try HookCall(protobuf: hookCall) + case nil: + break + } } internal func toProtobuf() -> Protobuf { @@ -376,6 +611,20 @@ extension AbstractTokenTransferTransaction.NftTransfer: TryProtobufCodable { proto.receiverAccountID = receiverAccountId.toProtobuf() proto.serialNumber = Int64(bitPattern: serial) proto.isApproval = isApproval + + // Handle sender allowance hook calls + if let preTxSenderHook = preTxSenderAllowanceHook { + proto.senderAllowanceHookCall = .preTxSenderAllowanceHook(preTxSenderHook.toProtobuf()) + } else if let prePostTxSenderHook = prePostTxSenderAllowanceHook { + proto.senderAllowanceHookCall = .prePostTxSenderAllowanceHook(prePostTxSenderHook.toProtobuf()) + } + + // Handle receiver allowance hook calls + if let preTxReceiverHook = preTxReceiverAllowanceHook { + proto.receiverAllowanceHookCall = .preTxReceiverAllowanceHook(preTxReceiverHook.toProtobuf()) + } else if let prePostTxReceiverHook = prePostTxReceiverAllowanceHook { + proto.receiverAllowanceHookCall = .prePostTxReceiverAllowanceHook(prePostTxReceiverHook.toProtobuf()) + } } } } diff --git a/Sources/Hiero/TransferTransaction.swift b/Sources/Hiero/TransferTransaction.swift index 0db89049..c2bb81b3 100644 --- a/Sources/Hiero/TransferTransaction.swift +++ b/Sources/Hiero/TransferTransaction.swift @@ -76,6 +76,62 @@ public final class TransferTransaction: AbstractTokenTransferTransaction { return self } + /// Add a non-approved hbar transfer with a pre-transaction allowance hook to the transaction. + @discardableResult + public func hbarTransferWithPreTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { + doHbarTransferWithHook(accountId, amount.toTinybars(), false, hookCall, nil) + } + + /// Add a non-approved hbar transfer with a pre-post-transaction allowance hook to the transaction. + @discardableResult + public func hbarTransferWithPrePostTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { + doHbarTransferWithHook(accountId, amount.toTinybars(), false, nil, hookCall) + } + + /// Add an approved hbar transfer with a pre-transaction allowance hook to the transaction. + @discardableResult + public func approvedHbarTransferWithPreTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { + doHbarTransferWithHook(accountId, amount.toTinybars(), true, hookCall, nil) + } + + /// Add an approved hbar transfer with a pre-post-transaction allowance hook to the transaction. + @discardableResult + public func approvedHbarTransferWithPrePostTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { + doHbarTransferWithHook(accountId, amount.toTinybars(), true, nil, hookCall) + } + + private func doHbarTransferWithHook( + _ accountId: AccountId, + _ amount: Int64, + _ approved: Bool, + _ preTxHook: HookCall?, + _ prePostTxHook: HookCall? + ) -> Self { + for (index, transfer) in transfers.enumerated() + where transfer.accountId == accountId && transfer.isApproval == approved { + let newTinybars = transfer.amount + amount + if newTinybars == 0 { + transfers.remove(at: index) + } else { + transfers[index].amount = newTinybars + transfers[index].preTxAllowanceHook = preTxHook + transfers[index].prePostTxAllowanceHook = prePostTxHook + } + + return self + } + + transfers.append(Transfer( + accountId: accountId, + amount: amount, + isApproval: approved, + preTxAllowanceHook: preTxHook, + prePostTxAllowanceHook: prePostTxHook + )) + + return self + } + internal override func validateChecksums(on ledgerId: LedgerId) throws { try transfers.validateChecksums(on: ledgerId) try tokenTransfersInner.validateChecksums(on: ledgerId) diff --git a/Tests/HieroE2ETests/TransferTransactionHooks.swift b/Tests/HieroE2ETests/TransferTransactionHooks.swift new file mode 100644 index 00000000..7ffa7f72 --- /dev/null +++ b/Tests/HieroE2ETests/TransferTransactionHooks.swift @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import Hiero +import XCTest + +internal final class TransferTransactionHooks: XCTestCase { + + internal func testHbarTransferWithPreTxHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create a test account + let key = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(key.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create transfer transaction with hook + let transferTx = TransferTransaction() + .hbarTransferWithPreTxHook(accountId, 1, hookCall) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testHbarTransferWithPrePostTxHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create a test account + let key = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(key.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create transfer transaction with hook + let transferTx = TransferTransaction() + .hbarTransferWithPrePostTxHook(accountId, 1, hookCall) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testTokenTransferWithPreTxHook() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create a test account + let key = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(key.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + // Create a token + let tokenId = try await TokenCreateTransaction() + .name("Test Token") + .symbol("TT") + .decimals(2) + .initialSupply(1000) + .treasuryAccountId(testEnv.operator.accountId) + .adminKey(.single(testEnv.operator.privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId! + + addTeardownBlock { + _ = try await TokenDeleteTransaction(tokenId: tokenId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } + + // Associate the account with the token + _ = try await TokenAssociateTransaction(accountId: accountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create transfer transaction with hook + let transferTx = TransferTransaction() + .tokenTransferWithPreTxHook(tokenId, accountId, 100, hookCall) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testNftTransferWithSenderHooks() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create test accounts + let senderKey = PrivateKey.generateEd25519() + let senderReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(senderKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let senderAccountId = try XCTUnwrap(senderReceipt.accountId) + + let receiverKey = PrivateKey.generateEd25519() + let receiverReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(receiverKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) + + // Create an NFT token + let tokenId = try await TokenCreateTransaction() + .name("Test NFT") + .symbol("TNFT") + .tokenType(.nonFungibleUnique) + .treasuryAccountId(testEnv.operator.accountId) + .adminKey(.single(testEnv.operator.privateKey.publicKey)) + .supplyKey(.single(testEnv.operator.privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId! + + addTeardownBlock { + _ = try await TokenDeleteTransaction(tokenId: tokenId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } + + // Associate accounts with the token + _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Mint an NFT + let nftSerial = try await TokenMintTransaction(tokenId: tokenId) + .metadata([Data("NFT Metadata".utf8)]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .serials?.first! + + // Transfer NFT to sender + _ = try await TransferTransaction() + .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create transfer transaction with sender hook + let transferTx = TransferTransaction() + .nftTransferWithSenderHooks( + NftId(tokenId: tokenId, serial: nftSerial!), + senderAccountId, + receiverAccountId, + preTxSenderHook: hookCall + ) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testNftTransferWithReceiverHooks() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create test accounts + let senderKey = PrivateKey.generateEd25519() + let senderReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(senderKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let senderAccountId = try XCTUnwrap(senderReceipt.accountId) + + let receiverKey = PrivateKey.generateEd25519() + let receiverReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(receiverKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) + + // Create an NFT token + let tokenId = try await TokenCreateTransaction() + .name("Test NFT") + .symbol("TNFT") + .tokenType(.nonFungibleUnique) + .treasuryAccountId(testEnv.operator.accountId) + .adminKey(.single(testEnv.operator.privateKey.publicKey)) + .supplyKey(.single(testEnv.operator.privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId! + + addTeardownBlock { + _ = try await TokenDeleteTransaction(tokenId: tokenId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } + + // Associate accounts with the token + _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Mint an NFT + let nftSerial = try await TokenMintTransaction(tokenId: tokenId) + .metadata([Data("NFT Metadata".utf8)]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .serials?.first! + + // Transfer NFT to sender + _ = try await TransferTransaction() + .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create transfer transaction with receiver hook + let transferTx = TransferTransaction() + .nftTransferWithReceiverHooks( + NftId(tokenId: tokenId, serial: nftSerial!), + senderAccountId, + receiverAccountId, + preTxReceiverHook: hookCall + ) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testNftTransferWithAllHooks() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create test accounts + let senderKey = PrivateKey.generateEd25519() + let senderReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(senderKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let senderAccountId = try XCTUnwrap(senderReceipt.accountId) + + let receiverKey = PrivateKey.generateEd25519() + let receiverReceipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(receiverKey.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) + + // Create an NFT token + let tokenId = try await TokenCreateTransaction() + .name("Test NFT") + .symbol("TNFT") + .tokenType(.nonFungibleUnique) + .treasuryAccountId(testEnv.operator.accountId) + .adminKey(.single(testEnv.operator.privateKey.publicKey)) + .supplyKey(.single(testEnv.operator.privateKey.publicKey)) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .tokenId! + + addTeardownBlock { + _ = try await TokenDeleteTransaction(tokenId: tokenId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + } + + // Associate accounts with the token + _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Mint an NFT + let nftSerial = try await TokenMintTransaction(tokenId: tokenId) + .metadata([Data("NFT Metadata".utf8)]) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .serials?.first! + + // Transfer NFT to sender + _ = try await TransferTransaction() + .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) + .execute(testEnv.client) + .getReceipt(testEnv.client) + + // Create hook calls + var senderHookCall = HookCall() + senderHookCall = senderHookCall.hookId(1) + var senderEvmHookCall = EvmHookCall() + senderEvmHookCall = senderEvmHookCall.data(Data([0x01, 0x02, 0x03])) + senderEvmHookCall = senderEvmHookCall.gasLimit(100000) + senderHookCall = senderHookCall.evmHookCall(senderEvmHookCall) + + var receiverHookCall = HookCall() + receiverHookCall = receiverHookCall.hookId(2) + var receiverEvmHookCall = EvmHookCall() + receiverEvmHookCall = receiverEvmHookCall.data(Data([0x04, 0x05, 0x06])) + receiverEvmHookCall = receiverEvmHookCall.gasLimit(100000) + receiverHookCall = receiverHookCall.evmHookCall(receiverEvmHookCall) + + // Create transfer transaction with all hooks + let transferTx = TransferTransaction() + .nftTransferWithAllHooks( + NftId(tokenId: tokenId, serial: nftSerial!), + senderAccountId, + receiverAccountId, + preTxSenderHook: senderHookCall, + preTxReceiverHook: receiverHookCall + ) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } + + internal func testApprovedTransferWithHooks() async throws { + let testEnv = try TestEnvironment.nonFree + + // Create a test account + let key = PrivateKey.generateEd25519() + let receipt = try await AccountCreateTransaction() + .keyWithoutAlias(.single(key.publicKey)) + .initialBalance(10) + .execute(testEnv.client) + .getReceipt(testEnv.client) + let accountId = try XCTUnwrap(receipt.accountId) + + // Create a simple hook call + var hookCall = HookCall() + hookCall = hookCall.hookId(1) + var evmHookCall = EvmHookCall() + evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) + evmHookCall = evmHookCall.gasLimit(100000) + hookCall = hookCall.evmHookCall(evmHookCall) + + // Create approved transfer transaction with hook + let transferTx = TransferTransaction() + .approvedHbarTransferWithPreTxHook(accountId, 1, hookCall) + + // Execute the transaction + let response = try await transferTx.execute(testEnv.client) + let transferReceipt = try await response.getReceipt(testEnv.client) + + XCTAssertEqual(transferReceipt.status, .success) + } +} From 4778222372df8e109d6f457f75d67318b363676d Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Mon, 27 Oct 2025 19:56:05 -0400 Subject: [PATCH 5/7] feat: update examples Signed-off-by: Rob Walworth --- Examples/AccountHooks/main.swift | 74 ++++++++++++++---------- Examples/ContractHooks/main.swift | 80 +++++++++++++++----------- Examples/LambdaSStore/main.swift | 82 ++++++++++++++++----------- Examples/TransferWithHooks/main.swift | 46 +++++++-------- Sources/Hiero/Data+Extensions.swift | 2 +- 5 files changed, 166 insertions(+), 118 deletions(-) diff --git a/Examples/AccountHooks/main.swift b/Examples/AccountHooks/main.swift index 63457787..2dcf6f6e 100644 --- a/Examples/AccountHooks/main.swift +++ b/Examples/AccountHooks/main.swift @@ -1,40 +1,39 @@ // SPDX-License-Identifier: Apache-2.0 -import Foundation import Hiero -import HieroProtobufs +import HieroExampleUtilities +import SwiftDotenv +import Foundation @main -struct AccountHooksExample { - static func main() async throws { - // Initialize the client - let client = Client.forTestnet() - - // Create operator account - let operatorKey = PrivateKey.generateEd25519() - let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID - - client.setOperator(operatorId, operatorKey) +internal enum Program { + internal static func main() async throws { + /// Grab the environment variables. + let env = try Dotenv.load() + + /// Initialize the client based on the provided environment. + let client = try Client.forName(env.networkName) + client.setOperator(env.operatorAccountId, env.operatorKey) - print("🚀 Account Hooks Example") - print("=======================") + print("Account Hooks Example") + print("====================") // Create a contract that will serve as our hook - print("\n📝 Creating hook contract...") + print("Creating hook contract...") let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) .adminKey(.single(contractKey.publicKey)) .gas(100000) .execute(client) let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - print("✅ Created hook contract: \(contractId)") + print("Created hook contract: \(contractId)") // Create Lambda EVM Hook specification - print("\n🔧 Creating Lambda EVM Hook specification...") + print("Creating Lambda EVM Hook specification...") var evmHookSpec = EvmHookSpec() evmHookSpec = evmHookSpec.contractId(contractId) @@ -47,7 +46,7 @@ struct AccountHooksExample { hookCreationDetails = hookCreationDetails.adminKey(Key.single(contractKey.publicKey)) // Example 1: Create account with hooks - print("\n👤 Example 1: Creating account with hooks") + print("Example 1: Creating account with hooks") let accountKey = PrivateKey.generateEd25519() let accountResponse = try await AccountCreateTransaction() @@ -58,10 +57,10 @@ struct AccountHooksExample { let accountReceipt = try await accountResponse.getReceipt(client) let accountId = accountReceipt.accountId! - print("✅ Created account with hooks: \(accountId)") + print("Created account with hooks: \(accountId)") // Example 2: Update account to add more hooks - print("\n🔄 Example 2: Updating account to add more hooks") + print("Example 2: Updating account to add more hooks") var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) @@ -73,10 +72,10 @@ struct AccountHooksExample { .execute(client) let updateReceipt = try await updateResponse.getReceipt(client) - print("✅ Updated account with additional hooks: \(updateReceipt.status)") + print("Updated account with additional hooks: \(updateReceipt.status)") // Example 3: Update account to delete hooks - print("\n🗑️ Example 3: Updating account to delete hooks") + print("Example 3: Updating account to delete hooks") let deleteResponse = try await AccountUpdateTransaction() .accountId(accountId) @@ -84,24 +83,41 @@ struct AccountHooksExample { .execute(client) let deleteReceipt = try await deleteResponse.getReceipt(client) - print("✅ Updated account to delete hooks: \(deleteReceipt.status)") + print("Updated account to delete hooks: \(deleteReceipt.status)") // Cleanup - print("\n🧹 Cleaning up...") + print("Cleaning up...") _ = try await AccountDeleteTransaction() .accountId(accountId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) _ = try await ContractDeleteTransaction() .contractId(contractId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - print("✅ Cleanup completed") - print("\n🎉 Account Hooks Example completed successfully!") + print("Cleanup completed") + print("\nAccount Hooks Example completed successfully!") + } +} + +extension Environment { + /// Account ID for the operator to use in this example. + internal var operatorAccountId: AccountId { + AccountId(self["OPERATOR_ID"]!.stringValue)! + } + + /// Private key for the operator to use in this example. + internal var operatorKey: PrivateKey { + PrivateKey(self["OPERATOR_KEY"]!.stringValue)! + } + + /// The name of the Hiero network this example should run against. + internal var networkName: String { + self["HEDERA_NETWORK"]?.stringValue ?? "testnet" } } diff --git a/Examples/ContractHooks/main.swift b/Examples/ContractHooks/main.swift index 75946b3d..5ddc3476 100644 --- a/Examples/ContractHooks/main.swift +++ b/Examples/ContractHooks/main.swift @@ -1,40 +1,39 @@ // SPDX-License-Identifier: Apache-2.0 -import Foundation import Hiero -import HieroProtobufs +import HieroExampleUtilities +import SwiftDotenv +import Foundation @main -struct ContractHooksExample { - static func main() async throws { - // Initialize the client - let client = Client.forTestnet() - - // Create operator account - let operatorKey = PrivateKey.generateEd25519() - let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID - - client.setOperator(operatorId, operatorKey) +internal enum Program { + internal static func main() async throws { + /// Grab the environment variables. + let env = try Dotenv.load() + + /// Initialize the client based on the provided environment. + let client = try Client.forName(env.networkName) + client.setOperator(env.operatorAccountId, env.operatorKey) - print("🚀 Contract Hooks Example") - print("========================") + print("Contract Hooks Example") + print("=====================") // Create a contract that will serve as our hook - print("\n📝 Creating hook contract...") + print("Creating hook contract...") let hookContractKey = PrivateKey.generateEd25519() let hookContractResponse = try await ContractCreateTransaction() - .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) .adminKey(.single(hookContractKey.publicKey)) .gas(100000) .execute(client) let hookContractReceipt = try await hookContractResponse.getReceipt(client) let hookContractId = hookContractReceipt.contractId! - print("✅ Created hook contract: \(hookContractId)") + print("Created hook contract: \(hookContractId)") // Create Lambda EVM Hook specification - print("\n🔧 Creating Lambda EVM Hook specification...") + print("Creating Lambda EVM Hook specification...") var evmHookSpec = EvmHookSpec() evmHookSpec = evmHookSpec.contractId(hookContractId) @@ -47,11 +46,11 @@ struct ContractHooksExample { hookCreationDetails = hookCreationDetails.adminKey(Key.single(hookContractKey.publicKey)) // Example 1: Create contract with hooks - print("\n📄 Example 1: Creating contract with hooks") + print("Example 1: Creating contract with hooks") let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecodeFileId(FileId.fromString("0.0.1236")) // Replace with your contract file ID + .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) .adminKey(.single(contractKey.publicKey)) .gas(200000) .addHook(hookCreationDetails) @@ -59,10 +58,10 @@ struct ContractHooksExample { let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - print("✅ Created contract with hooks: \(contractId)") + print("Created contract with hooks: \(contractId)") // Example 2: Update contract to add more hooks - print("\n🔄 Example 2: Updating contract to add more hooks") + print("Example 2: Updating contract to add more hooks") var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) @@ -74,10 +73,10 @@ struct ContractHooksExample { .execute(client) let updateReceipt = try await updateResponse.getReceipt(client) - print("✅ Updated contract with additional hooks: \(updateReceipt.status)") + print("Updated contract with additional hooks: \(updateReceipt.status)") // Example 3: Update contract to delete hooks - print("\n🗑️ Example 3: Updating contract to delete hooks") + print("Example 3: Updating contract to delete hooks") let deleteResponse = try await ContractUpdateTransaction() .contractId(contractId) @@ -85,10 +84,10 @@ struct ContractHooksExample { .execute(client) let deleteReceipt = try await deleteResponse.getReceipt(client) - print("✅ Updated contract to delete hooks: \(deleteReceipt.status)") + print("Updated contract to delete hooks: \(deleteReceipt.status)") // Example 4: Execute contract with hooks - print("\n⚡ Example 4: Executing contract with hooks") + print("Example 4: Executing contract with hooks") let executeResponse = try await ContractExecuteTransaction() .contractId(contractId) @@ -97,24 +96,41 @@ struct ContractHooksExample { .execute(client) let executeReceipt = try await executeResponse.getReceipt(client) - print("✅ Executed contract with hooks: \(executeReceipt.status)") + print("Executed contract with hooks: \(executeReceipt.status)") // Cleanup - print("\n🧹 Cleaning up...") + print("Cleaning up...") _ = try await ContractDeleteTransaction() .contractId(contractId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) _ = try await ContractDeleteTransaction() .contractId(hookContractId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - print("✅ Cleanup completed") - print("\n🎉 Contract Hooks Example completed successfully!") + print("Cleanup completed") + print("\nContract Hooks Example completed successfully!") + } +} + +extension Environment { + /// Account ID for the operator to use in this example. + internal var operatorAccountId: AccountId { + AccountId(self["OPERATOR_ID"]!.stringValue)! + } + + /// Private key for the operator to use in this example. + internal var operatorKey: PrivateKey { + PrivateKey(self["OPERATOR_KEY"]!.stringValue)! + } + + /// The name of the Hiero network this example should run against. + internal var networkName: String { + self["HEDERA_NETWORK"]?.stringValue ?? "testnet" } } diff --git a/Examples/LambdaSStore/main.swift b/Examples/LambdaSStore/main.swift index affbdecf..61de918a 100644 --- a/Examples/LambdaSStore/main.swift +++ b/Examples/LambdaSStore/main.swift @@ -2,39 +2,38 @@ import Foundation import Hiero -import HieroProtobufs +import HieroExampleUtilities +import SwiftDotenv @main -struct LambdaSStoreExample { - static func main() async throws { - // Initialize the client - let client = Client.forTestnet() - - // Create operator account - let operatorKey = PrivateKey.generateEd25519() - let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID - - client.setOperator(operatorId, operatorKey) +internal enum Program { + internal static func main() async throws { + /// Grab the environment variables. + let env = try Dotenv.load() + + /// Initialize the client based on the provided environment. + let client = try Client.forName(env.networkName) + client.setOperator(env.operatorAccountId, env.operatorKey) - print("🚀 Lambda SSTORE Example") - print("========================") + print("Lambda SSTORE Example") + print("====================") // Create a contract that will serve as our hook - print("\n📝 Creating hook contract...") + print("Creating hook contract...") let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecodeFileId(FileId.fromString("0.0.1235")) // Replace with your contract file ID + .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) .adminKey(.single(contractKey.publicKey)) .gas(100000) .execute(client) let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - print("✅ Created hook contract: \(contractId)") + print("Created hook contract: \(contractId)") // Create account with hooks - print("\n👤 Creating account with hooks...") + print("Creating account with hooks...") var evmHookSpec = EvmHookSpec() evmHookSpec = evmHookSpec.contractId(contractId) @@ -54,20 +53,20 @@ struct LambdaSStoreExample { let accountReceipt = try await accountResponse.getReceipt(client) let accountId = accountReceipt.accountId! - print("✅ Created account with hooks: \(accountId)") + print("Created account with hooks: \(accountId)") // Create HookId for the Lambda SSTORE transaction - print("\n🔧 Creating HookId...") + print("Creating HookId...") var hookEntityId = HookEntityId() hookEntityId = hookEntityId.accountId(accountId) let hookId = HookId(entityId: hookEntityId, hookId: 1) - print("✅ Created HookId: \(hookId)") + print("Created HookId: \(hookId)") // Example 1: Update storage slot - print("\n💾 Example 1: Updating storage slot") + print("Example 1: Updating storage slot") var storageSlot = LambdaStorageSlot() storageSlot = storageSlot.key(Data([0x01, 0x02, 0x03, 0x04])) // 32-byte key @@ -82,10 +81,10 @@ struct LambdaSStoreExample { .execute(client) let sstoreReceipt1 = try await sstoreResponse1.getReceipt(client) - print("✅ Updated storage slot: \(sstoreReceipt1.status)") + print("Updated storage slot: \(sstoreReceipt1.status)") // Example 2: Update mapping entries - print("\n🗺️ Example 2: Updating mapping entries") + print("Example 2: Updating mapping entries") var mappingEntry1 = LambdaMappingEntry() mappingEntry1 = mappingEntry1.key(Data([0x09, 0x0A, 0x0B, 0x0C])) // 32-byte key @@ -108,10 +107,10 @@ struct LambdaSStoreExample { .execute(client) let sstoreReceipt2 = try await sstoreResponse2.getReceipt(client) - print("✅ Updated mapping entries: \(sstoreReceipt2.status)") + print("Updated mapping entries: \(sstoreReceipt2.status)") // Example 3: Multiple storage updates - print("\n📦 Example 3: Multiple storage updates") + print("Example 3: Multiple storage updates") var storageSlot2 = LambdaStorageSlot() storageSlot2 = storageSlot2.key(Data([0x1D, 0x1E, 0x1F, 0x20])) @@ -126,10 +125,10 @@ struct LambdaSStoreExample { .execute(client) let sstoreReceipt3 = try await sstoreResponse3.getReceipt(client) - print("✅ Applied multiple storage updates: \(sstoreReceipt3.status)") + print("Applied multiple storage updates: \(sstoreReceipt3.status)") // Example 4: Clear storage updates - print("\n🧹 Example 4: Clearing storage updates") + print("Example 4: Clearing storage updates") let sstoreResponse4 = try await LambdaSStoreTransaction() .hookId(hookId) @@ -137,24 +136,41 @@ struct LambdaSStoreExample { .execute(client) let sstoreReceipt4 = try await sstoreResponse4.getReceipt(client) - print("✅ Cleared storage updates: \(sstoreReceipt4.status)") + print("Cleared storage updates: \(sstoreReceipt4.status)") // Cleanup - print("\n🧹 Cleaning up...") + print("Cleaning up...") _ = try await AccountDeleteTransaction() .accountId(accountId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) _ = try await ContractDeleteTransaction() .contractId(contractId) - .transferAccountId(operatorId) + .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - print("✅ Cleanup completed") - print("\n🎉 Lambda SSTORE Example completed successfully!") + print("Cleanup completed") + print("\nLambda SSTORE Example completed successfully!") + } +} + +extension Environment { + /// Account ID for the operator to use in this example. + internal var operatorAccountId: AccountId { + AccountId(self["OPERATOR_ID"]!.stringValue)! + } + + /// Private key for the operator to use in this example. + internal var operatorKey: PrivateKey { + PrivateKey(self["OPERATOR_KEY"]!.stringValue)! + } + + /// The name of the Hiero network this example should run against. + internal var networkName: String { + self["HEDERA_NETWORK"]?.stringValue ?? "testnet" } } diff --git a/Examples/TransferWithHooks/main.swift b/Examples/TransferWithHooks/main.swift index 9103abf7..25c1637a 100644 --- a/Examples/TransferWithHooks/main.swift +++ b/Examples/TransferWithHooks/main.swift @@ -22,11 +22,11 @@ struct TransferWithHooksExample { client.setOperator(operatorId, operatorKey) - print("🚀 Transfer with Hooks Example") + print("Transfer with Hooks Example") print("==============================") // Create test accounts - print("\n📝 Creating test accounts...") + print("Creating test accounts...") let senderKey = PrivateKey.generateEd25519() let senderResponse = try await AccountCreateTransaction() @@ -44,11 +44,11 @@ struct TransferWithHooksExample { let receiverReceipt = try await receiverResponse.getReceipt(client) let receiverAccountId = receiverReceipt.accountId! - print("✅ Created sender account: \(senderAccountId)") - print("✅ Created receiver account: \(receiverAccountId)") + print("Created sender account: \(senderAccountId)") + print("Created receiver account: \(receiverAccountId)") // Example 1: HBAR transfer with pre-transaction hook - print("\n💰 Example 1: HBAR transfer with pre-transaction hook") + print("Example 1: HBAR transfer with pre-transaction hook") var preTxHook = HookCall() preTxHook = preTxHook.hookId(1) @@ -63,10 +63,10 @@ struct TransferWithHooksExample { let hbarResponse = try await hbarTransferTx.execute(client) let hbarReceipt = try await hbarResponse.getReceipt(client) - print("✅ HBAR transfer with hook completed: \(hbarReceipt.status)") + print("HBAR transfer with hook completed: \(hbarReceipt.status)") // Example 2: HBAR transfer with pre-post-transaction hook - print("\n💰 Example 2: HBAR transfer with pre-post-transaction hook") + print("Example 2: HBAR transfer with pre-post-transaction hook") var prePostTxHook = HookCall() prePostTxHook = prePostTxHook.hookId(2) @@ -81,10 +81,10 @@ struct TransferWithHooksExample { let hbarPrePostResponse = try await hbarPrePostTx.execute(client) let hbarPrePostReceipt = try await hbarPrePostResponse.getReceipt(client) - print("✅ HBAR transfer with pre-post hook completed: \(hbarPrePostReceipt.status)") + print("HBAR transfer with pre-post hook completed: \(hbarPrePostReceipt.status)") // Example 3: Create a token and transfer with hooks - print("\n🪙 Example 3: Token transfer with hooks") + print("Example 3: Token transfer with hooks") let tokenId = try await TokenCreateTransaction() .name("Hook Test Token") @@ -97,7 +97,7 @@ struct TransferWithHooksExample { .getReceipt(client) .tokenId! - print("✅ Created token: \(tokenId)") + print("Created token: \(tokenId)") // Associate accounts with the token _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) @@ -108,7 +108,7 @@ struct TransferWithHooksExample { .execute(client) .getReceipt(client) - print("✅ Associated accounts with token") + print("Associated accounts with token") // Transfer tokens with hook var tokenHook = HookCall() @@ -124,10 +124,10 @@ struct TransferWithHooksExample { let tokenResponse = try await tokenTransferTx.execute(client) let tokenReceipt = try await tokenResponse.getReceipt(client) - print("✅ Token transfer with hook completed: \(tokenReceipt.status)") + print("Token transfer with hook completed: \(tokenReceipt.status)") // Example 4: NFT transfer with hooks - print("\n🎨 Example 4: NFT transfer with hooks") + print("Example 4: NFT transfer with hooks") let nftTokenId = try await TokenCreateTransaction() .name("Hook Test NFT") @@ -140,7 +140,7 @@ struct TransferWithHooksExample { .getReceipt(client) .tokenId! - print("✅ Created NFT token: \(nftTokenId)") + print("Created NFT token: \(nftTokenId)") // Associate accounts with the NFT token _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [nftTokenId]) @@ -158,7 +158,7 @@ struct TransferWithHooksExample { .getReceipt(client) .serials?.first! - print("✅ Minted NFT with serial: \(nftSerial!)") + print("Minted NFT with serial: \(nftSerial!)") // Transfer NFT to sender first _ = try await TransferTransaction() @@ -185,10 +185,10 @@ struct TransferWithHooksExample { let nftResponse = try await nftTransferTx.execute(client) let nftReceipt = try await nftResponse.getReceipt(client) - print("✅ NFT transfer with sender hook completed: \(nftReceipt.status)") + print("NFT transfer with sender hook completed: \(nftReceipt.status)") // Example 5: NFT transfer with receiver hook - print("\n🎨 Example 5: NFT transfer with receiver hook") + print("Example 5: NFT transfer with receiver hook") var receiverHook = HookCall() receiverHook = receiverHook.hookId(5) @@ -208,10 +208,10 @@ struct TransferWithHooksExample { let nftReceiverResponse = try await nftReceiverTx.execute(client) let nftReceiverReceipt = try await nftReceiverResponse.getReceipt(client) - print("✅ NFT transfer with receiver hook completed: \(nftReceiverReceipt.status)") + print("NFT transfer with receiver hook completed: \(nftReceiverReceipt.status)") // Example 6: NFT transfer with all hooks - print("\n🎨 Example 6: NFT transfer with all hooks") + print("Example 6: NFT transfer with all hooks") var allSenderHook = HookCall() allSenderHook = allSenderHook.hookId(6) @@ -239,10 +239,10 @@ struct TransferWithHooksExample { let nftAllHooksResponse = try await nftAllHooksTx.execute(client) let nftAllHooksReceipt = try await nftAllHooksResponse.getReceipt(client) - print("✅ NFT transfer with all hooks completed: \(nftAllHooksReceipt.status)") + print("NFT transfer with all hooks completed: \(nftAllHooksReceipt.status)") // Cleanup - print("\n🧹 Cleaning up...") + print("Cleaning up...") _ = try await AccountDeleteTransaction() .accountId(senderAccountId) @@ -264,7 +264,7 @@ struct TransferWithHooksExample { .execute(client) .getReceipt(client) - print("✅ Cleanup completed") - print("\n🎉 Transfer with Hooks Example completed successfully!") + print("Cleanup completed") + print("Transfer with Hooks Example completed successfully!") } } diff --git a/Sources/Hiero/Data+Extensions.swift b/Sources/Hiero/Data+Extensions.swift index 6aa62a50..1044f6a4 100644 --- a/Sources/Hiero/Data+Extensions.swift +++ b/Sources/Hiero/Data+Extensions.swift @@ -36,7 +36,7 @@ extension Data { }) } - internal init?(hexEncoded: S) { + public init?(hexEncoded: S) { let chars = Array(hexEncoded.utf8) // note: hex check is done character by character let count = chars.count From d983cd8be220e6f1759ba753f8d4e4f562c3a32b Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 28 Oct 2025 18:48:08 -0400 Subject: [PATCH 6/7] feat: finish up features and tests Signed-off-by: Rob Walworth --- Examples/AccountHooks/main.swift | 69 +-- Examples/ContractHooks/main.swift | 82 ++-- Examples/LambdaSStore/main.swift | 109 ++--- Examples/TransferWithHooks/main.swift | 436 ++++++++--------- Sources/Hiero/Hooks/EvmHookSpec.swift | 2 +- Sources/Hiero/Hooks/FungibleHookCall.swift | 76 +++ Sources/Hiero/Hooks/FungibleHookType.swift | 54 +++ Sources/Hiero/Hooks/NftHookCall.swift | 76 +++ Sources/Hiero/Hooks/NftHookType.swift | 54 +++ .../AbstractTokenTransferTransaction.swift | 231 +++------ Sources/Hiero/TransferTransaction.swift | 44 +- .../Generated/services/basic_types.pb.swift | 39 +- .../Generated/services/hook_types.pb.swift | 110 +---- .../Generated/services/response_code.pb.swift | 81 ++++ .../Protos/services/basic_types.proto | 4 + .../Protos/services/hook_types.proto | 19 +- .../Protos/services/response_code.proto | 50 ++ .../HieroE2ETests/Account/AccountCreate.swift | 48 +- .../HieroE2ETests/Account/AccountUpdate.swift | 16 +- .../Contract/ContractCreate.swift | 94 ++-- .../Contract/ContractCreateFlow.swift | 6 +- .../Contract/ContractUpdate.swift | 152 +++--- .../TransferTransactionHooks.swift | 445 ++++++------------ Tests/HieroTests/EvmHookSpecTests.swift | 12 +- Tests/HieroTests/FungibleHookCallTests.swift | 183 +++++++ Tests/HieroTests/FungibleHookTypeTests.swift | 49 ++ Tests/HieroTests/NFTHookCallTests.swift | 183 +++++++ Tests/HieroTests/NFTHookTypeTests.swift | 49 ++ protobufs | 2 +- 29 files changed, 1705 insertions(+), 1070 deletions(-) create mode 100644 Sources/Hiero/Hooks/FungibleHookCall.swift create mode 100644 Sources/Hiero/Hooks/FungibleHookType.swift create mode 100644 Sources/Hiero/Hooks/NftHookCall.swift create mode 100644 Sources/Hiero/Hooks/NftHookType.swift create mode 100644 Tests/HieroTests/FungibleHookCallTests.swift create mode 100644 Tests/HieroTests/FungibleHookTypeTests.swift create mode 100644 Tests/HieroTests/NFTHookCallTests.swift create mode 100644 Tests/HieroTests/NFTHookTypeTests.swift diff --git a/Examples/AccountHooks/main.swift b/Examples/AccountHooks/main.swift index 2dcf6f6e..4500cb06 100644 --- a/Examples/AccountHooks/main.swift +++ b/Examples/AccountHooks/main.swift @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +import Foundation import Hiero import HieroExampleUtilities import SwiftDotenv -import Foundation @main internal enum Program { @@ -14,40 +14,43 @@ internal enum Program { /// Initialize the client based on the provided environment. let client = try Client.forName(env.networkName) client.setOperator(env.operatorAccountId, env.operatorKey) - + print("Account Hooks Example") print("====================") - + // Create a contract that will serve as our hook print("Creating hook contract...") - + let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .adminKey(.single(contractKey.publicKey)) .gas(100000) .execute(client) let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - + print("Created hook contract: \(contractId)") - + // Create Lambda EVM Hook specification print("Creating Lambda EVM Hook specification...") - - var evmHookSpec = EvmHookSpec() - evmHookSpec = evmHookSpec.contractId(contractId) - + + let evmHookSpec = EvmHookSpec(contractId: contractId) let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) - + // Create hook creation details - var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) - hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) - hookCreationDetails = hookCreationDetails.adminKey(Key.single(contractKey.publicKey)) - + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, lambdaEvmHook: lambdaEvmHook, + adminKey: Key.single(contractKey.publicKey)) + // Example 1: Create account with hooks print("Example 1: Creating account with hooks") - + let accountKey = PrivateKey.generateEd25519() let accountResponse = try await AccountCreateTransaction() .keyWithoutAlias(.single(accountKey.publicKey)) @@ -56,50 +59,50 @@ internal enum Program { .execute(client) let accountReceipt = try await accountResponse.getReceipt(client) let accountId = accountReceipt.accountId! - + print("Created account with hooks: \(accountId)") - + // Example 2: Update account to add more hooks print("Example 2: Updating account to add more hooks") - - var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) - hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) - hookCreationDetails2 = hookCreationDetails2.adminKey(Key.single(contractKey.publicKey)) - + + let hookCreationDetails2 = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, lambdaEvmHook: lambdaEvmHook, + adminKey: Key.single(contractKey.publicKey)) + let updateResponse = try await AccountUpdateTransaction() .accountId(accountId) .addHookToCreate(hookCreationDetails2) .execute(client) let updateReceipt = try await updateResponse.getReceipt(client) - + print("Updated account with additional hooks: \(updateReceipt.status)") - + // Example 3: Update account to delete hooks print("Example 3: Updating account to delete hooks") - + let deleteResponse = try await AccountUpdateTransaction() .accountId(accountId) - .addHookToDelete(1) // Delete hook with ID 1 + .addHookToDelete(1) // Delete hook with ID 1 .execute(client) let deleteReceipt = try await deleteResponse.getReceipt(client) - + print("Updated account to delete hooks: \(deleteReceipt.status)") - + // Cleanup print("Cleaning up...") - + _ = try await AccountDeleteTransaction() .accountId(accountId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + _ = try await ContractDeleteTransaction() .contractId(contractId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + print("Cleanup completed") print("\nAccount Hooks Example completed successfully!") } diff --git a/Examples/ContractHooks/main.swift b/Examples/ContractHooks/main.swift index 5ddc3476..c44f9703 100644 --- a/Examples/ContractHooks/main.swift +++ b/Examples/ContractHooks/main.swift @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 +import Foundation import Hiero import HieroExampleUtilities import SwiftDotenv -import Foundation @main internal enum Program { @@ -14,105 +14,113 @@ internal enum Program { /// Initialize the client based on the provided environment. let client = try Client.forName(env.networkName) client.setOperator(env.operatorAccountId, env.operatorKey) - + print("Contract Hooks Example") print("=====================") - + // Create a contract that will serve as our hook print("Creating hook contract...") - + let hookContractKey = PrivateKey.generateEd25519() let hookContractResponse = try await ContractCreateTransaction() - .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .adminKey(.single(hookContractKey.publicKey)) .gas(100000) .execute(client) let hookContractReceipt = try await hookContractResponse.getReceipt(client) let hookContractId = hookContractReceipt.contractId! - + print("Created hook contract: \(hookContractId)") - + // Create Lambda EVM Hook specification print("Creating Lambda EVM Hook specification...") - - var evmHookSpec = EvmHookSpec() - evmHookSpec = evmHookSpec.contractId(hookContractId) - + + let evmHookSpec = EvmHookSpec(contractId: hookContractId) let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) - + // Create hook creation details - var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) - hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) - hookCreationDetails = hookCreationDetails.adminKey(Key.single(hookContractKey.publicKey)) - + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, lambdaEvmHook: lambdaEvmHook, + adminKey: Key.single(hookContractKey.publicKey)) + // Example 1: Create contract with hooks print("Example 1: Creating contract with hooks") - + let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .adminKey(.single(contractKey.publicKey)) .gas(200000) .addHook(hookCreationDetails) .execute(client) let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - + print("Created contract with hooks: \(contractId)") - + // Example 2: Update contract to add more hooks print("Example 2: Updating contract to add more hooks") - - var hookCreationDetails2 = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) - hookCreationDetails2 = hookCreationDetails2.lambdaEvmHook(lambdaEvmHook) - hookCreationDetails2 = hookCreationDetails2.adminKey(Key.single(hookContractKey.publicKey)) - + + let hookCreationDetails2 = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, lambdaEvmHook: lambdaEvmHook, + adminKey: Key.single(hookContractKey.publicKey)) + let updateResponse = try await ContractUpdateTransaction() .contractId(contractId) .addHookToCreate(hookCreationDetails2) .execute(client) let updateReceipt = try await updateResponse.getReceipt(client) - + print("Updated contract with additional hooks: \(updateReceipt.status)") - + // Example 3: Update contract to delete hooks print("Example 3: Updating contract to delete hooks") - + let deleteResponse = try await ContractUpdateTransaction() .contractId(contractId) - .addHookToDelete(1) // Delete hook with ID 1 + .addHookToDelete(1) // Delete hook with ID 1 .execute(client) let deleteReceipt = try await deleteResponse.getReceipt(client) - + print("Updated contract to delete hooks: \(deleteReceipt.status)") - + // Example 4: Execute contract with hooks print("Example 4: Executing contract with hooks") - + let executeResponse = try await ContractExecuteTransaction() .contractId(contractId) .gas(100000) .function("someFunction") .execute(client) let executeReceipt = try await executeResponse.getReceipt(client) - + print("Executed contract with hooks: \(executeReceipt.status)") - + // Cleanup print("Cleaning up...") - + _ = try await ContractDeleteTransaction() .contractId(contractId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + _ = try await ContractDeleteTransaction() .contractId(hookContractId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + print("Cleanup completed") print("\nContract Hooks Example completed successfully!") } diff --git a/Examples/LambdaSStore/main.swift b/Examples/LambdaSStore/main.swift index 61de918a..fac37b85 100644 --- a/Examples/LambdaSStore/main.swift +++ b/Examples/LambdaSStore/main.swift @@ -14,36 +14,41 @@ internal enum Program { /// Initialize the client based on the provided environment. let client = try Client.forName(env.networkName) client.setOperator(env.operatorAccountId, env.operatorKey) - + print("Lambda SSTORE Example") print("====================") - + // Create a contract that will serve as our hook print("Creating hook contract...") - + let contractKey = PrivateKey.generateEd25519() let contractResponse = try await ContractCreateTransaction() - .bytecode(Data(hexEncoded: "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033")!) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .adminKey(.single(contractKey.publicKey)) .gas(100000) .execute(client) let contractReceipt = try await contractResponse.getReceipt(client) let contractId = contractReceipt.contractId! - + print("Created hook contract: \(contractId)") - + // Create account with hooks print("Creating account with hooks...") - + var evmHookSpec = EvmHookSpec() evmHookSpec = evmHookSpec.contractId(contractId) - + let lambdaEvmHook = LambdaEvmHook(spec: evmHookSpec) - - var hookCreationDetails = HookCreationDetails(hookExtensionPoint: .accountAllowanceHook) - hookCreationDetails = hookCreationDetails.lambdaEvmHook(lambdaEvmHook) - hookCreationDetails = hookCreationDetails.adminKey(Key.single(contractKey.publicKey)) - + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, lambdaEvmHook: lambdaEvmHook, + adminKey: Key.single(contractKey.publicKey)) + let accountKey = PrivateKey.generateEd25519() let accountResponse = try await AccountCreateTransaction() .keyWithoutAlias(.single(accountKey.publicKey)) @@ -52,107 +57,105 @@ internal enum Program { .execute(client) let accountReceipt = try await accountResponse.getReceipt(client) let accountId = accountReceipt.accountId! - + print("Created account with hooks: \(accountId)") - + // Create HookId for the Lambda SSTORE transaction print("Creating HookId...") - - var hookEntityId = HookEntityId() - hookEntityId = hookEntityId.accountId(accountId) - + + let hookEntityId = HookEntityId(accountId) let hookId = HookId(entityId: hookEntityId, hookId: 1) - + print("Created HookId: \(hookId)") - + // Example 1: Update storage slot print("Example 1: Updating storage slot") - + var storageSlot = LambdaStorageSlot() - storageSlot = storageSlot.key(Data([0x01, 0x02, 0x03, 0x04])) // 32-byte key - storageSlot = storageSlot.value(Data([0x05, 0x06, 0x07, 0x08])) // 32-byte value - + storageSlot = storageSlot.key(Data([0x01, 0x02, 0x03, 0x04])) // 32-byte key + storageSlot = storageSlot.value(Data([0x05, 0x06, 0x07, 0x08])) // 32-byte value + var storageUpdate = LambdaStorageUpdate() storageUpdate = storageUpdate.setStorageSlot(storageSlot) - + let sstoreResponse1 = try await LambdaSStoreTransaction() .hookId(hookId) .addStorageUpdate(storageUpdate) .execute(client) let sstoreReceipt1 = try await sstoreResponse1.getReceipt(client) - + print("Updated storage slot: \(sstoreReceipt1.status)") - + // Example 2: Update mapping entries print("Example 2: Updating mapping entries") - + var mappingEntry1 = LambdaMappingEntry() - mappingEntry1 = mappingEntry1.key(Data([0x09, 0x0A, 0x0B, 0x0C])) // 32-byte key - mappingEntry1 = mappingEntry1.value(Data([0x0D, 0x0E, 0x0F, 0x10])) // 32-byte value - + mappingEntry1 = mappingEntry1.key(Data([0x09, 0x0A, 0x0B, 0x0C])) // 32-byte key + mappingEntry1 = mappingEntry1.value(Data([0x0D, 0x0E, 0x0F, 0x10])) // 32-byte value + var mappingEntry2 = LambdaMappingEntry() - mappingEntry2 = mappingEntry2.preimage(Data([0x11, 0x12, 0x13, 0x14])) // Preimage - mappingEntry2 = mappingEntry2.value(Data([0x15, 0x16, 0x17, 0x18])) // 32-byte value - + mappingEntry2 = mappingEntry2.preimage(Data([0x11, 0x12, 0x13, 0x14])) // Preimage + mappingEntry2 = mappingEntry2.value(Data([0x15, 0x16, 0x17, 0x18])) // 32-byte value + var mappingEntries = LambdaMappingEntries() - mappingEntries = mappingEntries.mappingSlot(Data([0x19, 0x1A, 0x1B, 0x1C])) // Mapping slot + mappingEntries = mappingEntries.mappingSlot(Data([0x19, 0x1A, 0x1B, 0x1C])) // Mapping slot mappingEntries = mappingEntries.setEntries([mappingEntry1, mappingEntry2]) - + var mappingUpdate = LambdaStorageUpdate() mappingUpdate = mappingUpdate.setMappingEntries(mappingEntries) - + let sstoreResponse2 = try await LambdaSStoreTransaction() .hookId(hookId) .addStorageUpdate(mappingUpdate) .execute(client) let sstoreReceipt2 = try await sstoreResponse2.getReceipt(client) - + print("Updated mapping entries: \(sstoreReceipt2.status)") - + // Example 3: Multiple storage updates print("Example 3: Multiple storage updates") - + var storageSlot2 = LambdaStorageSlot() storageSlot2 = storageSlot2.key(Data([0x1D, 0x1E, 0x1F, 0x20])) storageSlot2 = storageSlot2.value(Data([0x21, 0x22, 0x23, 0x24])) - + var storageUpdate2 = LambdaStorageUpdate() storageUpdate2 = storageUpdate2.setStorageSlot(storageSlot2) - + let sstoreResponse3 = try await LambdaSStoreTransaction() .hookId(hookId) - .storageUpdates([storageUpdate, storageUpdate2]) // Multiple updates + .storageUpdates([storageUpdate, storageUpdate2]) // Multiple updates .execute(client) let sstoreReceipt3 = try await sstoreResponse3.getReceipt(client) - + print("Applied multiple storage updates: \(sstoreReceipt3.status)") - + // Example 4: Clear storage updates print("Example 4: Clearing storage updates") - + let sstoreResponse4 = try await LambdaSStoreTransaction() .hookId(hookId) - .clearStorageUpdates() // Clear all updates + .clearStorageUpdates() // Clear all updates .execute(client) let sstoreReceipt4 = try await sstoreResponse4.getReceipt(client) - + print("Cleared storage updates: \(sstoreReceipt4.status)") - + // Cleanup print("Cleaning up...") - + _ = try await AccountDeleteTransaction() .accountId(accountId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + _ = try await ContractDeleteTransaction() .contractId(contractId) .transferAccountId(env.operatorAccountId) .execute(client) .getReceipt(client) - + print("Cleanup completed") print("\nLambda SSTORE Example completed successfully!") } diff --git a/Examples/TransferWithHooks/main.swift b/Examples/TransferWithHooks/main.swift index 25c1637a..1aa8ab4e 100644 --- a/Examples/TransferWithHooks/main.swift +++ b/Examples/TransferWithHooks/main.swift @@ -4,267 +4,239 @@ import Foundation import Hiero /// Example demonstrating how to use hooks with transfer transactions. -/// +/// /// This example shows how to: -/// 1. Create accounts -/// 2. Create a hook call -/// 3. Execute transfers with hooks -/// 4. Handle different types of hook calls +/// 1. Set up prerequisites - create tokens and NFTs +/// 2. Demonstrate TransferTransaction API with hooks +/// 3. Execute different types of transfers with hooks @main struct TransferWithHooksExample { static func main() async throws { // Initialize the client let client = Client.forTestnet() - + // Create operator account let operatorKey = PrivateKey.generateEd25519() - let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID - + let operatorId = try AccountId.fromString("0.0.1234") // Replace with your account ID + client.setOperator(operatorId, operatorKey) + + print("Transfer Transaction Hooks Example Start!") + + /* + * Step 1: Set up prerequisites - create tokens and NFTs + */ + print("Setting up prerequisites...") + + // Create hook contract bytecode (simplified for Swift example) + let hookBytecode = Data([ + 0x60, 0x80, 0x60, 0x40, 0x52, 0x34, 0x80, 0x15, 0x61, 0x00, 0x10, 0x57, 0x60, 0x00, 0x80, 0xfd, + 0x5b, 0x50, 0x60, 0x04, 0x36, 0x10, 0x61, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, + 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, + 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35 + ]) + + let hookContractResponse = try await ContractCreateTransaction() + .adminKey(.single(operatorKey.publicKey)) + .gas(1_000_000) + .bytecode(hookBytecode) + .execute(client) + + let hookContractReceipt = try await hookContractResponse.getReceipt(client) + guard let hookContractId = hookContractReceipt.contractId else { + print("Failed to create hook contract!") + return + } + + print("Created hook contract: \(hookContractId)") + + // Create hook details + var evmHookSpec = EvmHookSpec() + evmHookSpec.contractId = hookContractId - print("Transfer with Hooks Example") - print("==============================") - - // Create test accounts - print("Creating test accounts...") + let lambdaHook = LambdaEvmHook(spec: evmHookSpec) + let hookDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaHook + ) + + // Create sender account let senderKey = PrivateKey.generateEd25519() let senderResponse = try await AccountCreateTransaction() .keyWithoutAlias(.single(senderKey.publicKey)) .initialBalance(10) + .addHook(hookDetails) .execute(client) - let senderReceipt = try await senderResponse.getReceipt(client) - let senderAccountId = senderReceipt.accountId! + let senderReceipt = try await senderResponse.getReceipt(client) + guard let senderAccountId = senderReceipt.accountId else { + print("Failed to create sender account!") + return + } + + print("Created sender account: \(senderAccountId)") + + // Create receiver account let receiverKey = PrivateKey.generateEd25519() let receiverResponse = try await AccountCreateTransaction() .keyWithoutAlias(.single(receiverKey.publicKey)) - .initialBalance(5) + .maxAutomaticTokenAssociations(100) + .initialBalance(10) + .addHook(hookDetails) .execute(client) - let receiverReceipt = try await receiverResponse.getReceipt(client) - let receiverAccountId = receiverReceipt.accountId! - print("Created sender account: \(senderAccountId)") + let receiverReceipt = try await receiverResponse.getReceipt(client) + guard let receiverAccountId = receiverReceipt.accountId else { + print("Failed to create receiver account!") + return + } + print("Created receiver account: \(receiverAccountId)") - - // Example 1: HBAR transfer with pre-transaction hook - print("Example 1: HBAR transfer with pre-transaction hook") - - var preTxHook = HookCall() - preTxHook = preTxHook.hookId(1) - var evmHookCall1 = EvmHookCall() - evmHookCall1 = evmHookCall1.data(Data([0x01, 0x02, 0x03])) // Hook call data - evmHookCall1 = evmHookCall1.gasLimit(100000) // Gas limit for hook execution - preTxHook = preTxHook.evmHookCall(evmHookCall1) - - let hbarTransferTx = TransferTransaction() - .hbarTransferWithPreTxHook(senderAccountId, 2, preTxHook) - - let hbarResponse = try await hbarTransferTx.execute(client) - let hbarReceipt = try await hbarResponse.getReceipt(client) - - print("HBAR transfer with hook completed: \(hbarReceipt.status)") - - // Example 2: HBAR transfer with pre-post-transaction hook - print("Example 2: HBAR transfer with pre-post-transaction hook") - - var prePostTxHook = HookCall() - prePostTxHook = prePostTxHook.hookId(2) - var evmHookCall2 = EvmHookCall() - evmHookCall2 = evmHookCall2.data(Data([0x04, 0x05, 0x06])) - evmHookCall2 = evmHookCall2.gasLimit(150000) - prePostTxHook = prePostTxHook.evmHookCall(evmHookCall2) - - let hbarPrePostTx = TransferTransaction() - .hbarTransferWithPrePostTxHook(senderAccountId, 1, prePostTxHook) - - let hbarPrePostResponse = try await hbarPrePostTx.execute(client) - let hbarPrePostReceipt = try await hbarPrePostResponse.getReceipt(client) - - print("HBAR transfer with pre-post hook completed: \(hbarPrePostReceipt.status)") - - // Example 3: Create a token and transfer with hooks - print("Example 3: Token transfer with hooks") - - let tokenId = try await TokenCreateTransaction() - .name("Hook Test Token") - .symbol("HTT") + + // Create fungible token + print("Creating fungible token...") + let fungibleTokenResponse = try await TokenCreateTransaction() + .name("Example Fungible Token") + .symbol("EFT") + .tokenType(.fungibleCommon) .decimals(2) - .initialSupply(1000) - .treasuryAccountId(operatorId) - .adminKey(.single(operatorKey.publicKey)) - .execute(client) - .getReceipt(client) - .tokenId! - - print("Created token: \(tokenId)") - - // Associate accounts with the token - _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) + .initialSupply(10000) + .treasuryAccountId(senderAccountId) + .adminKey(.single(senderKey.publicKey)) + .supplyKey(.single(senderKey.publicKey)) .execute(client) - .getReceipt(client) - - _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) - .execute(client) - .getReceipt(client) - - print("Associated accounts with token") - - // Transfer tokens with hook - var tokenHook = HookCall() - tokenHook = tokenHook.hookId(3) - var evmHookCall3 = EvmHookCall() - evmHookCall3 = evmHookCall3.data(Data([0x07, 0x08, 0x09])) - evmHookCall3 = evmHookCall3.gasLimit(200000) - tokenHook = tokenHook.evmHookCall(evmHookCall3) - - let tokenTransferTx = TransferTransaction() - .tokenTransferWithPreTxHook(tokenId, senderAccountId, 100, tokenHook) - - let tokenResponse = try await tokenTransferTx.execute(client) - let tokenReceipt = try await tokenResponse.getReceipt(client) - - print("Token transfer with hook completed: \(tokenReceipt.status)") - - // Example 4: NFT transfer with hooks - print("Example 4: NFT transfer with hooks") - - let nftTokenId = try await TokenCreateTransaction() - .name("Hook Test NFT") - .symbol("HTNFT") + + let fungibleTokenReceipt = try await fungibleTokenResponse.getReceipt(client) + guard let fungibleTokenId = fungibleTokenReceipt.tokenId else { + print("Failed to create fungible token!") + return + } + + print("Created fungible token with ID: \(fungibleTokenId)") + + // Create NFT token + print("Creating NFT token...") + let nftTokenResponse = try await TokenCreateTransaction() + .name("Example NFT Token") + .symbol("ENT") .tokenType(.nonFungibleUnique) - .treasuryAccountId(operatorId) - .adminKey(.single(operatorKey.publicKey)) - .supplyKey(.single(operatorKey.publicKey)) + .treasuryAccountId(senderAccountId) + .adminKey(.single(senderKey.publicKey)) + .supplyKey(.single(senderKey.publicKey)) .execute(client) - .getReceipt(client) - .tokenId! - - print("Created NFT token: \(nftTokenId)") - - // Associate accounts with the NFT token - _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [nftTokenId]) - .execute(client) - .getReceipt(client) - - _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [nftTokenId]) - .execute(client) - .getReceipt(client) - - // Mint an NFT - let nftSerial = try await TokenMintTransaction(tokenId: nftTokenId) - .metadata([Data("Hook Test NFT Metadata".utf8)]) - .execute(client) - .getReceipt(client) - .serials?.first! - - print("Minted NFT with serial: \(nftSerial!)") - - // Transfer NFT to sender first - _ = try await TransferTransaction() - .nftTransfer(NftId(tokenId: nftTokenId, serial: nftSerial!), operatorId, senderAccountId) - .execute(client) - .getReceipt(client) - - // Transfer NFT with sender hook - var senderHook = HookCall() - senderHook = senderHook.hookId(4) - var evmHookCall4 = EvmHookCall() - evmHookCall4 = evmHookCall4.data(Data([0x0A, 0x0B, 0x0C])) - evmHookCall4 = evmHookCall4.gasLimit(250000) - senderHook = senderHook.evmHookCall(evmHookCall4) - - let nftTransferTx = TransferTransaction() - .nftTransferWithSenderHooks( - NftId(tokenId: nftTokenId, serial: nftSerial!), - senderAccountId, - receiverAccountId, - preTxSenderHook: senderHook - ) - - let nftResponse = try await nftTransferTx.execute(client) - let nftReceipt = try await nftResponse.getReceipt(client) - - print("NFT transfer with sender hook completed: \(nftReceipt.status)") - - // Example 5: NFT transfer with receiver hook - print("Example 5: NFT transfer with receiver hook") - - var receiverHook = HookCall() - receiverHook = receiverHook.hookId(5) - var evmHookCall5 = EvmHookCall() - evmHookCall5 = evmHookCall5.data(Data([0x0D, 0x0E, 0x0F])) - evmHookCall5 = evmHookCall5.gasLimit(300000) - receiverHook = receiverHook.evmHookCall(evmHookCall5) - - let nftReceiverTx = TransferTransaction() - .nftTransferWithReceiverHooks( - NftId(tokenId: nftTokenId, serial: nftSerial!), - receiverAccountId, - senderAccountId, - preTxReceiverHook: receiverHook - ) - - let nftReceiverResponse = try await nftReceiverTx.execute(client) - let nftReceiverReceipt = try await nftReceiverResponse.getReceipt(client) - - print("NFT transfer with receiver hook completed: \(nftReceiverReceipt.status)") - - // Example 6: NFT transfer with all hooks - print("Example 6: NFT transfer with all hooks") - - var allSenderHook = HookCall() - allSenderHook = allSenderHook.hookId(6) - var evmHookCall6 = EvmHookCall() - evmHookCall6 = evmHookCall6.data(Data([0x10, 0x11, 0x12])) - evmHookCall6 = evmHookCall6.gasLimit(350000) - allSenderHook = allSenderHook.evmHookCall(evmHookCall6) - - var allReceiverHook = HookCall() - allReceiverHook = allReceiverHook.hookId(7) - var evmHookCall7 = EvmHookCall() - evmHookCall7 = evmHookCall7.data(Data([0x13, 0x14, 0x15])) - evmHookCall7 = evmHookCall7.gasLimit(400000) - allReceiverHook = allReceiverHook.evmHookCall(evmHookCall7) - - let nftAllHooksTx = TransferTransaction() - .nftTransferWithAllHooks( - NftId(tokenId: nftTokenId, serial: nftSerial!), - senderAccountId, - receiverAccountId, - preTxSenderHook: allSenderHook, - preTxReceiverHook: allReceiverHook - ) - - let nftAllHooksResponse = try await nftAllHooksTx.execute(client) - let nftAllHooksReceipt = try await nftAllHooksResponse.getReceipt(client) - - print("NFT transfer with all hooks completed: \(nftAllHooksReceipt.status)") - - // Cleanup - print("Cleaning up...") - - _ = try await AccountDeleteTransaction() - .accountId(senderAccountId) - .transferAccountId(operatorId) + + let nftTokenReceipt = try await nftTokenResponse.getReceipt(client) + guard let nftTokenId = nftTokenReceipt.tokenId else { + print("Failed to create NFT token!") + return + } + + print("Created NFT token with ID: \(nftTokenId)") + + // Mint NFT + print("Minting NFT...") + let nftMetadata = Data("Example NFT Metadata".utf8) + let mintResponse = try await TokenMintTransaction() + .tokenId(nftTokenId) + .metadata([nftMetadata]) .execute(client) - .getReceipt(client) - - _ = try await AccountDeleteTransaction() - .accountId(receiverAccountId) - .transferAccountId(operatorId) + + let mintReceipt = try await mintResponse.getReceipt(client) + guard let serialNumber = mintReceipt.serials?.first else { + print("Failed to mint NFT!") + return + } + + let nftId = NftId(tokenId: nftTokenId, serial: serialNumber) + print("Minted NFT with ID: \(nftId)") + + /* + * Step 2: Demonstrate TransferTransaction API with hooks (demonstration only) + */ + print("\n=== TransferTransaction with Hooks API Demonstration ===") + + // Create different hooks for different transfer types (for demonstration) + print("Creating hook call objects (demonstration)...") + + // HBAR transfer with pre-tx allowance hook + var hbarEvmHookCall = EvmHookCall() + hbarEvmHookCall.data = Data([0x01, 0x02]) + hbarEvmHookCall.gasLimit = 20000 + + let hbarHook = FungibleHookCall( + hookCall: HookCall(hookId: 1, evmHookCall: hbarEvmHookCall), + hookType: .preTxAllowanceHook + ) + + // NFT sender hook (pre-hook) + var nftSenderEvmHookCall = EvmHookCall() + nftSenderEvmHookCall.data = Data([0x03, 0x04]) + nftSenderEvmHookCall.gasLimit = 20000 + + let nftSenderHook = NftHookCall( + hookCall: HookCall(hookId: 1, evmHookCall: nftSenderEvmHookCall), + hookType: .preHook + ) + + // NFT receiver hook (pre-hook) + var nftReceiverEvmHookCall = EvmHookCall() + nftReceiverEvmHookCall.data = Data([0x05, 0x06]) + nftReceiverEvmHookCall.gasLimit = 20000 + + let nftReceiverHook = NftHookCall( + hookCall: HookCall(hookId: 1, evmHookCall: nftReceiverEvmHookCall), + hookType: .preHook + ) + + // Fungible token transfer with pre-post allowance hook + var fungibleTokenEvmHookCall = EvmHookCall() + fungibleTokenEvmHookCall.data = Data([0x07, 0x08]) + fungibleTokenEvmHookCall.gasLimit = 20000 + + let fungibleTokenHook = FungibleHookCall( + hookCall: HookCall(hookId: 1, evmHookCall: fungibleTokenEvmHookCall), + hookType: .prePostTxAllowanceHook + ) + + // Build separate TransferTransactions with hooks (demonstration) + print("Building separate TransferTransactions with hooks...") + + // Transaction 1: HBAR transfers with hook + print("\n1. Building HBAR TransferTransaction with hook...") + let hbarTransferResponse = try await TransferTransaction() + .hbarTransferWithHook(senderAccountId, Hbar(-1), hbarHook) + .hbarTransfer(receiverAccountId, Hbar(1)) .execute(client) - .getReceipt(client) - - _ = try await TokenDeleteTransaction(tokenId: tokenId) + + let hbarTransferReceipt = try await hbarTransferResponse.getReceipt(client) + print("HBAR transfer completed with status: \(hbarTransferReceipt.status)") + + // Transaction 2: NFT transfer with sender and receiver hooks + print("\n2. Building NFT TransferTransaction with hooks...") + let nftTransferResponse = try await TransferTransaction() + .nftTransferWithHooks(nftId, senderAccountId, receiverAccountId, nftSenderHook, nftReceiverHook) .execute(client) - .getReceipt(client) - - _ = try await TokenDeleteTransaction(tokenId: nftTokenId) + + let nftTransferReceipt = try await nftTransferResponse.getReceipt(client) + print("NFT transfer completed with status: \(nftTransferReceipt.status)") + + // Transaction 3: Fungible token transfers with hook + print("\n3. Building Fungible Token TransferTransaction with hook...") + let fungibleTransferResponse = try await TransferTransaction() + .tokenTransferWithHook(fungibleTokenId, senderAccountId, -1000, fungibleTokenHook) + .tokenTransfer(fungibleTokenId, receiverAccountId, 1000) .execute(client) - .getReceipt(client) - - print("Cleanup completed") - print("Transfer with Hooks Example completed successfully!") + + let fungibleTransferReceipt = try await fungibleTransferResponse.getReceipt(client) + print("Fungible token transfer completed with status: \(fungibleTransferReceipt.status)") + + print("\nAll TransferTransactions executed successfully with the following hook calls:") + print(" - Transaction 1: HBAR transfer with pre-tx allowance hook") + print(" - Transaction 2: NFT transfer with sender and receiver hooks") + print(" - Transaction 3: Fungible token transfer with pre-post allowance hook") + + print("Transfer Transaction Hooks Example Complete!") } -} +} \ No newline at end of file diff --git a/Sources/Hiero/Hooks/EvmHookSpec.swift b/Sources/Hiero/Hooks/EvmHookSpec.swift index bd36ff0c..ebe28d30 100644 --- a/Sources/Hiero/Hooks/EvmHookSpec.swift +++ b/Sources/Hiero/Hooks/EvmHookSpec.swift @@ -37,7 +37,7 @@ extension EvmHookSpec: TryProtobufCodable { internal func toProtobuf() -> Protobuf { .with { proto in if let id = contractId { - proto.contractID = id.toProtobuf() + proto.bytecodeSource = .contractID(id.toProtobuf()) } } } diff --git a/Sources/Hiero/Hooks/FungibleHookCall.swift b/Sources/Hiero/Hooks/FungibleHookCall.swift new file mode 100644 index 00000000..a24bbcfa --- /dev/null +++ b/Sources/Hiero/Hooks/FungibleHookCall.swift @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Specifies a call to a hook from within a transaction that interacts with fungible tokens (HBAR included). +public struct FungibleHookCall { + /// The underlying hook call. + public var hookCall: HookCall + + /// The type of the fungible hook to call. + public var hookType: FungibleHookType + + public init(hookCall: HookCall = HookCall(), hookType: FungibleHookType = .uninitialized) { + self.hookCall = hookCall + self.hookType = hookType + } + + /// Set the underlying hook call. + @discardableResult + public mutating func hookCall(_ hookCall: HookCall) -> Self { + self.hookCall = hookCall + return self + } + + /// Set the type of the fungible hook to call. + @discardableResult + public mutating func hookType(_ hookType: FungibleHookType) -> Self { + self.hookType = hookType + return self + } + + /// Set the full hook ID. + @discardableResult + public mutating func fullHookId(_ hookId: HookId) -> Self { + self.hookCall.fullHookId(hookId) + return self + } + + /// Set the hook ID. + @discardableResult + public mutating func hookId(_ hookId: Int64) -> Self { + self.hookCall.hookId(hookId) + return self + } + + /// Set the EVM hook call. + @discardableResult + public mutating func evmHookCall(_ evmHookCall: EvmHookCall) -> Self { + self.hookCall.evmHookCall(evmHookCall) + return self + } +} + +extension FungibleHookCall: TryProtobufCodable { + internal typealias Protobuf = Proto_HookCall + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.hookCall = try HookCall(protobuf: proto) + // Note: The hook type is not stored in the protobuf, so we default to uninitialized + // The caller should set the appropriate hook type based on context + self.hookType = .uninitialized + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + return hookCall.toProtobuf() + } +} + +extension FungibleHookCall: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + try hookCall.validateChecksums(on: ledgerId) + } +} diff --git a/Sources/Hiero/Hooks/FungibleHookType.swift b/Sources/Hiero/Hooks/FungibleHookType.swift new file mode 100644 index 00000000..9b46e32a --- /dev/null +++ b/Sources/Hiero/Hooks/FungibleHookType.swift @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +/// Enumeration specifying the different types of hooks for fungible tokens (including HBAR). +public enum FungibleHookType: CaseIterable { + /// Execute the allowance hook before the transaction. + case preTxAllowanceHook + + /// Execute the allowance hook before and after the transaction. + case prePostTxAllowanceHook + + /// Hook type not set. + case uninitialized +} + +extension FungibleHookType: CustomStringConvertible { + public var description: String { + switch self { + case .preTxAllowanceHook: + return "PRE_TX_ALLOWANCE_HOOK" + case .prePostTxAllowanceHook: + return "PRE_POST_TX_ALLOWANCE_HOOK" + case .uninitialized: + return "UNINITIALIZED" + } + } +} + +extension FungibleHookType: Equatable { + public static func == (lhs: FungibleHookType, rhs: FungibleHookType) -> Bool { + switch (lhs, rhs) { + case (.preTxAllowanceHook, .preTxAllowanceHook), + (.prePostTxAllowanceHook, .prePostTxAllowanceHook), + (.uninitialized, .uninitialized): + return true + default: + return false + } + } +} + +extension FungibleHookType: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case .preTxAllowanceHook: + hasher.combine(0) + case .prePostTxAllowanceHook: + hasher.combine(1) + case .uninitialized: + hasher.combine(2) + } + } +} diff --git a/Sources/Hiero/Hooks/NftHookCall.swift b/Sources/Hiero/Hooks/NftHookCall.swift new file mode 100644 index 00000000..5570eb88 --- /dev/null +++ b/Sources/Hiero/Hooks/NftHookCall.swift @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs + +/// Specifies a call to a hook from within a transaction that interacts with NFTs. +public struct NftHookCall { + /// The underlying hook call. + public var hookCall: HookCall + + /// The type of the NFT hook to call. + public var hookType: NftHookType + + public init(hookCall: HookCall = HookCall(), hookType: NftHookType = .uninitialized) { + self.hookCall = hookCall + self.hookType = hookType + } + + /// Set the underlying hook call. + @discardableResult + public mutating func hookCall(_ hookCall: HookCall) -> Self { + self.hookCall = hookCall + return self + } + + /// Set the type of the NFT hook to call. + @discardableResult + public mutating func hookType(_ hookType: NftHookType) -> Self { + self.hookType = hookType + return self + } + + /// Set the full hook ID. + @discardableResult + public mutating func fullHookId(_ hookId: HookId) -> Self { + self.hookCall.fullHookId(hookId) + return self + } + + /// Set the hook ID. + @discardableResult + public mutating func hookId(_ hookId: Int64) -> Self { + self.hookCall.hookId(hookId) + return self + } + + /// Set the EVM hook call. + @discardableResult + public mutating func evmHookCall(_ evmHookCall: EvmHookCall) -> Self { + self.hookCall.evmHookCall(evmHookCall) + return self + } +} + +extension NftHookCall: TryProtobufCodable { + internal typealias Protobuf = Proto_HookCall + + /// Construct from protobuf. + internal init(protobuf proto: Protobuf) throws { + self.hookCall = try HookCall(protobuf: proto) + // Note: The hook type is not stored in the protobuf, so we default to uninitialized + // The caller should set the appropriate hook type based on context + self.hookType = .uninitialized + } + + /// Convert to protobuf. + internal func toProtobuf() -> Protobuf { + return hookCall.toProtobuf() + } +} + +extension NftHookCall: ValidateChecksums { + internal func validateChecksums(on ledgerId: LedgerId) throws { + try hookCall.validateChecksums(on: ledgerId) + } +} diff --git a/Sources/Hiero/Hooks/NftHookType.swift b/Sources/Hiero/Hooks/NftHookType.swift new file mode 100644 index 00000000..97aae5ae --- /dev/null +++ b/Sources/Hiero/Hooks/NftHookType.swift @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation + +/// Enumeration specifying the different types of hooks for NFTs. +public enum NftHookType: CaseIterable { + /// Execute the hook before the transaction. + case preHook + + /// Execute the hook before and after the transaction. + case prePostHook + + /// Hook type uninitialized. + case uninitialized +} + +extension NftHookType: CustomStringConvertible { + public var description: String { + switch self { + case .preHook: + return "PRE_HOOK" + case .prePostHook: + return "PRE_POST_HOOK" + case .uninitialized: + return "UNINITIALIZED" + } + } +} + +extension NftHookType: Equatable { + public static func == (lhs: NftHookType, rhs: NftHookType) -> Bool { + switch (lhs, rhs) { + case (.preHook, .preHook), + (.prePostHook, .prePostHook), + (.uninitialized, .uninitialized): + return true + default: + return false + } + } +} + +extension NftHookType: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case .preHook: + hasher.combine(0) + case .prePostHook: + hasher.combine(1) + case .uninitialized: + hasher.combine(2) + } + } +} diff --git a/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift b/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift index faebe254..5756d826 100644 --- a/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift +++ b/Sources/Hiero/Token/AbstractTokenTransferTransaction.swift @@ -12,13 +12,11 @@ public class AbstractTokenTransferTransaction: Transaction { let accountId: AccountId var amount: Int64 let isApproval: Bool - var preTxAllowanceHook: HookCall? - var prePostTxAllowanceHook: HookCall? + var hookCall: FungibleHookCall? internal func validateChecksums(on ledgerId: LedgerId) throws { try accountId.validateChecksums(on: ledgerId) - try preTxAllowanceHook?.validateChecksums(on: ledgerId) - try prePostTxAllowanceHook?.validateChecksums(on: ledgerId) + try hookCall?.hookCall.validateChecksums(on: ledgerId) } } @@ -40,18 +38,14 @@ public class AbstractTokenTransferTransaction: Transaction { let receiverAccountId: AccountId let serial: UInt64 let isApproval: Bool - var preTxSenderAllowanceHook: HookCall? - var prePostTxSenderAllowanceHook: HookCall? - var preTxReceiverAllowanceHook: HookCall? - var prePostTxReceiverAllowanceHook: HookCall? + var senderHookCall: NftHookCall? + var receiverHookCall: NftHookCall? internal func validateChecksums(on ledgerId: LedgerId) throws { try senderAccountId.validateChecksums(on: ledgerId) try receiverAccountId.validateChecksums(on: ledgerId) - try preTxSenderAllowanceHook?.validateChecksums(on: ledgerId) - try prePostTxSenderAllowanceHook?.validateChecksums(on: ledgerId) - try preTxReceiverAllowanceHook?.validateChecksums(on: ledgerId) - try prePostTxReceiverAllowanceHook?.validateChecksums(on: ledgerId) + try senderHookCall?.validateChecksums(on: ledgerId) + try receiverHookCall?.validateChecksums(on: ledgerId) } } @@ -159,6 +153,14 @@ public class AbstractTokenTransferTransaction: Transaction { doTokenTransferWithDecimals(tokenId, accountId, amount, true, expectedDecimals) } + /// Add a fungible token transfer with a hook to be submitted as part of this TransferTransaction. + @discardableResult + public func tokenTransferWithHook( + _ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: FungibleHookCall + ) -> Self { + doTokenTransferWithHook(tokenId, accountId, amount, false, hookCall) + } + /// Add a non-approved nft transfer to the transaction. @discardableResult public func nftTransfer(_ nftId: NftId, _ senderAccountId: AccountId, _ receiverAccountId: AccountId) @@ -175,13 +177,27 @@ public class AbstractTokenTransferTransaction: Transaction { doNftTransfer(nftId, senderAccountId, receiverAccountId, true) } + /// Add an NFT transfer with separate sender and receiver hook calls to this TransferTransaction. + @discardableResult + public func nftTransferWithHooks( + _ nftId: NftId, + _ senderAccountId: AccountId, + _ receiverAccountId: AccountId, + _ senderHookCall: NftHookCall, + _ receiverHookCall: NftHookCall + ) -> Self { + doNftTransferWithHooks( + nftId, senderAccountId, receiverAccountId, false, senderHookCall, receiverHookCall) + } + private func doTokenTransfer( _ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ approved: Bool ) -> Self { - let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: nil, prePostTxAllowanceHook: nil) + let transfer = Transfer( + accountId: accountId, amount: amount, isApproval: approved, hookCall: nil) if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId }) { @@ -215,7 +231,8 @@ public class AbstractTokenTransferTransaction: Transaction { _ approved: Bool, _ expectedDecimals: UInt32 ) -> Self { - let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: nil, prePostTxAllowanceHook: nil) + let transfer = Transfer( + accountId: accountId, amount: amount, isApproval: approved, hookCall: nil) if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId }) { @@ -251,39 +268,15 @@ public class AbstractTokenTransferTransaction: Transaction { return self } - /// Add a non-approved token transfer with a pre-transaction allowance hook to the transaction. - @discardableResult - public func tokenTransferWithPreTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { - doTokenTransferWithHook(tokenId, accountId, amount, false, hookCall, nil) - } - - /// Add a non-approved token transfer with a pre-post-transaction allowance hook to the transaction. - @discardableResult - public func tokenTransferWithPrePostTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { - doTokenTransferWithHook(tokenId, accountId, amount, false, nil, hookCall) - } - - /// Add an approved token transfer with a pre-transaction allowance hook to the transaction. - @discardableResult - public func approvedTokenTransferWithPreTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { - doTokenTransferWithHook(tokenId, accountId, amount, true, hookCall, nil) - } - - /// Add an approved token transfer with a pre-post-transaction allowance hook to the transaction. - @discardableResult - public func approvedTokenTransferWithPrePostTxHook(_ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ hookCall: HookCall) -> Self { - doTokenTransferWithHook(tokenId, accountId, amount, true, nil, hookCall) - } - private func doTokenTransferWithHook( _ tokenId: TokenId, _ accountId: AccountId, _ amount: Int64, _ approved: Bool, - _ preTxHook: HookCall?, - _ prePostTxHook: HookCall? + _ hookCall: FungibleHookCall ) -> Self { - let transfer = Transfer(accountId: accountId, amount: amount, isApproval: approved, preTxAllowanceHook: preTxHook, prePostTxAllowanceHook: prePostTxHook) + let transfer = Transfer( + accountId: accountId, amount: amount, isApproval: approved, hookCall: hookCall) if let firstIndex = tokenTransfersInner.firstIndex(where: { (tokenTransfer) in tokenTransfer.tokenId == tokenId }) { @@ -293,8 +286,7 @@ public class AbstractTokenTransferTransaction: Transaction { $0.accountId == accountId && $0.isApproval == approved }) { tokenTransfersInner[firstIndex].transfers[existingTransferIndex].amount += amount - tokenTransfersInner[firstIndex].transfers[existingTransferIndex].preTxAllowanceHook = preTxHook - tokenTransfersInner[firstIndex].transfers[existingTransferIndex].prePostTxAllowanceHook = prePostTxHook + tokenTransfersInner[firstIndex].transfers[existingTransferIndex].hookCall = hookCall } else { tokenTransfersInner[firstIndex].expectedDecimals = nil tokenTransfersInner[firstIndex].transfers.append(transfer) @@ -323,10 +315,8 @@ public class AbstractTokenTransferTransaction: Transaction { receiverAccountId: receiverAccountId, serial: nftId.serial, isApproval: approved, - preTxSenderAllowanceHook: nil, - prePostTxSenderAllowanceHook: nil, - preTxReceiverAllowanceHook: nil, - prePostTxReceiverAllowanceHook: nil + senderHookCall: nil, + receiverHookCall: nil ) if let index = tokenTransfersInner.firstIndex(where: { transfer in transfer.tokenId == nftId.tokenId }) { @@ -347,101 +337,21 @@ public class AbstractTokenTransferTransaction: Transaction { return self } - /// Add a non-approved NFT transfer with sender allowance hooks to the transaction. - @discardableResult - public func nftTransferWithSenderHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxSenderHook: HookCall? = nil, - prePostTxSenderHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, preTxSenderHook, prePostTxSenderHook, nil, nil) - } - - /// Add a non-approved NFT transfer with receiver allowance hooks to the transaction. - @discardableResult - public func nftTransferWithReceiverHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxReceiverHook: HookCall? = nil, - prePostTxReceiverHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, nil, nil, preTxReceiverHook, prePostTxReceiverHook) - } - - /// Add a non-approved NFT transfer with both sender and receiver allowance hooks to the transaction. - @discardableResult - public func nftTransferWithAllHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxSenderHook: HookCall? = nil, - prePostTxSenderHook: HookCall? = nil, - preTxReceiverHook: HookCall? = nil, - prePostTxReceiverHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, false, preTxSenderHook, prePostTxSenderHook, preTxReceiverHook, prePostTxReceiverHook) - } - - /// Add an approved NFT transfer with sender allowance hooks to the transaction. - @discardableResult - public func approvedNftTransferWithSenderHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxSenderHook: HookCall? = nil, - prePostTxSenderHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, preTxSenderHook, prePostTxSenderHook, nil, nil) - } - - /// Add an approved NFT transfer with receiver allowance hooks to the transaction. - @discardableResult - public func approvedNftTransferWithReceiverHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxReceiverHook: HookCall? = nil, - prePostTxReceiverHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, nil, nil, preTxReceiverHook, prePostTxReceiverHook) - } - - /// Add an approved NFT transfer with both sender and receiver allowance hooks to the transaction. - @discardableResult - public func approvedNftTransferWithAllHooks( - _ nftId: NftId, - _ senderAccountId: AccountId, - _ receiverAccountId: AccountId, - preTxSenderHook: HookCall? = nil, - prePostTxSenderHook: HookCall? = nil, - preTxReceiverHook: HookCall? = nil, - prePostTxReceiverHook: HookCall? = nil - ) -> Self { - doNftTransferWithHooks(nftId, senderAccountId, receiverAccountId, true, preTxSenderHook, prePostTxSenderHook, preTxReceiverHook, prePostTxReceiverHook) - } - private func doNftTransferWithHooks( _ nftId: NftId, _ senderAccountId: AccountId, _ receiverAccountId: AccountId, _ approved: Bool, - _ preTxSenderHook: HookCall?, - _ prePostTxSenderHook: HookCall?, - _ preTxReceiverHook: HookCall?, - _ prePostTxReceiverHook: HookCall? + _ senderHookCall: NftHookCall, + _ receiverHookCall: NftHookCall ) -> Self { let transfer = NftTransfer( senderAccountId: senderAccountId, receiverAccountId: receiverAccountId, serial: nftId.serial, isApproval: approved, - preTxSenderAllowanceHook: preTxSenderHook, - prePostTxSenderAllowanceHook: prePostTxSenderHook, - preTxReceiverAllowanceHook: preTxReceiverHook, - prePostTxReceiverAllowanceHook: prePostTxReceiverHook + senderHookCall: senderHookCall, + receiverHookCall: receiverHookCall ) if let index = tokenTransfersInner.firstIndex(where: { transfer in transfer.tokenId == nftId.tokenId }) { @@ -512,16 +422,15 @@ extension AbstractTokenTransferTransaction.Transfer: TryProtobufCodable { accountId: try .fromProtobuf(proto.accountID), amount: proto.amount, isApproval: proto.isApproval, - preTxAllowanceHook: nil, - prePostTxAllowanceHook: nil + hookCall: nil ) - + // Handle hook calls switch proto.hookCall { case .preTxAllowanceHook(let hookCall): - self.preTxAllowanceHook = try HookCall(protobuf: hookCall) + self.hookCall = try FungibleHookCall(protobuf: hookCall) case .prePostTxAllowanceHook(let hookCall): - self.prePostTxAllowanceHook = try HookCall(protobuf: hookCall) + self.hookCall = try FungibleHookCall(protobuf: hookCall) case nil: break } @@ -532,12 +441,14 @@ extension AbstractTokenTransferTransaction.Transfer: TryProtobufCodable { proto.accountID = accountId.toProtobuf() proto.amount = amount proto.isApproval = isApproval - + // Handle hook calls - if let preTxHook = preTxAllowanceHook { - proto.hookCall = .preTxAllowanceHook(preTxHook.toProtobuf()) - } else if let prePostTxHook = prePostTxAllowanceHook { - proto.hookCall = .prePostTxAllowanceHook(prePostTxHook.toProtobuf()) + if let hookCall = hookCall { + if hookCall.hookType == .preTxAllowanceHook { + proto.hookCall = .preTxAllowanceHook(hookCall.toProtobuf()) + } else if hookCall.hookType == .prePostTxAllowanceHook { + proto.hookCall = .prePostTxAllowanceHook(hookCall.toProtobuf()) + } } } } @@ -578,28 +489,26 @@ extension AbstractTokenTransferTransaction.NftTransfer: TryProtobufCodable { receiverAccountId: try .fromProtobuf(proto.receiverAccountID), serial: UInt64(proto.serialNumber), isApproval: proto.isApproval, - preTxSenderAllowanceHook: nil, - prePostTxSenderAllowanceHook: nil, - preTxReceiverAllowanceHook: nil, - prePostTxReceiverAllowanceHook: nil + senderHookCall: nil, + receiverHookCall: nil ) - + // Handle sender allowance hook calls switch proto.senderAllowanceHookCall { case .preTxSenderAllowanceHook(let hookCall): - self.preTxSenderAllowanceHook = try HookCall(protobuf: hookCall) + self.senderHookCall = try NftHookCall(protobuf: hookCall) case .prePostTxSenderAllowanceHook(let hookCall): - self.prePostTxSenderAllowanceHook = try HookCall(protobuf: hookCall) + self.senderHookCall = try NftHookCall(protobuf: hookCall) case nil: break } - + // Handle receiver allowance hook calls switch proto.receiverAllowanceHookCall { case .preTxReceiverAllowanceHook(let hookCall): - self.preTxReceiverAllowanceHook = try HookCall(protobuf: hookCall) + self.receiverHookCall = try NftHookCall(protobuf: hookCall) case .prePostTxReceiverAllowanceHook(let hookCall): - self.prePostTxReceiverAllowanceHook = try HookCall(protobuf: hookCall) + self.receiverHookCall = try NftHookCall(protobuf: hookCall) case nil: break } @@ -611,19 +520,19 @@ extension AbstractTokenTransferTransaction.NftTransfer: TryProtobufCodable { proto.receiverAccountID = receiverAccountId.toProtobuf() proto.serialNumber = Int64(bitPattern: serial) proto.isApproval = isApproval - + // Handle sender allowance hook calls - if let preTxSenderHook = preTxSenderAllowanceHook { - proto.senderAllowanceHookCall = .preTxSenderAllowanceHook(preTxSenderHook.toProtobuf()) - } else if let prePostTxSenderHook = prePostTxSenderAllowanceHook { - proto.senderAllowanceHookCall = .prePostTxSenderAllowanceHook(prePostTxSenderHook.toProtobuf()) + if let senderHookCall = senderHookCall { + proto.senderAllowanceHookCall = .preTxSenderAllowanceHook(senderHookCall.toProtobuf()) + } else if let senderHookCall = senderHookCall { + proto.senderAllowanceHookCall = .prePostTxSenderAllowanceHook(senderHookCall.toProtobuf()) } - + // Handle receiver allowance hook calls - if let preTxReceiverHook = preTxReceiverAllowanceHook { - proto.receiverAllowanceHookCall = .preTxReceiverAllowanceHook(preTxReceiverHook.toProtobuf()) - } else if let prePostTxReceiverHook = prePostTxReceiverAllowanceHook { - proto.receiverAllowanceHookCall = .prePostTxReceiverAllowanceHook(prePostTxReceiverHook.toProtobuf()) + if let receiverHookCall = receiverHookCall { + proto.receiverAllowanceHookCall = .preTxReceiverAllowanceHook(receiverHookCall.toProtobuf()) + } else if let receiverHookCall = receiverHookCall { + proto.receiverAllowanceHookCall = .prePostTxReceiverAllowanceHook(receiverHookCall.toProtobuf()) } } } diff --git a/Sources/Hiero/TransferTransaction.swift b/Sources/Hiero/TransferTransaction.swift index c2bb81b3..a9ba9988 100644 --- a/Sources/Hiero/TransferTransaction.swift +++ b/Sources/Hiero/TransferTransaction.swift @@ -76,36 +76,17 @@ public final class TransferTransaction: AbstractTokenTransferTransaction { return self } - /// Add a non-approved hbar transfer with a pre-transaction allowance hook to the transaction. + /// Add an Hbar transfer with a hook to be submitted as part of this TransferTransaction. @discardableResult - public func hbarTransferWithPreTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { - doHbarTransferWithHook(accountId, amount.toTinybars(), false, hookCall, nil) - } - - /// Add a non-approved hbar transfer with a pre-post-transaction allowance hook to the transaction. - @discardableResult - public func hbarTransferWithPrePostTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { - doHbarTransferWithHook(accountId, amount.toTinybars(), false, nil, hookCall) - } - - /// Add an approved hbar transfer with a pre-transaction allowance hook to the transaction. - @discardableResult - public func approvedHbarTransferWithPreTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { - doHbarTransferWithHook(accountId, amount.toTinybars(), true, hookCall, nil) - } - - /// Add an approved hbar transfer with a pre-post-transaction allowance hook to the transaction. - @discardableResult - public func approvedHbarTransferWithPrePostTxHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: HookCall) -> Self { - doHbarTransferWithHook(accountId, amount.toTinybars(), true, nil, hookCall) + public func hbarTransferWithHook(_ accountId: AccountId, _ amount: Hbar, _ hookCall: FungibleHookCall) -> Self { + doHbarTransferWithHook(accountId, amount.toTinybars(), false, hookCall) } private func doHbarTransferWithHook( _ accountId: AccountId, _ amount: Int64, _ approved: Bool, - _ preTxHook: HookCall?, - _ prePostTxHook: HookCall? + _ hookCall: FungibleHookCall ) -> Self { for (index, transfer) in transfers.enumerated() where transfer.accountId == accountId && transfer.isApproval == approved { @@ -114,20 +95,19 @@ public final class TransferTransaction: AbstractTokenTransferTransaction { transfers.remove(at: index) } else { transfers[index].amount = newTinybars - transfers[index].preTxAllowanceHook = preTxHook - transfers[index].prePostTxAllowanceHook = prePostTxHook + transfers[index].hookCall = hookCall } return self } - transfers.append(Transfer( - accountId: accountId, - amount: amount, - isApproval: approved, - preTxAllowanceHook: preTxHook, - prePostTxAllowanceHook: prePostTxHook - )) + transfers.append( + Transfer( + accountId: accountId, + amount: amount, + isApproval: approved, + hookCall: hookCall + )) return self } diff --git a/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift b/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift index 3a28719f..7f450867 100644 --- a/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/basic_types.pb.swift @@ -1834,12 +1834,25 @@ public struct Proto_HookEntityId: Sendable { set {entityID = .accountID(newValue)} } + ///* + /// A contract using a hook. + public var contractID: Proto_ContractID { + get { + if case .contractID(let v)? = entityID {return v} + return Proto_ContractID() + } + set {entityID = .contractID(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_EntityID: Equatable, Sendable { ///* /// An account using a hook. case accountID(Proto_AccountID) + ///* + /// A contract using a hook. + case contractID(Proto_ContractID) } @@ -4657,6 +4670,7 @@ extension Proto_HookEntityId: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public static let protoMessageName: String = _protobuf_package + ".HookEntityId" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "account_id"), + 2: .standard(proto: "contract_id"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -4678,6 +4692,19 @@ extension Proto_HookEntityId: SwiftProtobuf.Message, SwiftProtobuf._MessageImple self.entityID = .accountID(v) } }() + case 2: try { + var v: Proto_ContractID? + var hadOneofValue = false + if let current = self.entityID { + hadOneofValue = true + if case .contractID(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.entityID = .contractID(v) + } + }() default: break } } @@ -4688,9 +4715,17 @@ extension Proto_HookEntityId: SwiftProtobuf.Message, SwiftProtobuf._MessageImple // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - try { if case .accountID(let v)? = self.entityID { + switch self.entityID { + case .accountID?: try { + guard case .accountID(let v)? = self.entityID else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() + }() + case .contractID?: try { + guard case .contractID(let v)? = self.entityID else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } try unknownFields.traverse(visitor: &visitor) } diff --git a/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift b/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift index 457b2655..509e5af8 100644 --- a/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/hook_types.pb.swift @@ -124,17 +124,6 @@ public struct Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: Sendable { /// The hook implementation. public var hook: Com_Hedera_Hapi_Node_Hooks_HookCreationDetails.OneOf_Hook? = nil - ///* - /// A hook programmed in EVM bytecode that does not require access to state - /// or interactions with external contracts. - public var pureEvmHook: Com_Hedera_Hapi_Node_Hooks_PureEvmHook { - get { - if case .pureEvmHook(let v)? = hook {return v} - return Com_Hedera_Hapi_Node_Hooks_PureEvmHook() - } - set {hook = .pureEvmHook(newValue)} - } - ///* /// A hook programmed in EVM bytecode that may access state or interact with /// external contracts. @@ -164,10 +153,6 @@ public struct Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: Sendable { ///* /// The hook implementation. public enum OneOf_Hook: Equatable, Sendable { - ///* - /// A hook programmed in EVM bytecode that does not require access to state - /// or interactions with external contracts. - case pureEvmHook(Com_Hedera_Hapi_Node_Hooks_PureEvmHook) ///* /// A hook programmed in EVM bytecode that may access state or interact with /// external contracts. @@ -180,31 +165,6 @@ public struct Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: Sendable { fileprivate var _adminKey: Proto_Key? = nil } -///* -/// Definition of a lambda EVM hook. -public struct Com_Hedera_Hapi_Node_Hooks_PureEvmHook: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - ///* - /// The specification for the hook. - public var spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec { - get {return _spec ?? Com_Hedera_Hapi_Node_Hooks_EvmHookSpec()} - set {_spec = newValue} - } - /// Returns true if `spec` has been explicitly set. - public var hasSpec: Bool {return self._spec != nil} - /// Clears the value of `spec`. Subsequent reads from it will return its default value. - public mutating func clearSpec() {self._spec = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _spec: Com_Hedera_Hapi_Node_Hooks_EvmHookSpec? = nil -} - ///* /// Definition of a lambda EVM hook. public struct Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook: Sendable { @@ -495,9 +455,8 @@ extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "extension_point"), 2: .standard(proto: "hook_id"), - 3: .standard(proto: "pure_evm_hook"), - 4: .standard(proto: "lambda_evm_hook"), - 5: .standard(proto: "admin_key"), + 3: .standard(proto: "lambda_evm_hook"), + 4: .standard(proto: "admin_key"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -509,19 +468,6 @@ extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, case 1: try { try decoder.decodeSingularEnumField(value: &self.extensionPoint) }() case 2: try { try decoder.decodeSingularInt64Field(value: &self.hookID) }() case 3: try { - var v: Com_Hedera_Hapi_Node_Hooks_PureEvmHook? - var hadOneofValue = false - if let current = self.hook { - hadOneofValue = true - if case .pureEvmHook(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.hook = .pureEvmHook(v) - } - }() - case 4: try { var v: Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook? var hadOneofValue = false if let current = self.hook { @@ -534,7 +480,7 @@ extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, self.hook = .lambdaEvmHook(v) } }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._adminKey) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._adminKey) }() default: break } } @@ -551,19 +497,11 @@ extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, if self.hookID != 0 { try visitor.visitSingularInt64Field(value: self.hookID, fieldNumber: 2) } - switch self.hook { - case .pureEvmHook?: try { - guard case .pureEvmHook(let v)? = self.hook else { preconditionFailure() } + try { if case .lambdaEvmHook(let v)? = self.hook { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .lambdaEvmHook?: try { - guard case .lambdaEvmHook(let v)? = self.hook else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case nil: break - } + } }() try { if let v = self._adminKey { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) } }() try unknownFields.traverse(visitor: &visitor) } @@ -578,42 +516,6 @@ extension Com_Hedera_Hapi_Node_Hooks_HookCreationDetails: SwiftProtobuf.Message, } } -extension Com_Hedera_Hapi_Node_Hooks_PureEvmHook: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".PureEvmHook" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "spec"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._spec) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._spec { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Com_Hedera_Hapi_Node_Hooks_PureEvmHook, rhs: Com_Hedera_Hapi_Node_Hooks_PureEvmHook) -> Bool { - if lhs._spec != rhs._spec {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - extension Com_Hedera_Hapi_Node_Hooks_LambdaEvmHook: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".LambdaEvmHook" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/Sources/HieroProtobufs/Generated/services/response_code.pb.swift b/Sources/HieroProtobufs/Generated/services/response_code.pb.swift index 1e93b190..aa047658 100644 --- a/Sources/HieroProtobufs/Generated/services/response_code.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/response_code.pb.swift @@ -1612,6 +1612,47 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { ///* /// The LambdaSStore tried to update too many storage slots in a single transaction. case tooManyLambdaStorageUpdates // = 513 + + ///* + /// A lambda mapping slot, storage key, or storage value failed to use the + /// minimal representation (i.e., no leading zeros). + case hookCreationBytesMustUseMinimalRepresentation // = 514 + + ///* + /// A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + case hookCreationBytesTooLong // = 515 + + ///* + /// A hook creation spec was not found. + case invalidHookCreationSpec // = 516 + + ///* + /// A hook extension point was empty. + case hookExtensionEmpty // = 517 + + ///* + /// A hook admin key was invalid. + case invalidHookAdminKey // = 518 + + ///* + /// The hook deletion requires the hook to have zero storage slots. + case hookDeletionRequiresZeroStorageSlots // = 519 + + ///* + /// Cannot set both a hook call and an approval on the same AccountAmount or NftTransfer message. + case cannotSetHooksAndApproval // = 520 + + ///* + /// The attempted operation is invalid until all the target entity's hooks have been deleted. + case transactionRequiresZeroHooks // = 521 + + ///* + /// The HookCall set in the transaction is invalid + case invalidHookCall // = 522 + + ///* + /// Hooks are not supported to be used in TokenAirdrop transactions + case hooksAreNotSupportedInAirdrops // = 523 case UNRECOGNIZED(Int) public init() { @@ -1994,6 +2035,16 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { case 511: self = .hookIsNotALambda case 512: self = .hookDeleted case 513: self = .tooManyLambdaStorageUpdates + case 514: self = .hookCreationBytesMustUseMinimalRepresentation + case 515: self = .hookCreationBytesTooLong + case 516: self = .invalidHookCreationSpec + case 517: self = .hookExtensionEmpty + case 518: self = .invalidHookAdminKey + case 519: self = .hookDeletionRequiresZeroStorageSlots + case 520: self = .cannotSetHooksAndApproval + case 521: self = .transactionRequiresZeroHooks + case 522: self = .invalidHookCall + case 523: self = .hooksAreNotSupportedInAirdrops default: self = .UNRECOGNIZED(rawValue) } } @@ -2374,6 +2425,16 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { case .hookIsNotALambda: return 511 case .hookDeleted: return 512 case .tooManyLambdaStorageUpdates: return 513 + case .hookCreationBytesMustUseMinimalRepresentation: return 514 + case .hookCreationBytesTooLong: return 515 + case .invalidHookCreationSpec: return 516 + case .hookExtensionEmpty: return 517 + case .invalidHookAdminKey: return 518 + case .hookDeletionRequiresZeroStorageSlots: return 519 + case .cannotSetHooksAndApproval: return 520 + case .transactionRequiresZeroHooks: return 521 + case .invalidHookCall: return 522 + case .hooksAreNotSupportedInAirdrops: return 523 case .UNRECOGNIZED(let i): return i } } @@ -2754,6 +2815,16 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { .hookIsNotALambda, .hookDeleted, .tooManyLambdaStorageUpdates, + .hookCreationBytesMustUseMinimalRepresentation, + .hookCreationBytesTooLong, + .invalidHookCreationSpec, + .hookExtensionEmpty, + .invalidHookAdminKey, + .hookDeletionRequiresZeroStorageSlots, + .cannotSetHooksAndApproval, + .transactionRequiresZeroHooks, + .invalidHookCall, + .hooksAreNotSupportedInAirdrops, ] } @@ -3136,5 +3207,15 @@ extension Proto_ResponseCodeEnum: SwiftProtobuf._ProtoNameProviding { 511: .same(proto: "HOOK_IS_NOT_A_LAMBDA"), 512: .same(proto: "HOOK_DELETED"), 513: .same(proto: "TOO_MANY_LAMBDA_STORAGE_UPDATES"), + 514: .same(proto: "HOOK_CREATION_BYTES_MUST_USE_MINIMAL_REPRESENTATION"), + 515: .same(proto: "HOOK_CREATION_BYTES_TOO_LONG"), + 516: .same(proto: "INVALID_HOOK_CREATION_SPEC"), + 517: .same(proto: "HOOK_EXTENSION_EMPTY"), + 518: .same(proto: "INVALID_HOOK_ADMIN_KEY"), + 519: .same(proto: "HOOK_DELETION_REQUIRES_ZERO_STORAGE_SLOTS"), + 520: .same(proto: "CANNOT_SET_HOOKS_AND_APPROVAL"), + 521: .same(proto: "TRANSACTION_REQUIRES_ZERO_HOOKS"), + 522: .same(proto: "INVALID_HOOK_CALL"), + 523: .same(proto: "HOOKS_ARE_NOT_SUPPORTED_IN_AIRDROPS"), ] } diff --git a/Sources/HieroProtobufs/Protos/services/basic_types.proto b/Sources/HieroProtobufs/Protos/services/basic_types.proto index a41d53ab..c71a2f65 100644 --- a/Sources/HieroProtobufs/Protos/services/basic_types.proto +++ b/Sources/HieroProtobufs/Protos/services/basic_types.proto @@ -462,6 +462,10 @@ message HookEntityId { * An account using a hook. */ AccountID account_id = 1; + /** + * A contract using a hook. + */ + ContractID contract_id = 2; } } diff --git a/Sources/HieroProtobufs/Protos/services/hook_types.proto b/Sources/HieroProtobufs/Protos/services/hook_types.proto index 3b52d52b..c1fcccec 100644 --- a/Sources/HieroProtobufs/Protos/services/hook_types.proto +++ b/Sources/HieroProtobufs/Protos/services/hook_types.proto @@ -59,16 +59,11 @@ message HookCreationDetails { * The hook implementation. */ oneof hook { - /** - * A hook programmed in EVM bytecode that does not require access to state - * or interactions with external contracts. - */ - PureEvmHook pure_evm_hook = 3; /** * A hook programmed in EVM bytecode that may access state or interact with * external contracts. */ - LambdaEvmHook lambda_evm_hook = 4; + LambdaEvmHook lambda_evm_hook = 3; } /** @@ -76,17 +71,7 @@ message HookCreationDetails { * applicable, as with a lambda EVM hook) perform transactions that customize * the hook. */ - proto.Key admin_key = 5; -} - -/** - * Definition of a lambda EVM hook. - */ -message PureEvmHook { - /** - * The specification for the hook. - */ - EvmHookSpec spec = 1; + proto.Key admin_key = 4; } /** diff --git a/Sources/HieroProtobufs/Protos/services/response_code.proto b/Sources/HieroProtobufs/Protos/services/response_code.proto index 9daa7cdf..2d3b8649 100644 --- a/Sources/HieroProtobufs/Protos/services/response_code.proto +++ b/Sources/HieroProtobufs/Protos/services/response_code.proto @@ -1842,4 +1842,54 @@ enum ResponseCodeEnum { * The LambdaSStore tried to update too many storage slots in a single transaction. */ TOO_MANY_LAMBDA_STORAGE_UPDATES = 513; + + /** + * A lambda mapping slot, storage key, or storage value failed to use the + * minimal representation (i.e., no leading zeros). + */ + HOOK_CREATION_BYTES_MUST_USE_MINIMAL_REPRESENTATION = 514; + + /** + * A lambda mapping slot, storage key, or storage value exceeded 32 bytes. + */ + HOOK_CREATION_BYTES_TOO_LONG = 515; + + /** + * A hook creation spec was not found. + */ + INVALID_HOOK_CREATION_SPEC = 516; + + /** + * A hook extension point was empty. + */ + HOOK_EXTENSION_EMPTY = 517; + + /** + * A hook admin key was invalid. + */ + INVALID_HOOK_ADMIN_KEY = 518; + + /** + * The hook deletion requires the hook to have zero storage slots. + */ + HOOK_DELETION_REQUIRES_ZERO_STORAGE_SLOTS = 519; + + /** + * Cannot set both a hook call and an approval on the same AccountAmount or NftTransfer message. + */ + CANNOT_SET_HOOKS_AND_APPROVAL = 520; + + /** + * The attempted operation is invalid until all the target entity's hooks have been deleted. + */ + TRANSACTION_REQUIRES_ZERO_HOOKS = 521; + + /** + * The HookCall set in the transaction is invalid + */ + INVALID_HOOK_CALL = 522; + /** + * Hooks are not supported to be used in TokenAirdrop transactions + */ + HOOKS_ARE_NOT_SUPPORTED_IN_AIRDROPS = 523; } diff --git a/Tests/HieroE2ETests/Account/AccountCreate.swift b/Tests/HieroE2ETests/Account/AccountCreate.swift index db9820fb..51e65901 100644 --- a/Tests/HieroE2ETests/Account/AccountCreate.swift +++ b/Tests/HieroE2ETests/Account/AccountCreate.swift @@ -447,8 +447,21 @@ internal final class AccountCreate: XCTestCase { // Given let ecdsaPrivateKey = PrivateKey.generateEcdsa() + // Create a real contract first + let contractResponse = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) + .execute(testEnv.client) + let contractReceipt = try await contractResponse.getReceipt(testEnv.client) + let contractId = try XCTUnwrap(contractReceipt.contractId) + var lambdaEvmHook = LambdaEvmHook() - lambdaEvmHook.spec.contractId = testContractId + lambdaEvmHook.spec.contractId = contractId let hookCreationDetails = HookCreationDetails( hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) @@ -476,8 +489,23 @@ internal final class AccountCreate: XCTestCase { // Given let ecdsaPrivateKey = PrivateKey.generateEcdsa() + // Create a real contract first + let contractResponse = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) + .execute(testEnv.client) + let contractReceipt = try await contractResponse.getReceipt(testEnv.client) + let contractId = try XCTUnwrap(contractReceipt.contractId) + + print("contractId: \(contractId)") + var lambdaEvmHook = LambdaEvmHook() - lambdaEvmHook.spec.contractId = testContractId + lambdaEvmHook.spec.contractId = contractId var lambdaStorageSlot = LambdaStorageSlot() lambdaStorageSlot.key = Data([0x01, 0x23, 0x45]) @@ -492,20 +520,19 @@ internal final class AccountCreate: XCTestCase { hookExtensionPoint: .accountAllowanceHook, hookId: 1, lambdaEvmHook: lambdaEvmHook) // When / Then - var txReceipt: Hiero.TransactionReceipt! do { - txReceipt = try await AccountCreateTransaction() + let txResponse = try await AccountCreateTransaction() .keyWithoutAlias(.single(ecdsaPrivateKey.publicKey)) .addHook(hookCreationDetails) .freezeWith(testEnv.client) .sign(ecdsaPrivateKey) .execute(testEnv.client) - .getReceipt(testEnv.client) + + let txReceipt = try await txResponse.getReceipt(testEnv.client) + XCTAssertNotNil(txReceipt.accountId) } catch { XCTFail("Unexpected throw: \(error)") } - - XCTAssertNotNil(txReceipt.accountId) } func test_CreateTransactionWithLambdaHookWithNoContractId() async throws { @@ -536,12 +563,12 @@ internal final class AccountCreate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client), "expected error creating account" ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") return } - XCTAssertEqual(status, .invalidHookId) + XCTAssertEqual(status, .invalidHookCreationSpec) } } @@ -560,6 +587,7 @@ internal final class AccountCreate: XCTestCase { // When / Then — expect precheck error when duplicate hook IDs supplied await assertThrowsHErrorAsync( try await AccountCreateTransaction() + .keyWithoutAlias(.single(ecdsaPrivateKey.publicKey)) .addHook(hookCreationDetails) .addHook(hookCreationDetails) .execute(testEnv.client) diff --git a/Tests/HieroE2ETests/Account/AccountUpdate.swift b/Tests/HieroE2ETests/Account/AccountUpdate.swift index 383160b7..6c50bcfd 100644 --- a/Tests/HieroE2ETests/Account/AccountUpdate.swift +++ b/Tests/HieroE2ETests/Account/AccountUpdate.swift @@ -123,7 +123,7 @@ internal final class AccountUpdate: XCTestCase { } } - internal func test_CanaddHookToCreateToAccount() async throws { + internal func test_CanAddHookToCreateToAccount() async throws { let testEnv = try TestEnvironment.nonFree // Given @@ -258,15 +258,15 @@ internal final class AccountUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.receiptStatus`, got \(error.kind)") return } XCTAssertEqual(status, .hookIdInUse) } } - internal func test_CanaddHookToCreateToAccountWithStorageUpdates() async throws { + internal func test_CanAddHookToCreateToAccountWithStorageUpdates() async throws { let testEnv = try TestEnvironment.nonFree // Given @@ -416,8 +416,8 @@ internal final class AccountUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.receiptStatus`, got \(error.kind)") return } XCTAssertEqual(status, .hookNotFound) @@ -463,8 +463,8 @@ internal final class AccountUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("Expected `.transactionPreCheckStatus`, got \(error.kind)") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("Expected `.receiptStatus`, got \(error.kind)") return } XCTAssertEqual(status, .hookNotFound) diff --git a/Tests/HieroE2ETests/Contract/ContractCreate.swift b/Tests/HieroE2ETests/Contract/ContractCreate.swift index 66c757b7..35e274f8 100644 --- a/Tests/HieroE2ETests/Contract/ContractCreate.swift +++ b/Tests/HieroE2ETests/Contract/ContractCreate.swift @@ -12,7 +12,7 @@ internal final class ContractCreate: XCTestCase { let receipt = try await ContractCreateTransaction() .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .gas(200000) + .gas(300000) .constructorParameters(ContractFunctionParameters().addString("Hello from Hiero.")) .bytecodeFileId(bytecode.fileId) .contractMemo("[e2e::ContractCreateTransaction]") @@ -43,7 +43,7 @@ internal final class ContractCreate: XCTestCase { let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) let receipt = try await ContractCreateTransaction() - .gas(200000) + .gas(300000) .constructorParameters(ContractFunctionParameters().addString("Hello from Hiero.")) .bytecodeFileId(bytecode.fileId) .contractMemo("[e2e::ContractCreateTransaction]") @@ -135,10 +135,13 @@ internal final class ContractCreate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - let lambdaId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .gas(300_000) .execute(testEnv.client) .getReceipt(testEnv.client) @@ -155,7 +158,12 @@ internal final class ContractCreate: XCTestCase { // When let txResponse = try await ContractCreateTransaction() - .bytecode(ContractHelpers.bytecode) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .gas(300_000) .addHook(hookCreationDetails) .execute(testEnv.client) @@ -169,11 +177,14 @@ internal final class ContractCreate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - let lambdaId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -198,7 +209,12 @@ internal final class ContractCreate: XCTestCase { // When let txResponse = try await ContractCreateTransaction() - .bytecode(ContractHelpers.bytecode) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) .gas(300_000) .addHook(hookCreationDetails) .execute(testEnv.client) @@ -223,8 +239,13 @@ internal final class ContractCreate: XCTestCase { // When / Then await assertThrowsHErrorAsync( try await ContractCreateTransaction() - .bytecode(ContractHelpers.bytecode) - .gas(300_000) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .addHook(hookCreationDetails) .execute(testEnv.client) .getReceipt(testEnv.client) @@ -241,17 +262,20 @@ internal final class ContractCreate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let lambdaId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractResponse = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) - .getReceipt(testEnv.client) - .contractId! + let contractReceipt = try await contractResponse.getReceipt(testEnv.client) + let contractId = try XCTUnwrap(contractReceipt.contractId) var lambdaEvmHook = LambdaEvmHook() - lambdaEvmHook.spec.contractId = lambdaId + lambdaEvmHook.spec.contractId = contractId let hookCreationDetails = HookCreationDetails( hookExtensionPoint: .accountAllowanceHook, @@ -262,8 +286,13 @@ internal final class ContractCreate: XCTestCase { // When / Then await assertThrowsHErrorAsync( try await ContractCreateTransaction() - .bytecode(ContractHelpers.bytecode) - .gas(300_000) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .addHook(hookCreationDetails) .addHook(hookCreationDetails) .execute(testEnv.client) @@ -282,11 +311,15 @@ internal final class ContractCreate: XCTestCase { // Given let adminKey = PrivateKey.generateEcdsa() - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) let lambdaId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -303,8 +336,13 @@ internal final class ContractCreate: XCTestCase { // When let txResponse = try await ContractCreateTransaction() - .bytecode(ContractHelpers.bytecode) - .gas(300_000) + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .addHook(hookCreationDetails) .execute(testEnv.client) diff --git a/Tests/HieroE2ETests/Contract/ContractCreateFlow.swift b/Tests/HieroE2ETests/Contract/ContractCreateFlow.swift index 8870412e..48f0d6db 100644 --- a/Tests/HieroE2ETests/Contract/ContractCreateFlow.swift +++ b/Tests/HieroE2ETests/Contract/ContractCreateFlow.swift @@ -17,7 +17,7 @@ internal final class ContractCreateFlow: XCTestCase { let receipt = try await Hiero.ContractCreateFlow() .bytecode(ContractHelpers.bytecodeString) .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .gas(200000) + .gas(300000) .constructorParameters(ContractFunctionParameters().addString("Hello from Hiero.")) .contractMemo("[e2e::ContractCreateFlow]") .execute(testEnv.client) @@ -84,7 +84,7 @@ internal final class ContractCreateFlow: XCTestCase { let receipt = try await Hiero.ContractCreateFlow() .bytecode(ContractHelpers.bytecodeString) .adminKey(.single(adminKey.publicKey)) - .gas(200000) + .gas(300000) .constructorParameters(ContractFunctionParameters().addString("Hello from Hiero.")) .contractMemo("[e2e::ContractCreateFlow]") .sign(adminKey) @@ -124,7 +124,7 @@ internal final class ContractCreateFlow: XCTestCase { let receipt = try await Hiero.ContractCreateFlow() .bytecode(ContractHelpers.bytecodeString) .adminKey(.single(adminKey.publicKey)) - .gas(200000) + .gas(300000) .constructorParameters(ContractFunctionParameters().addString("Hello from Hiero.")) .contractMemo("[e2e::ContractCreateFlow]") .signWith(adminKey.publicKey, adminKey.sign(_:)) diff --git a/Tests/HieroE2ETests/Contract/ContractUpdate.swift b/Tests/HieroE2ETests/Contract/ContractUpdate.swift index 826be859..58e2848d 100644 --- a/Tests/HieroE2ETests/Contract/ContractUpdate.swift +++ b/Tests/HieroE2ETests/Contract/ContractUpdate.swift @@ -71,12 +71,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -95,7 +98,6 @@ internal final class ContractUpdate: XCTestCase { _ = try await ContractUpdateTransaction() .contractId(contractId) .addHookToCreate(hookCreationDetails) - .freezeWith(testEnv.client) .execute(testEnv.client) .getReceipt(testEnv.client) } catch { @@ -107,12 +109,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -147,12 +152,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -182,8 +190,8 @@ internal final class ContractUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") return } XCTAssertEqual(status, .hookIdInUse) @@ -194,12 +202,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: false) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -238,12 +249,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -283,15 +297,35 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookId: Int64 = 1 + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: hookId, + lambdaEvmHook: lambdaEvmHook + ) + + _ = try await ContractUpdateTransaction() + .contractId(contractId) + .addHookToCreate(hookCreationDetails) + .execute(testEnv.client) + .getReceipt(testEnv.client) + // When / Then await assertThrowsHErrorAsync( try await ContractUpdateTransaction() @@ -300,8 +334,8 @@ internal final class ContractUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") return } XCTAssertEqual(status, .hookNotFound) @@ -312,12 +346,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -341,8 +378,8 @@ internal final class ContractUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") return } XCTAssertEqual(status, .hookNotFound) @@ -353,12 +390,15 @@ internal final class ContractUpdate: XCTestCase { let testEnv = try TestEnvironment.nonFree // Given - let lambdaId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) - let bytecode = try await File.forContent(ContractHelpers.bytecode, testEnv) - - let contractId = try await ContractCreateTransaction() - .bytecodeFileId(bytecode.fileId) - .gas(300_000) + let contractId = try await ContractHelpers.makeContract(testEnv, operatorAdminKey: true) + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) .contractId! @@ -399,8 +439,8 @@ internal final class ContractUpdate: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) ) { error in - guard case .transactionPreCheckStatus(let status, transactionId: _) = error.kind else { - XCTFail("\(error.kind) is not `.transactionPreCheckStatus(status: _)`") + guard case .receiptStatus(let status, transactionId: _) = error.kind else { + XCTFail("\(error.kind) is not `.receiptStatus(status: _)`") return } XCTAssertEqual(status, .hookNotFound) diff --git a/Tests/HieroE2ETests/TransferTransactionHooks.swift b/Tests/HieroE2ETests/TransferTransactionHooks.swift index 7ffa7f72..cc47407a 100644 --- a/Tests/HieroE2ETests/TransferTransactionHooks.swift +++ b/Tests/HieroE2ETests/TransferTransactionHooks.swift @@ -9,28 +9,48 @@ internal final class TransferTransactionHooks: XCTestCase { internal func testHbarTransferWithPreTxHook() async throws { let testEnv = try TestEnvironment.nonFree - // Create a test account + // Given + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 2, + lambdaEvmHook: lambdaEvmHook + ) + let key = PrivateKey.generateEd25519() let receipt = try await AccountCreateTransaction() .keyWithoutAlias(.single(key.publicKey)) - .initialBalance(10) + .initialBalance(1) + .addHook(hookCreationDetails) .execute(testEnv.client) .getReceipt(testEnv.client) let accountId = try XCTUnwrap(receipt.accountId) - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) + let hookCall = FungibleHookCall( + hookCall: HookCall(hookId: 2, evmHookCall: EvmHookCall(gasLimit: 25000)), + hookType: .preTxAllowanceHook + ) - // Create transfer transaction with hook + // When let transferTx = TransferTransaction() - .hbarTransferWithPreTxHook(accountId, 1, hookCall) + .hbarTransferWithHook(accountId, Hbar(1), hookCall) + .hbarTransfer(testEnv.operator.accountId, Hbar(-1)) - // Execute the transaction + // Then let response = try await transferTx.execute(testEnv.client) let transferReceipt = try await response.getReceipt(testEnv.client) @@ -40,28 +60,48 @@ internal final class TransferTransactionHooks: XCTestCase { internal func testHbarTransferWithPrePostTxHook() async throws { let testEnv = try TestEnvironment.nonFree - // Create a test account + // Given + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 2, + lambdaEvmHook: lambdaEvmHook + ) + let key = PrivateKey.generateEd25519() let receipt = try await AccountCreateTransaction() .keyWithoutAlias(.single(key.publicKey)) - .initialBalance(10) + .initialBalance(1) + .addHook(hookCreationDetails) .execute(testEnv.client) .getReceipt(testEnv.client) let accountId = try XCTUnwrap(receipt.accountId) - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) + let hookCall = FungibleHookCall( + hookCall: HookCall(hookId: 2, evmHookCall: EvmHookCall(gasLimit: 25000)), + hookType: .prePostTxAllowanceHook + ) - // Create transfer transaction with hook + // When let transferTx = TransferTransaction() - .hbarTransferWithPrePostTxHook(accountId, 1, hookCall) + .hbarTransferWithHook(accountId, Hbar(1), hookCall) + .hbarTransfer(testEnv.operator.accountId, Hbar(-1)) - // Execute the transaction + // Then let response = try await transferTx.execute(testEnv.client) let transferReceipt = try await response.getReceipt(testEnv.client) @@ -71,16 +111,37 @@ internal final class TransferTransactionHooks: XCTestCase { internal func testTokenTransferWithPreTxHook() async throws { let testEnv = try TestEnvironment.nonFree - // Create a test account + // Given + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! + ) + .gas(300000) + .execute(testEnv.client) + .getReceipt(testEnv.client) + .contractId! + + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId + + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 2, + lambdaEvmHook: lambdaEvmHook + ) + let key = PrivateKey.generateEd25519() let receipt = try await AccountCreateTransaction() .keyWithoutAlias(.single(key.publicKey)) - .initialBalance(10) + .initialBalance(1) + .addHook(hookCreationDetails) .execute(testEnv.client) .getReceipt(testEnv.client) let accountId = try XCTUnwrap(receipt.accountId) - // Create a token let tokenId = try await TokenCreateTransaction() .name("Test Token") .symbol("TT") @@ -92,321 +153,129 @@ internal final class TransferTransactionHooks: XCTestCase { .getReceipt(testEnv.client) .tokenId! - addTeardownBlock { - _ = try await TokenDeleteTransaction(tokenId: tokenId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - } - - // Associate the account with the token _ = try await TokenAssociateTransaction(accountId: accountId, tokenIds: [tokenId]) + .freezeWith(testEnv.client) + .sign(key) .execute(testEnv.client) .getReceipt(testEnv.client) - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) + let hookCall = FungibleHookCall( + hookCall: HookCall(hookId: 2, evmHookCall: EvmHookCall(gasLimit: 25000)), + hookType: .preTxAllowanceHook + ) - // Create transfer transaction with hook + // When let transferTx = TransferTransaction() - .tokenTransferWithPreTxHook(tokenId, accountId, 100, hookCall) + .tokenTransferWithHook(tokenId, accountId, 1000, hookCall) + .tokenTransfer(tokenId, testEnv.operator.accountId, -1000) - // Execute the transaction + // Then let response = try await transferTx.execute(testEnv.client) let transferReceipt = try await response.getReceipt(testEnv.client) XCTAssertEqual(transferReceipt.status, .success) } - internal func testNftTransferWithSenderHooks() async throws { + internal func disable_testNftTransferWithSenderAndReceiverHooks() async throws { let testEnv = try TestEnvironment.nonFree - // Create test accounts - let senderKey = PrivateKey.generateEd25519() - let senderReceipt = try await AccountCreateTransaction() - .keyWithoutAlias(.single(senderKey.publicKey)) - .initialBalance(10) - .execute(testEnv.client) - .getReceipt(testEnv.client) - let senderAccountId = try XCTUnwrap(senderReceipt.accountId) - - let receiverKey = PrivateKey.generateEd25519() - let receiverReceipt = try await AccountCreateTransaction() - .keyWithoutAlias(.single(receiverKey.publicKey)) - .initialBalance(10) - .execute(testEnv.client) - .getReceipt(testEnv.client) - let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) - - // Create an NFT token - let tokenId = try await TokenCreateTransaction() - .name("Test NFT") - .symbol("TNFT") - .tokenType(.nonFungibleUnique) - .treasuryAccountId(testEnv.operator.accountId) - .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .supplyKey(.single(testEnv.operator.privateKey.publicKey)) - .execute(testEnv.client) - .getReceipt(testEnv.client) - .tokenId! - - addTeardownBlock { - _ = try await TokenDeleteTransaction(tokenId: tokenId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - } - - // Associate accounts with the token - _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - // Mint an NFT - let nftSerial = try await TokenMintTransaction(tokenId: tokenId) - .metadata([Data("NFT Metadata".utf8)]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - .serials?.first! - - // Transfer NFT to sender - _ = try await TransferTransaction() - .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) - - // Create transfer transaction with sender hook - let transferTx = TransferTransaction() - .nftTransferWithSenderHooks( - NftId(tokenId: tokenId, serial: nftSerial!), - senderAccountId, - receiverAccountId, - preTxSenderHook: hookCall + // Given + let lambdaId = try await ContractCreateTransaction() + .bytecode( + Data( + hexEncoded: + "608060405234801561001057600080fd5b50610167806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80632f570a2314610030575b600080fd5b61004a600480360381019061004591906100b6565b610060565b604051610057919061010a565b60405180910390f35b60006001905092915050565b60008083601f84011261007e57600080fd5b8235905067ffffffffffffffff81111561009757600080fd5b6020830191508360018202830111156100af57600080fd5b9250929050565b600080602083850312156100c957600080fd5b600083013567ffffffffffffffff8111156100e357600080fd5b6100ef8582860161006c565b92509250509250929050565b61010481610125565b82525050565b600060208201905061011f60008301846100fb565b92915050565b6000811515905091905056fea264697066735822122097fc0c3ac3155b53596be3af3b4d2c05eb5e273c020ee447f01b72abc3416e1264736f6c63430008000033" + )! ) - - // Execute the transaction - let response = try await transferTx.execute(testEnv.client) - let transferReceipt = try await response.getReceipt(testEnv.client) - - XCTAssertEqual(transferReceipt.status, .success) - } - - internal func testNftTransferWithReceiverHooks() async throws { - let testEnv = try TestEnvironment.nonFree - - // Create test accounts - let senderKey = PrivateKey.generateEd25519() - let senderReceipt = try await AccountCreateTransaction() - .keyWithoutAlias(.single(senderKey.publicKey)) - .initialBalance(10) + .gas(300000) .execute(testEnv.client) .getReceipt(testEnv.client) - let senderAccountId = try XCTUnwrap(senderReceipt.accountId) - - let receiverKey = PrivateKey.generateEd25519() - let receiverReceipt = try await AccountCreateTransaction() - .keyWithoutAlias(.single(receiverKey.publicKey)) - .initialBalance(10) - .execute(testEnv.client) - .getReceipt(testEnv.client) - let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) - - // Create an NFT token - let tokenId = try await TokenCreateTransaction() - .name("Test NFT") - .symbol("TNFT") - .tokenType(.nonFungibleUnique) - .treasuryAccountId(testEnv.operator.accountId) - .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .supplyKey(.single(testEnv.operator.privateKey.publicKey)) - .execute(testEnv.client) - .getReceipt(testEnv.client) - .tokenId! - - addTeardownBlock { - _ = try await TokenDeleteTransaction(tokenId: tokenId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - } - - // Associate accounts with the token - _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - // Mint an NFT - let nftSerial = try await TokenMintTransaction(tokenId: tokenId) - .metadata([Data("NFT Metadata".utf8)]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - .serials?.first! - - // Transfer NFT to sender - _ = try await TransferTransaction() - .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) - - // Create transfer transaction with receiver hook - let transferTx = TransferTransaction() - .nftTransferWithReceiverHooks( - NftId(tokenId: tokenId, serial: nftSerial!), - senderAccountId, - receiverAccountId, - preTxReceiverHook: hookCall - ) + .contractId! - // Execute the transaction - let response = try await transferTx.execute(testEnv.client) - let transferReceipt = try await response.getReceipt(testEnv.client) + var lambdaEvmHook = LambdaEvmHook() + lambdaEvmHook.spec.contractId = lambdaId - XCTAssertEqual(transferReceipt.status, .success) - } + let hookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 1, + lambdaEvmHook: lambdaEvmHook + ) - internal func testNftTransferWithAllHooks() async throws { - let testEnv = try TestEnvironment.nonFree - - // Create test accounts let senderKey = PrivateKey.generateEd25519() let senderReceipt = try await AccountCreateTransaction() .keyWithoutAlias(.single(senderKey.publicKey)) - .initialBalance(10) + .initialBalance(2) + .addHook(hookCreationDetails) + .freezeWith(testEnv.client) + .sign(senderKey) .execute(testEnv.client) .getReceipt(testEnv.client) let senderAccountId = try XCTUnwrap(senderReceipt.accountId) + + var receiverLambdaEvmHook = LambdaEvmHook() + receiverLambdaEvmHook.spec.contractId = lambdaId + + let receiverHookCreationDetails = HookCreationDetails( + hookExtensionPoint: .accountAllowanceHook, + hookId: 2, + lambdaEvmHook: receiverLambdaEvmHook + ) let receiverKey = PrivateKey.generateEd25519() let receiverReceipt = try await AccountCreateTransaction() .keyWithoutAlias(.single(receiverKey.publicKey)) - .initialBalance(10) + .initialBalance(2) + .addHook(receiverHookCreationDetails) + .freezeWith(testEnv.client) + .sign(receiverKey) .execute(testEnv.client) .getReceipt(testEnv.client) let receiverAccountId = try XCTUnwrap(receiverReceipt.accountId) - // Create an NFT token let tokenId = try await TokenCreateTransaction() .name("Test NFT") .symbol("TNFT") .tokenType(.nonFungibleUnique) - .treasuryAccountId(testEnv.operator.accountId) - .adminKey(.single(testEnv.operator.privateKey.publicKey)) - .supplyKey(.single(testEnv.operator.privateKey.publicKey)) + .initialSupply(0) + .treasuryAccountId(senderAccountId) + .adminKey(.single(senderKey.publicKey)) + .supplyKey(.single(senderKey.publicKey)) + .freezeWith(testEnv.client) + .sign(senderKey) .execute(testEnv.client) .getReceipt(testEnv.client) .tokenId! - addTeardownBlock { - _ = try await TokenDeleteTransaction(tokenId: tokenId) - .execute(testEnv.client) - .getReceipt(testEnv.client) - } - - // Associate accounts with the token - _ = try await TokenAssociateTransaction(accountId: senderAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) - .execute(testEnv.client) - .getReceipt(testEnv.client) - - // Mint an NFT - let nftSerial = try await TokenMintTransaction(tokenId: tokenId) + let _ = try await TokenMintTransaction(tokenId: tokenId) .metadata([Data("NFT Metadata".utf8)]) + .freezeWith(testEnv.client) + .sign(senderKey) .execute(testEnv.client) .getReceipt(testEnv.client) - .serials?.first! - // Transfer NFT to sender - _ = try await TransferTransaction() - .nftTransfer(NftId(tokenId: tokenId, serial: nftSerial!), testEnv.operator.accountId, senderAccountId) + _ = try await TokenAssociateTransaction(accountId: receiverAccountId, tokenIds: [tokenId]) + .freezeWith(testEnv.client) + .sign(receiverKey) .execute(testEnv.client) .getReceipt(testEnv.client) - // Create hook calls - var senderHookCall = HookCall() - senderHookCall = senderHookCall.hookId(1) - var senderEvmHookCall = EvmHookCall() - senderEvmHookCall = senderEvmHookCall.data(Data([0x01, 0x02, 0x03])) - senderEvmHookCall = senderEvmHookCall.gasLimit(100000) - senderHookCall = senderHookCall.evmHookCall(senderEvmHookCall) - - var receiverHookCall = HookCall() - receiverHookCall = receiverHookCall.hookId(2) - var receiverEvmHookCall = EvmHookCall() - receiverEvmHookCall = receiverEvmHookCall.data(Data([0x04, 0x05, 0x06])) - receiverEvmHookCall = receiverEvmHookCall.gasLimit(100000) - receiverHookCall = receiverHookCall.evmHookCall(receiverEvmHookCall) - - // Create transfer transaction with all hooks - let transferTx = TransferTransaction() - .nftTransferWithAllHooks( - NftId(tokenId: tokenId, serial: nftSerial!), - senderAccountId, - receiverAccountId, - preTxSenderHook: senderHookCall, - preTxReceiverHook: receiverHookCall - ) - - // Execute the transaction - let response = try await transferTx.execute(testEnv.client) - let transferReceipt = try await response.getReceipt(testEnv.client) - - XCTAssertEqual(transferReceipt.status, .success) - } - - internal func testApprovedTransferWithHooks() async throws { - let testEnv = try TestEnvironment.nonFree - - // Create a test account - let key = PrivateKey.generateEd25519() - let receipt = try await AccountCreateTransaction() - .keyWithoutAlias(.single(key.publicKey)) - .initialBalance(10) - .execute(testEnv.client) - .getReceipt(testEnv.client) - let accountId = try XCTUnwrap(receipt.accountId) + let senderCall = NftHookCall( + hookCall: HookCall(hookId: 1, evmHookCall: EvmHookCall(gasLimit: 25000)), + hookType: .preHook + ) - // Create a simple hook call - var hookCall = HookCall() - hookCall = hookCall.hookId(1) - var evmHookCall = EvmHookCall() - evmHookCall = evmHookCall.data(Data([0x01, 0x02, 0x03])) - evmHookCall = evmHookCall.gasLimit(100000) - hookCall = hookCall.evmHookCall(evmHookCall) + let receiverCall = NftHookCall( + hookCall: HookCall(hookId: 2, evmHookCall: EvmHookCall(gasLimit: 25000)), + hookType: .preHook + ) - // Create approved transfer transaction with hook + let nftId = NftId(tokenId: tokenId, serial: 1) + + // When let transferTx = TransferTransaction() - .approvedHbarTransferWithPreTxHook(accountId, 1, hookCall) + .nftTransferWithHooks(nftId, senderAccountId, receiverAccountId, senderCall, receiverCall) - // Execute the transaction + // Then let response = try await transferTx.execute(testEnv.client) let transferReceipt = try await response.getReceipt(testEnv.client) diff --git a/Tests/HieroTests/EvmHookSpecTests.swift b/Tests/HieroTests/EvmHookSpecTests.swift index 6787b6ea..5460d09d 100644 --- a/Tests/HieroTests/EvmHookSpecTests.swift +++ b/Tests/HieroTests/EvmHookSpecTests.swift @@ -24,7 +24,7 @@ final class EvmHookSpecUnitTests: XCTestCase { func test_FromProtobuf() throws { // Given var protoSpec = Com_Hedera_Hapi_Node_Hooks_EvmHookSpec() - protoSpec.contractID = testContractId.toProtobuf() + protoSpec.bytecodeSource = .contractID(testContractId.toProtobuf()) // When let evmHookSpec = try EvmHookSpec.fromProtobuf(protoSpec) @@ -42,8 +42,12 @@ final class EvmHookSpecUnitTests: XCTestCase { let protoSpec = evmHookSpec.toProtobuf() // Then - XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.shardNum), testContractId.shard) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.realmNum), testContractId.realm) - XCTAssertEqual(UInt64(truncatingIfNeeded: protoSpec.contractID.contractNum), testContractId.num) + guard case .contractID(let contractID)? = protoSpec.bytecodeSource else { + XCTFail("Expected bytecodeSource to be .contractID") + return + } + XCTAssertEqual(UInt64(truncatingIfNeeded: contractID.shardNum), testContractId.shard) + XCTAssertEqual(UInt64(truncatingIfNeeded: contractID.realmNum), testContractId.realm) + XCTAssertEqual(UInt64(truncatingIfNeeded: contractID.contractNum), testContractId.num) } } diff --git a/Tests/HieroTests/FungibleHookCallTests.swift b/Tests/HieroTests/FungibleHookCallTests.swift new file mode 100644 index 00000000..de489c7c --- /dev/null +++ b/Tests/HieroTests/FungibleHookCallTests.swift @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs +import XCTest + +@testable import Hiero + +final class FungibleHookCallUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let testAccountId = AccountId(shard: 1, realm: 2, num: 3) + private let testHookId: Int64 = 4 + private let testCallData = Data([0x56, 0x78, 0x9A]) + private let testGasLimit: UInt64 = 11 + + private var testHookEntityId: HookEntityId { + return HookEntityId(testAccountId) + } + + private var testFullHookId: HookId { + return HookId(entityId: testHookEntityId, hookId: testHookId) + } + + private var testEvmHookCall: EvmHookCall { + var c = EvmHookCall() + c.data = testCallData + c.gasLimit = testGasLimit + return c + } + + func test_DefaultInitialization() { + // Given & When + let fungibleHookCall = FungibleHookCall() + + // Then + XCTAssertEqual(fungibleHookCall.hookType, .uninitialized) + XCTAssertNil(fungibleHookCall.hookCall.fullHookId) + XCTAssertNil(fungibleHookCall.hookCall.hookId) + XCTAssertNil(fungibleHookCall.hookCall.evmHookCall) + } + + func test_CustomInitialization() { + // Given + let hookCall = HookCall() + let hookType = FungibleHookType.preTxAllowanceHook + + // When + let fungibleHookCall = FungibleHookCall(hookCall: hookCall, hookType: hookType) + + // Then + XCTAssertEqual(fungibleHookCall.hookType, hookType) + XCTAssertNil(fungibleHookCall.hookCall.fullHookId) + XCTAssertNil(fungibleHookCall.hookCall.hookId) + XCTAssertNil(fungibleHookCall.hookCall.evmHookCall) + } + + func test_SetHookCall() { + // Given + var fungibleHookCall = FungibleHookCall() + let hookCall = HookCall() + + // When + fungibleHookCall.hookCall(hookCall) + + // Then + XCTAssertNil(fungibleHookCall.hookCall.fullHookId) + XCTAssertNil(fungibleHookCall.hookCall.hookId) + XCTAssertNil(fungibleHookCall.hookCall.evmHookCall) + } + + func test_SetHookType() { + // Given + var fungibleHookCall = FungibleHookCall() + + // When + fungibleHookCall.hookType(.preTxAllowanceHook) + + // Then + XCTAssertEqual(fungibleHookCall.hookType, .preTxAllowanceHook) + } + + func test_SetFullHookId() { + // Given + var fungibleHookCall = FungibleHookCall() + + // When + fungibleHookCall.fullHookId(testFullHookId) + + // Then + XCTAssertNotNil(fungibleHookCall.hookCall.fullHookId?.entityId.accountId) + XCTAssertEqual(fungibleHookCall.hookCall.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(fungibleHookCall.hookCall.fullHookId?.hookId, testHookId) + } + + func test_SetHookId() { + // Given + var fungibleHookCall = FungibleHookCall() + + // When + fungibleHookCall.hookId(testHookId) + + // Then + XCTAssertEqual(fungibleHookCall.hookCall.hookId, testHookId) + } + + func test_SetEvmHookCall() { + // Given + var fungibleHookCall = FungibleHookCall() + + // When + fungibleHookCall.evmHookCall(testEvmHookCall) + + // Then + XCTAssertNotNil(fungibleHookCall.hookCall.evmHookCall) + XCTAssertEqual(fungibleHookCall.hookCall.evmHookCall?.data, testCallData) + XCTAssertEqual(fungibleHookCall.hookCall.evmHookCall?.gasLimit, testGasLimit) + } + + func test_FromProtobuf() throws { + // Given + var protoFull = Proto_HookCall() + var protoHookIdOnly = Proto_HookCall() + + // full_hook_id + evm_hook_call + protoFull.fullHookID = testFullHookId.toProtobuf() + protoFull.evmHookCall = testEvmHookCall.toProtobuf() + + // hook_id only + protoHookIdOnly.hookID = testHookId + + // When + let fungibleHookCallFull = try FungibleHookCall.fromProtobuf(protoFull) + let fungibleHookCallHookOnly = try FungibleHookCall.fromProtobuf(protoHookIdOnly) + + // Then + XCTAssertNotNil(fungibleHookCallFull.hookCall.fullHookId?.entityId.accountId) + XCTAssertEqual(fungibleHookCallFull.hookCall.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(fungibleHookCallFull.hookCall.fullHookId?.hookId, testHookId) + + XCTAssertNotNil(fungibleHookCallFull.hookCall.evmHookCall) + XCTAssertEqual(fungibleHookCallFull.hookCall.evmHookCall?.data, testCallData) + XCTAssertEqual(fungibleHookCallFull.hookCall.evmHookCall?.gasLimit, testGasLimit) + + XCTAssertEqual(fungibleHookCallHookOnly.hookCall.hookId, testHookId) + + // Hook type should be uninitialized since it's not stored in protobuf + XCTAssertEqual(fungibleHookCallFull.hookType, .uninitialized) + XCTAssertEqual(fungibleHookCallHookOnly.hookType, .uninitialized) + } + + func test_ToProtobuf() { + // Given + var fungibleHookCallFull = FungibleHookCall() + var fungibleHookCallHookOnly = FungibleHookCall() + + fungibleHookCallFull.hookCall.fullHookId = testFullHookId + fungibleHookCallFull.hookCall.evmHookCall = testEvmHookCall + fungibleHookCallFull.hookType = .preTxAllowanceHook + + fungibleHookCallHookOnly.hookCall.hookId = testHookId + fungibleHookCallHookOnly.hookType = .prePostTxAllowanceHook + + // When + let protoFull = fungibleHookCallFull.toProtobuf() + let protoHookOnly = fungibleHookCallHookOnly.toProtobuf() + + // Then + XCTAssertTrue(protoFull.fullHookID.hasEntityID) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.accountNum), testAccountId.num) + XCTAssertEqual(protoFull.fullHookID.hookID, testHookId) + + XCTAssertEqual(protoFull.evmHookCall.data, testCallData) + XCTAssertEqual(protoFull.evmHookCall.gasLimit, testGasLimit) + + XCTAssertEqual(protoHookOnly.hookID, testHookId) + } +} diff --git a/Tests/HieroTests/FungibleHookTypeTests.swift b/Tests/HieroTests/FungibleHookTypeTests.swift new file mode 100644 index 00000000..5a326b14 --- /dev/null +++ b/Tests/HieroTests/FungibleHookTypeTests.swift @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +@testable import Hiero + +final class FungibleHookTypeUnitTests: XCTestCase { + + func test_AllCases() { + // Given & When + let allCases = FungibleHookType.allCases + + // Then + XCTAssertEqual(allCases.count, 3) + XCTAssertTrue(allCases.contains(.preTxAllowanceHook)) + XCTAssertTrue(allCases.contains(.prePostTxAllowanceHook)) + XCTAssertTrue(allCases.contains(.uninitialized)) + } + + func test_Description() { + // Given & When & Then + XCTAssertEqual(FungibleHookType.preTxAllowanceHook.description, "PRE_TX_ALLOWANCE_HOOK") + XCTAssertEqual(FungibleHookType.prePostTxAllowanceHook.description, "PRE_POST_TX_ALLOWANCE_HOOK") + XCTAssertEqual(FungibleHookType.uninitialized.description, "UNINITIALIZED") + } + + func test_Equality() { + // Given & When & Then + XCTAssertEqual(FungibleHookType.preTxAllowanceHook, FungibleHookType.preTxAllowanceHook) + XCTAssertEqual(FungibleHookType.prePostTxAllowanceHook, FungibleHookType.prePostTxAllowanceHook) + XCTAssertEqual(FungibleHookType.uninitialized, FungibleHookType.uninitialized) + + XCTAssertNotEqual(FungibleHookType.preTxAllowanceHook, FungibleHookType.prePostTxAllowanceHook) + XCTAssertNotEqual(FungibleHookType.preTxAllowanceHook, FungibleHookType.uninitialized) + XCTAssertNotEqual(FungibleHookType.prePostTxAllowanceHook, FungibleHookType.uninitialized) + } + + func test_Hashable() { + // Given + let set: Set = [.preTxAllowanceHook, .prePostTxAllowanceHook, .uninitialized] + + // When & Then + XCTAssertEqual(set.count, 3) + XCTAssertTrue(set.contains(.preTxAllowanceHook)) + XCTAssertTrue(set.contains(.prePostTxAllowanceHook)) + XCTAssertTrue(set.contains(.uninitialized)) + } +} diff --git a/Tests/HieroTests/NFTHookCallTests.swift b/Tests/HieroTests/NFTHookCallTests.swift new file mode 100644 index 00000000..8ffa97a3 --- /dev/null +++ b/Tests/HieroTests/NFTHookCallTests.swift @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import HieroProtobufs +import XCTest + +@testable import Hiero + +final class NFTHookCallUnitTests: XCTestCase { + + // Fixture-equivalent constants + private let testAccountId = AccountId(shard: 1, realm: 2, num: 3) + private let testHookId: Int64 = 4 + private let testCallData = Data([0x56, 0x78, 0x9A]) + private let testGasLimit: UInt64 = 11 + + private var testHookEntityId: HookEntityId { + return HookEntityId(testAccountId) + } + + private var testFullHookId: HookId { + return HookId(entityId: testHookEntityId, hookId: testHookId) + } + + private var testEvmHookCall: EvmHookCall { + var c = EvmHookCall() + c.data = testCallData + c.gasLimit = testGasLimit + return c + } + + func test_DefaultInitialization() { + // Given & When + let nftHookCall = NftHookCall() + + // Then + XCTAssertEqual(nftHookCall.hookType, .uninitialized) + XCTAssertNil(nftHookCall.hookCall.fullHookId) + XCTAssertNil(nftHookCall.hookCall.hookId) + XCTAssertNil(nftHookCall.hookCall.evmHookCall) + } + + func test_CustomInitialization() { + // Given + let hookCall = HookCall() + let hookType = NftHookType.preHook + + // When + let nftHookCall = NftHookCall(hookCall: hookCall, hookType: hookType) + + // Then + XCTAssertEqual(nftHookCall.hookType, hookType) + XCTAssertNil(nftHookCall.hookCall.fullHookId) + XCTAssertNil(nftHookCall.hookCall.hookId) + XCTAssertNil(nftHookCall.hookCall.evmHookCall) + } + + func test_SetHookCall() { + // Given + var nftHookCall = NftHookCall() + let hookCall = HookCall() + + // When + nftHookCall.hookCall(hookCall) + + // Then + XCTAssertNil(nftHookCall.hookCall.fullHookId) + XCTAssertNil(nftHookCall.hookCall.hookId) + XCTAssertNil(nftHookCall.hookCall.evmHookCall) + } + + func test_SetHookType() { + // Given + var nftHookCall = NftHookCall() + + // When + nftHookCall.hookType(.preHook) + + // Then + XCTAssertEqual(nftHookCall.hookType, .preHook) + } + + func test_SetFullHookId() { + // Given + var nftHookCall = NftHookCall() + + // When + nftHookCall.fullHookId(testFullHookId) + + // Then + XCTAssertNotNil(nftHookCall.hookCall.fullHookId?.entityId.accountId) + XCTAssertEqual(nftHookCall.hookCall.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(nftHookCall.hookCall.fullHookId?.hookId, testHookId) + } + + func test_SetHookId() { + // Given + var nftHookCall = NftHookCall() + + // When + nftHookCall.hookId(testHookId) + + // Then + XCTAssertEqual(nftHookCall.hookCall.hookId, testHookId) + } + + func test_SetEvmHookCall() { + // Given + var nftHookCall = NftHookCall() + + // When + nftHookCall.evmHookCall(testEvmHookCall) + + // Then + XCTAssertNotNil(nftHookCall.hookCall.evmHookCall) + XCTAssertEqual(nftHookCall.hookCall.evmHookCall?.data, testCallData) + XCTAssertEqual(nftHookCall.hookCall.evmHookCall?.gasLimit, testGasLimit) + } + + func test_FromProtobuf() throws { + // Given + var protoFull = Proto_HookCall() + var protoHookIdOnly = Proto_HookCall() + + // full_hook_id + evm_hook_call + protoFull.fullHookID = testFullHookId.toProtobuf() + protoFull.evmHookCall = testEvmHookCall.toProtobuf() + + // hook_id only + protoHookIdOnly.hookID = testHookId + + // When + let nftHookCallFull = try NftHookCall.fromProtobuf(protoFull) + let nftHookCallHookOnly = try NftHookCall.fromProtobuf(protoHookIdOnly) + + // Then + XCTAssertNotNil(nftHookCallFull.hookCall.fullHookId?.entityId.accountId) + XCTAssertEqual(nftHookCallFull.hookCall.fullHookId?.entityId.accountId, testAccountId) + XCTAssertEqual(nftHookCallFull.hookCall.fullHookId?.hookId, testHookId) + + XCTAssertNotNil(nftHookCallFull.hookCall.evmHookCall) + XCTAssertEqual(nftHookCallFull.hookCall.evmHookCall?.data, testCallData) + XCTAssertEqual(nftHookCallFull.hookCall.evmHookCall?.gasLimit, testGasLimit) + + XCTAssertEqual(nftHookCallHookOnly.hookCall.hookId, testHookId) + + // Hook type should be uninitialized since it's not stored in protobuf + XCTAssertEqual(nftHookCallFull.hookType, .uninitialized) + XCTAssertEqual(nftHookCallHookOnly.hookType, .uninitialized) + } + + func test_ToProtobuf() { + // Given + var nftHookCallFull = NftHookCall() + var nftHookCallHookOnly = NftHookCall() + + nftHookCallFull.hookCall.fullHookId = testFullHookId + nftHookCallFull.hookCall.evmHookCall = testEvmHookCall + nftHookCallFull.hookType = .preHook + + nftHookCallHookOnly.hookCall.hookId = testHookId + nftHookCallHookOnly.hookType = .prePostHook + + // When + let protoFull = nftHookCallFull.toProtobuf() + let protoHookOnly = nftHookCallHookOnly.toProtobuf() + + // Then + XCTAssertTrue(protoFull.fullHookID.hasEntityID) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.shardNum), testAccountId.shard) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.realmNum), testAccountId.realm) + XCTAssertEqual( + UInt64(truncatingIfNeeded: protoFull.fullHookID.entityID.accountID.accountNum), testAccountId.num) + XCTAssertEqual(protoFull.fullHookID.hookID, testHookId) + + XCTAssertEqual(protoFull.evmHookCall.data, testCallData) + XCTAssertEqual(protoFull.evmHookCall.gasLimit, testGasLimit) + + XCTAssertEqual(protoHookOnly.hookID, testHookId) + } +} diff --git a/Tests/HieroTests/NFTHookTypeTests.swift b/Tests/HieroTests/NFTHookTypeTests.swift new file mode 100644 index 00000000..e8fb2bc3 --- /dev/null +++ b/Tests/HieroTests/NFTHookTypeTests.swift @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +import Foundation +import XCTest + +@testable import Hiero + +final class NFTHookTypeUnitTests: XCTestCase { + + func test_AllCases() { + // Given & When + let allCases = NftHookType.allCases + + // Then + XCTAssertEqual(allCases.count, 3) + XCTAssertTrue(allCases.contains(.preHook)) + XCTAssertTrue(allCases.contains(.prePostHook)) + XCTAssertTrue(allCases.contains(.uninitialized)) + } + + func test_Description() { + // Given & When & Then + XCTAssertEqual(NftHookType.preHook.description, "PRE_HOOK") + XCTAssertEqual(NftHookType.prePostHook.description, "PRE_POST_HOOK") + XCTAssertEqual(NftHookType.uninitialized.description, "UNINITIALIZED") + } + + func test_Equality() { + // Given & When & Then + XCTAssertEqual(NftHookType.preHook, NftHookType.preHook) + XCTAssertEqual(NftHookType.prePostHook, NftHookType.prePostHook) + XCTAssertEqual(NftHookType.uninitialized, NftHookType.uninitialized) + + XCTAssertNotEqual(NftHookType.preHook, NftHookType.prePostHook) + XCTAssertNotEqual(NftHookType.preHook, NftHookType.uninitialized) + XCTAssertNotEqual(NftHookType.prePostHook, NftHookType.uninitialized) + } + + func test_Hashable() { + // Given + let set: Set = [.preHook, .prePostHook, .uninitialized] + + // When & Then + XCTAssertEqual(set.count, 3) + XCTAssertTrue(set.contains(.preHook)) + XCTAssertTrue(set.contains(.prePostHook)) + XCTAssertTrue(set.contains(.uninitialized)) + } +} diff --git a/protobufs b/protobufs index 6cb044ab..3fd256e2 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 6cb044abed16f1c7859ae45e0cbdc567ce2c9a74 +Subproject commit 3fd256e226c24e3d7fea459e6bd2361704010515 From bfa963e0dc45f297d224a3755ed78c0847252c8c Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 28 Oct 2025 20:32:05 -0400 Subject: [PATCH 7/7] refactor: formatting Signed-off-by: Rob Walworth --- Examples/TransferWithHooks/main.swift | 12 ++++++------ Tests/HieroE2ETests/TransferTransactionHooks.swift | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/TransferWithHooks/main.swift b/Examples/TransferWithHooks/main.swift index 1aa8ab4e..067c6ef9 100644 --- a/Examples/TransferWithHooks/main.swift +++ b/Examples/TransferWithHooks/main.swift @@ -33,7 +33,7 @@ struct TransferWithHooksExample { 0x60, 0x80, 0x60, 0x40, 0x52, 0x34, 0x80, 0x15, 0x61, 0x00, 0x10, 0x57, 0x60, 0x00, 0x80, 0xfd, 0x5b, 0x50, 0x60, 0x04, 0x36, 0x10, 0x61, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, - 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35 + 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, 0x60, 0x00, 0x35, ]) let hookContractResponse = try await ContractCreateTransaction() @@ -53,9 +53,9 @@ struct TransferWithHooksExample { // Create hook details var evmHookSpec = EvmHookSpec() evmHookSpec.contractId = hookContractId - + let lambdaHook = LambdaEvmHook(spec: evmHookSpec) - + let hookDetails = HookCreationDetails( hookExtensionPoint: .accountAllowanceHook, hookId: 1, @@ -69,7 +69,7 @@ struct TransferWithHooksExample { .initialBalance(10) .addHook(hookDetails) .execute(client) - + let senderReceipt = try await senderResponse.getReceipt(client) guard let senderAccountId = senderReceipt.accountId else { print("Failed to create sender account!") @@ -86,7 +86,7 @@ struct TransferWithHooksExample { .initialBalance(10) .addHook(hookDetails) .execute(client) - + let receiverReceipt = try await receiverResponse.getReceipt(client) guard let receiverAccountId = receiverReceipt.accountId else { print("Failed to create receiver account!") @@ -239,4 +239,4 @@ struct TransferWithHooksExample { print("Transfer Transaction Hooks Example Complete!") } -} \ No newline at end of file +} diff --git a/Tests/HieroE2ETests/TransferTransactionHooks.swift b/Tests/HieroE2ETests/TransferTransactionHooks.swift index cc47407a..e8c67b2b 100644 --- a/Tests/HieroE2ETests/TransferTransactionHooks.swift +++ b/Tests/HieroE2ETests/TransferTransactionHooks.swift @@ -211,7 +211,7 @@ internal final class TransferTransactionHooks: XCTestCase { .execute(testEnv.client) .getReceipt(testEnv.client) let senderAccountId = try XCTUnwrap(senderReceipt.accountId) - + var receiverLambdaEvmHook = LambdaEvmHook() receiverLambdaEvmHook.spec.contractId = lambdaId @@ -270,7 +270,7 @@ internal final class TransferTransactionHooks: XCTestCase { ) let nftId = NftId(tokenId: tokenId, serial: 1) - + // When let transferTx = TransferTransaction() .nftTransferWithHooks(nftId, senderAccountId, receiverAccountId, senderCall, receiverCall)