diff --git a/Examples/SimpleTransferCrypto/main.swift b/Examples/SimpleTransferCrypto/main.swift index 24725f61..aa4e249b 100644 --- a/Examples/SimpleTransferCrypto/main.swift +++ b/Examples/SimpleTransferCrypto/main.swift @@ -14,6 +14,6 @@ public func clientFromEnvironment() -> Client { let client = clientFromEnvironment() .setMaxTransactionFee(100_000_000) -try! client.transferCryptoTo(recipient: AccountId("0.0.2")!, amount: 10000) +_ = try! client.transferCryptoTo(recipient: AccountId("0.0.2")!, amount: 10000) print("Crypto transferred successfully") diff --git a/Package.resolved b/Package.resolved index 7fc5470e..c57271ef 100644 --- a/Package.resolved +++ b/Package.resolved @@ -19,6 +19,15 @@ "version": "0.8.0" } }, + { + "package": "CryptoSwift", + "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", + "state": { + "branch": null, + "revision": "90e5b7af823d869fa8dea5a3abc7d95b6cb04c8c", + "version": "1.1.3" + } + }, { "package": "SwiftGRPC", "repositoryURL": "https://github.com/grpc/grpc-swift", diff --git a/Package.swift b/Package.swift index a1d72f39..465500d2 100644 --- a/Package.swift +++ b/Package.swift @@ -24,11 +24,12 @@ let package = Package( .package(url: "https://github.com/jedisct1/swift-sodium", .exact("0.8.0")), .package(url: "https://github.com/apple/swift-protobuf", .exact("1.6.0")), .package(url: "https://github.com/grpc/grpc-swift", .exact("0.9.1")), + .package(url: "https://github.com/krzyzanowskim/CryptoSwift", .exact("1.1.3")) ], targets: [ .target( name: "Hedera", - dependencies: ["Sodium", "SwiftProtobuf", "SwiftGRPC"]), + dependencies: ["Sodium", "SwiftProtobuf", "SwiftGRPC", "CryptoSwift"]), .testTarget( name: "HederaTests", dependencies: ["Hedera"]), diff --git a/Sources/Hedera/CallParams.swift b/Sources/Hedera/CallParams.swift new file mode 100644 index 00000000..c63cf159 --- /dev/null +++ b/Sources/Hedera/CallParams.swift @@ -0,0 +1,140 @@ +import CryptoSwift + +public final class CallParams { + private let functionSelector: FunctionSelector? + private var args: [Argument] + + public init(funcSelector: FunctionSelector?) { + self.functionSelector = funcSelector + args = [] + } + + private func addParamType(paramType: String) { + _ = try! functionSelector?.addParamType(typeName: paramType) + } + + public func addString(param: String) -> CallParams { + addParamType(paramType: "string") + args.append(Argument(value: encodeString(param))) + + return self + } + + public final class FunctionSelector { + private var state: SelectorState + private var needsComma: Bool + + private enum SelectorState { + case unfinished([UInt8]) + case finished([UInt8]) + } + + public init(_ funcName: String) { + needsComma = false + // 0x28 = '(' + state = .unfinished(Array(funcName.utf8) + [0x28]) + } + + public func addParamType(typeName: String) throws -> FunctionSelector { + guard case var .unfinished(bytes) = state else { + throw HederaError(message: "Can't add type params to FunctionSelectors once they have finished") + } + + bytes += (needsComma ? [0x23] : []) + Array(typeName.utf8) + + state = .unfinished(bytes) + needsComma = true + + return self + } + + public func finishIntermediate() -> [UInt8] { + switch state { + case let .unfinished(bytes): + // 0x29 = ')' + return Digest.sha3(bytes + [0x29], variant: .keccak256) + + case let .finished(hash): + return hash + } + } + + public func finish() -> [UInt8] { + let hash = finishIntermediate() + state = .finished(hash) + + return hash + } + } +} + +fileprivate final class Argument { + private let value: [UInt8] + private let isDynamic: Bool + + // this is a different init function so that it avoids the throws annotation + init(value: [UInt8]) { + self.value = value + self.isDynamic = true + + } + + init(value: [UInt8], isDynamic: Bool) throws { + guard isDynamic || value.count == 32 else { + throw HederaError(message: "invalid argument: value argument that was not 32 bytes") + } + + self.value = value + self.isDynamic = isDynamic + } +} + +private func encodeString(_ param: String) -> [UInt8] { + let strBytes = Array(param.utf8) + + // prepend the size of the string. + return int256(val: Int64(strBytes.count), bitwidth: 32) + padRight(strBytes) +} + +private func encodeBytes(bytes: [UInt8]) -> [UInt8] { + return int256(val: Int64(bytes.count), bitwidth: 32) + padRight(bytes) +} + +private func encodeArray(elements: [[UInt8]], prependLen: Bool) -> [UInt8] { + if (prependLen) { + return int256(val: Int64(elements.count), bitwidth: 32) + elements.joined() + } else { + return Array(elements.joined()) + } +} + +private func int256(val: Int64, bitwidth: UInt8) -> [UInt8] { + // this uses int internally for convinence, + // but uses UInt8 as a param type to ensure + // that nothing tries to use a bitwidth too big. + let bytes = Int(min(bitwidth, 64) / 8) + let remainder = rem(bytes) + var output = Array(repeating: val < 0 ? 0xff : 0x00, count: bytes + remainder) + + for i in stride(from: bytes - 1, through: 0, by: -1) { + output[remainder + i] = UInt8(val >> (i * 8)) + } + + return output +} + +private func padLeft(_ bytes: [UInt8], signExtend: Bool = false) -> [UInt8] { + return Array(repeating: signExtend ? 0xff : 0x00, count: rem(bytes.count)) + bytes +} + +private func padRight(_ bytes: [UInt8]) -> [UInt8] { + return bytes + Array(repeating: 0x00, count: rem(bytes.count)) +} + +private func rem(_ val: Int) -> Int { + guard val % 32 == 0 else { + return 0 + } + + return 32 - val % 32 +} \ No newline at end of file