From 5fba7b68081187315bafbdaefd13f40076c92fde Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 29 Jul 2025 10:01:02 -0600 Subject: [PATCH 1/3] feat: max_custom_fee limits Signed-off-by: Rob Walworth --- .../Schedule/ScheduleCreateTransaction.swift | 1 + .../services/contract_types.pb.swift | 5 ++-- .../Generated/services/node_update.pb.swift | 4 ++- .../Generated/services/response_code.pb.swift | 8 ++++++ .../schedulable_transaction_body.pb.swift | 19 ++++++++++++++ .../Protos/services/contract_types.proto | 5 ++-- .../Protos/services/node_update.proto | 4 ++- .../Protos/services/response_code.proto | 5 ++++ .../schedulable_transaction_body.proto | 10 ++++++++ .../TopicMessageSubmitTransactionTests.swift | 25 ++++++++++++++++++- protobufs | 2 +- 11 files changed, 80 insertions(+), 8 deletions(-) diff --git a/Sources/Hiero/Schedule/ScheduleCreateTransaction.swift b/Sources/Hiero/Schedule/ScheduleCreateTransaction.swift index 914ab4a8..c2a5359c 100644 --- a/Sources/Hiero/Schedule/ScheduleCreateTransaction.swift +++ b/Sources/Hiero/Schedule/ScheduleCreateTransaction.swift @@ -171,6 +171,7 @@ extension ScheduleCreateTransaction: ToProtobuf { Proto_SchedulableTransactionBody.with { proto in proto.data = scheduledTransaction.toSchedulableTransactionData() proto.memo = scheduledTransaction.transaction.transactionMemo + proto.maxCustomFees = scheduledTransaction.transaction.customFeeLimits.compactMap { $0.toProtobuf() } let transactionFee = scheduledTransaction.transaction.maxTransactionFee diff --git a/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift b/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift index 2359cb85..2c3bd1fd 100644 --- a/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/contract_types.pb.swift @@ -56,7 +56,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 +99,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/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..5a07298e 100644 --- a/Sources/HieroProtobufs/Generated/services/response_code.pb.swift +++ b/Sources/HieroProtobufs/Generated/services/response_code.pb.swift @@ -1546,6 +1546,10 @@ 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 case UNRECOGNIZED(Int) public init() { @@ -1912,6 +1916,7 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { case 397: self = .throttleGroupLcmOverflow case 398: self = .airdropContainsMultipleSendersForAToken case 399: self = .grpcWebProxyNotSupported + case 400: self = .nftTransfersOnlyAllowedForNonFungibleUnique default: self = .UNRECOGNIZED(rawValue) } } @@ -2276,6 +2281,7 @@ 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 .UNRECOGNIZED(let i): return i } } @@ -2640,6 +2646,7 @@ public enum Proto_ResponseCodeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { .throttleGroupLcmOverflow, .airdropContainsMultipleSendersForAToken, .grpcWebProxyNotSupported, + .nftTransfersOnlyAllowedForNonFungibleUnique, ] } @@ -3006,5 +3013,6 @@ 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"), ] } 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/Protos/services/contract_types.proto b/Sources/HieroProtobufs/Protos/services/contract_types.proto index 7202ea7b..5d84f346 100644 --- a/Sources/HieroProtobufs/Protos/services/contract_types.proto +++ b/Sources/HieroProtobufs/Protos/services/contract_types.proto @@ -42,7 +42,7 @@ message InternalCallContext { } /** - * Results of executing EVM transaction.
+ * Results of executing a EVM transaction.
*/ message EvmTransactionResult { /** @@ -73,7 +73,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/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..6dadd9f1 100644 --- a/Sources/HieroProtobufs/Protos/services/response_code.proto +++ b/Sources/HieroProtobufs/Protos/services/response_code.proto @@ -1760,4 +1760,9 @@ 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; } 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/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift b/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift index 6c265bd6..a00955fd 100644 --- a/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift +++ b/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift @@ -24,7 +24,7 @@ internal final class TopicMessageSubmitTransactionTests: XCTestCase { internal func testSerialize() throws { let tx = try Self.makeTransaction().makeProtoBody() - assertSnapshot(matching: tx, as: .description) + assertSnapshot(of: tx, as: .description) } internal func testToFromBytes() throws { @@ -128,4 +128,27 @@ internal final class TopicMessageSubmitTransactionTests: XCTestCase { XCTAssertEqual(tx.customFeeLimits, [customFeeLimitToAdd]) } + + internal func testScheduledCustomFeeLimits() throws { + let payerId = AccountId(3) + let amount: UInt64 = 4 + let tokenId = TokenId(3) + let customFeeLimitToAdd = CustomFeeLimit( + payerId: payerId, + customFees: [ + CustomFixedFee(amount, nil, tokenId) + ]) + + let tx = TopicMessageSubmitTransaction() + .addCustomFeeLimit(customFeeLimitToAdd) + .schedule() + .toProtobuf() + + XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees.count, 1) + XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].accountID.accountNum, Int64(payerId.num)) + XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees.count, 1) + XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees[0].amount, Int64(amount)) + XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees[0].denominatingTokenID.tokenNum, Int64(tokenId.num)) + + } } diff --git a/protobufs b/protobufs index c4f62d04..e7db7cd7 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit c4f62d047ec898519710feab6882beebe70a124d +Subproject commit e7db7cd74e1709ca719d1fcc9119aa062e82930f From 90a59dd8791b933dd4efac9c513b3801d7f88b73 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Tue, 29 Jul 2025 15:42:00 -0600 Subject: [PATCH 2/3] refactor: formatting Signed-off-by: Rob Walworth --- Tests/HieroTests/TopicMessageSubmitTransactionTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift b/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift index a00955fd..3da4f9e2 100644 --- a/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift +++ b/Tests/HieroTests/TopicMessageSubmitTransactionTests.swift @@ -148,7 +148,8 @@ internal final class TopicMessageSubmitTransactionTests: XCTestCase { XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].accountID.accountNum, Int64(payerId.num)) XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees.count, 1) XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees[0].amount, Int64(amount)) - XCTAssertEqual(tx.scheduledTransactionBody.maxCustomFees[0].fees[0].denominatingTokenID.tokenNum, Int64(tokenId.num)) + XCTAssertEqual( + tx.scheduledTransactionBody.maxCustomFees[0].fees[0].denominatingTokenID.tokenNum, Int64(tokenId.num)) } } From 277295680f317c194da683f26a16821f1dacd167 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Mon, 11 Aug 2025 15:50:21 -0400 Subject: [PATCH 3/3] chore: edit example to test Signed-off-by: Rob Walworth --- Examples/Schedule/main.swift | 70 ++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/Examples/Schedule/main.swift b/Examples/Schedule/main.swift index 0322fe1e..4be8165e 100644 --- a/Examples/Schedule/main.swift +++ b/Examples/Schedule/main.swift @@ -14,6 +14,13 @@ internal enum Program { // by this account and be signed by this key client.setOperator(env.operatorAccountId, env.operatorKey) + let key = PrivateKey.generateEd25519() + let accountId = try await AccountCreateTransaction() + .keyWithoutAlias(.single(key.publicKey)) + .execute(client) + .getReceipt(client) + .accountId! + // Generate a Ed25519 private, public key pair let key1 = PrivateKey.generateEd25519() let key2 = PrivateKey.generateEd25519() @@ -23,62 +30,45 @@ internal enum Program { print("private key 2 = \(key2)") print("public key 2 = \(key2.publicKey)") - let newAccountId = try await AccountCreateTransaction() - .keyWithoutAlias(.keyList([.single(key1.publicKey), .single(key2.publicKey)])) - .initialBalance(.fromTinybars(1000)) + var customFee = CustomFixedFee() + customFee.feeCollectorAccountId = accountId + customFee.amount = 10 + + let topicId = try await TopicCreateTransaction() + .feeScheduleKey(.single(key1.publicKey)) + .addCustomFee(customFee) .execute(client) .getReceipt(client) - .accountId! + .topicId! - print("new account ID: \(newAccountId)") + print("new topic ID: \(topicId)") - let tx = TransferTransaction() - .hbarTransfer(newAccountId, -Hbar(1)) - .hbarTransfer(env.operatorAccountId, Hbar(1)) + var customFeeLimitFee = CustomFixedFee() + customFeeLimitFee.amount = 5 + let customFeeLimit = CustomFeeLimit(payerId: env.operatorAccountId, customFees: [customFeeLimitFee]) - let response = - try await tx + let response = try await TopicMessageSubmitTransaction() + .topicId(topicId) + .message("hello from hashgraph".data(using: .utf8)!) + .addCustomFeeLimit(customFeeLimit) .schedule() - .expirationTime(.now + .days(1)) + .expirationTime(.now + .seconds(3)) .isWaitForExpiry(true) .execute(client) - print("scheduled transaction ID = \(response.transactionId)") + let scheduledTransactionId = response.transactionId + print("scheduled transaction ID = \(scheduledTransactionId)") let scheduleId = try await response.getReceipt(client).scheduleId! print("schedule ID = \(scheduleId)") - let record = try await response.getRecord(client) - print("record = \(record)") - - _ = try await ScheduleSignTransaction() - .scheduleId(scheduleId) - .freezeWith(client) - .sign(key1) - .execute(client) - .getReceipt(client) - - let info = try await ScheduleInfoQuery() - .scheduleId(scheduleId) - .execute(client) - - print("schedule info = \(info)") - - _ = try await ScheduleSignTransaction() - .scheduleId(scheduleId) - .freezeWith(client) - .sign(key2) - .execute(client) - .getReceipt(client) - - let transactionId = response.transactionId + _ = try await Task.sleep(nanoseconds: 1_000_000_000 * 6) - print("The following link should query the mirror node for the scheduled transaction:") + let scheduleInfo = try await ScheduleInfoQuery().scheduleId(scheduleId).execute(client) - let transactionIdString = - "\(transactionId.accountId)-\(transactionId.validStart.seconds)-\(transactionId.validStart.subSecondNanos)" + print("scheduleInfo = \(scheduleInfo)") - print("https://\(env.networkName).mirrornode.hedera.com/api/v1/transactions/\(transactionIdString)") + print("scheduled transaction receipt = \(try await TransactionReceiptQuery().transactionId(scheduledTransactionId).execute(client))") } }