From 0eea1e6d1b4cdedac4e76eaa7260c0a2524444a0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 26 Oct 2018 04:11:23 +0300 Subject: [PATCH] Added solidity function parser Solidity function encoder now supports any type of data Added a lot of decode functions Fixed RLP crash --- web3swift.xcodeproj/project.pbxproj | 24 ++++ web3swift/ABIv2/SolidityFunction.swift | 128 +++++++----------- web3swift/ABIv2/SolidityTypes.swift | 90 +++++++++--- web3swift/Contracts/ERC20.swift | 1 + web3swift/Convenience/Array+Extension.swift | 5 + web3swift/Convenience/Data+Extension.swift | 38 +++++- web3swift/Convenience/Int+Sequence.swift | 24 ++++ .../DataManagers/SolidityDataWriter.swift | 90 ++++++++++++ web3swift/Utils/RLP.swift | 2 +- .../Web3/Web3+TransactionIntermediate.swift | 70 ++++++++-- web3swiftTests/ABITests.swift | 1 + .../BetterABI/SolidityFunctionTests.swift | 128 ++++++++++++++++++ web3swiftTests/TransactionTests.swift | 6 + 13 files changed, 501 insertions(+), 106 deletions(-) create mode 100644 web3swift/Convenience/Int+Sequence.swift create mode 100644 web3swift/DataManagers/SolidityDataWriter.swift create mode 100644 web3swiftTests/BetterABI/SolidityFunctionTests.swift diff --git a/web3swift.xcodeproj/project.pbxproj b/web3swift.xcodeproj/project.pbxproj index c00efd6..ece1336 100644 --- a/web3swift.xcodeproj/project.pbxproj +++ b/web3swift.xcodeproj/project.pbxproj @@ -27,11 +27,16 @@ 1323B8062177B85F00306BBB /* ERC721.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1323B8042177B85F00306BBB /* ERC721.swift */; }; 1323B80B2177BA9700306BBB /* ERC777.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1323B80A2177BA9700306BBB /* ERC777.swift */; }; 1323B80C2177BA9700306BBB /* ERC777.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1323B80A2177BA9700306BBB /* ERC777.swift */; }; + 134B10CF2182447600113663 /* Int+Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 134B10CE2182447600113663 /* Int+Sequence.swift */; }; + 134B10D02182447600113663 /* Int+Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 134B10CE2182447600113663 /* Int+Sequence.swift */; }; 13A24E4E21629CBC004C48A0 /* Promise+Web3+Eth+EstimateGas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A1823F20D79FDB0016741F /* Promise+Web3+Eth+EstimateGas.swift */; }; 13A24E51216365A4004C48A0 /* EthURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13A24E50216365A4004C48A0 /* EthURL.swift */; }; 13A24E52216365A4004C48A0 /* EthURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13A24E50216365A4004C48A0 /* EthURL.swift */; }; 13A24EE7216F988C004C48A0 /* UInt256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13A24EE6216F988C004C48A0 /* UInt256.swift */; }; 13A24EE8216F9899004C48A0 /* UInt256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13A24EE6216F988C004C48A0 /* UInt256.swift */; }; + 13FD60DF21821A1100D73D4E /* SolidityFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FD60DE21821A1100D73D4E /* SolidityFunctionTests.swift */; }; + 13FD60F521822FAE00D73D4E /* SolidityDataWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FD60F421822FAE00D73D4E /* SolidityDataWriter.swift */; }; + 13FD60F621822FAE00D73D4E /* SolidityDataWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13FD60F421822FAE00D73D4E /* SolidityDataWriter.swift */; }; 1CD91B321FD769A6007BFB45 /* MainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD91B311FD769A6007BFB45 /* MainTests.swift */; }; 1CD91B341FD769A6007BFB45 /* web3swift_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD91AFC1FD76910007BFB45 /* web3swift_iOS.framework */; }; 3790E56739A28FF8E4CF5A5A /* libPods-web3swift-macOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B431175BA31FF402281E09CD /* libPods-web3swift-macOS.a */; }; @@ -257,8 +262,11 @@ 1323B8042177B85F00306BBB /* ERC721.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ERC721.swift; sourceTree = ""; }; 1323B80A2177BA9700306BBB /* ERC777.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ERC777.swift; sourceTree = ""; }; 1323B80D2178C16700306BBB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 134B10CE2182447600113663 /* Int+Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Sequence.swift"; sourceTree = ""; }; 13A24E50216365A4004C48A0 /* EthURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthURL.swift; sourceTree = ""; }; 13A24EE6216F988C004C48A0 /* UInt256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UInt256.swift; sourceTree = ""; }; + 13FD60DE21821A1100D73D4E /* SolidityFunctionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidityFunctionTests.swift; sourceTree = ""; }; + 13FD60F421822FAE00D73D4E /* SolidityDataWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolidityDataWriter.swift; sourceTree = ""; }; 1CD91AFC1FD76910007BFB45 /* web3swift_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = web3swift_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1CD91AFF1FD76910007BFB45 /* web3swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = web3swift.h; sourceTree = ""; }; 1CD91B001FD76910007BFB45 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -434,6 +442,7 @@ isa = PBXGroup; children = ( 1323B7FF2177A83000306BBB /* BetterERC20Tests.swift */, + 13FD60DE21821A1100D73D4E /* SolidityFunctionTests.swift */, ); path = BetterABI; sourceTree = ""; @@ -456,6 +465,14 @@ path = Contracts; sourceTree = ""; }; + 13FD60F321822F9700D73D4E /* DataManagers */ = { + isa = PBXGroup; + children = ( + 13FD60F421822FAE00D73D4E /* SolidityDataWriter.swift */, + ); + path = DataManagers; + sourceTree = ""; + }; 1CD91AF21FD76910007BFB45 = { isa = PBXGroup; children = ( @@ -483,6 +500,7 @@ 1CD91AFE1FD76910007BFB45 /* web3swift */ = { isa = PBXGroup; children = ( + 13FD60F321822F9700D73D4E /* DataManagers */, 13A24EDF216E4BF5004C48A0 /* Contracts */, 13A24E4F21636541004C48A0 /* EthURL */, 81FECD43211ADE20006DA367 /* ObjectiveCbridge */, @@ -686,6 +704,7 @@ 8113D2B01FD7E1590074282C /* CryptoExtensions.swift */, 13A24EE6216F988C004C48A0 /* UInt256.swift */, 8113D2B21FD7E1590074282C /* LibSecp256k1Extension.swift */, + 134B10CE2182447600113663 /* Int+Sequence.swift */, 8113DE7B1FD8514400CD8DF1 /* NSRegularExpressionExtension.swift */, 817EBB1E2006265400E02EAA /* Base58.swift */, 817EBB22200649E000E02EAA /* RIPEMD160+StackOveflow.swift */, @@ -1042,6 +1061,7 @@ 81D7D97220A31FB700A193EC /* ComparisonExtensions.swift in Sources */, 817EBB27200673D100E02EAA /* KeystoreV3JSONStructure.swift in Sources */, 81C5DA11207254F600424CD6 /* ABIv2.swift in Sources */, + 13FD60F521822FAE00D73D4E /* SolidityDataWriter.swift in Sources */, 1323B7F72176255900306BBB /* SolidityTypes.swift in Sources */, 817EBB102004FE2800E02EAA /* EthereumAddress.swift in Sources */, 81FB21FE207BB297007F9A83 /* EIP67Code.swift in Sources */, @@ -1100,6 +1120,7 @@ 1323B80B2177BA9700306BBB /* ERC777.swift in Sources */, 81A1821520D5A2700016741F /* Promise+Web3+Eth+GetBalance.swift in Sources */, 0073F22820D94A11000791F1 /* BlockExplorer+GetTransactionHistory.swift in Sources */, + 134B10CF2182447600113663 /* Int+Sequence.swift in Sources */, 0073F22620D949F7000791F1 /* BlockExplorer.swift in Sources */, 81C0FD012044A89300D82FAF /* Web3+TransactionIntermediate.swift in Sources */, 81A1823C20D79C270016741F /* Promise+Web3+Eth+Call.swift in Sources */, @@ -1130,6 +1151,7 @@ E23B5ADF20EA68FA00DC7F32 /* SECP256K1Tests.swift in Sources */, 1323B7F42174BB1E00306BBB /* Support.swift in Sources */, 00E5FE7E20EA3A3F0030E0D6 /* ABITests.swift in Sources */, + 13FD60DF21821A1100D73D4E /* SolidityFunctionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1139,6 +1161,7 @@ files = ( 4194811B203630530065A83B /* Web3+HttpProvider.swift in Sources */, 4194811E203630530065A83B /* Web3.swift in Sources */, + 134B10D02182447600113663 /* Int+Sequence.swift in Sources */, 4194811F203630530065A83B /* Web3+Instance.swift in Sources */, 41948120203630530065A83B /* Web3+Contract.swift in Sources */, 41948121203630530065A83B /* Web3+Utils.swift in Sources */, @@ -1199,6 +1222,7 @@ 81C0FD052044A8AE00D82FAF /* Web3+Protocols.swift in Sources */, 81A1823520D6E1FD0016741F /* Promise+Web3+Eth+GetBlockByNumber.swift in Sources */, 81A1822920D678BF0016741F /* Promise+Web3+Eth+GetAccounts.swift in Sources */, + 13FD60F621822FAE00D73D4E /* SolidityDataWriter.swift in Sources */, 81C5DA322074EC1E00424CD6 /* ContractProtocol.swift in Sources */, 81C5DA232072DFE600424CD6 /* ABIv2TypeParser.swift in Sources */, 81A1824C20D7DF1B0016741F /* Promise+Web3+Personal+UnlockAccount.swift in Sources */, diff --git a/web3swift/ABIv2/SolidityFunction.swift b/web3swift/ABIv2/SolidityFunction.swift index 0842baa..5638dbd 100644 --- a/web3swift/ABIv2/SolidityFunction.swift +++ b/web3swift/ABIv2/SolidityFunction.swift @@ -12,9 +12,15 @@ import PromiseKit public protocol SolidityDataRepresentable { var solidityData: Data { get } + var isSolidityBinaryType: Bool { get } } +public extension SolidityDataRepresentable { + var isSolidityBinaryType: Bool { return false } +} + extension BinaryInteger { public var solidityData: Data { return BigInt(self).abiEncode(bits: 256) } + } extension Int: SolidityDataRepresentable {} extension Int8: SolidityDataRepresentable {} @@ -33,13 +39,47 @@ extension EthereumAddress: SolidityDataRepresentable { } extension Data: SolidityDataRepresentable { public var solidityData: Data { return self } + public var isSolidityBinaryType: Bool { return true } } extension String: SolidityDataRepresentable { public var solidityData: Data { return data } + public var isSolidityBinaryType: Bool { return true } } -extension Array where Element == SolidityDataRepresentable { +extension Array: SolidityDataRepresentable where Element == SolidityDataRepresentable { + public var solidityData: Data { + var data = Data(capacity: 32 * count) + for element in self { + data.append(element.solidityData) + } + return data + } + +// func dynamicSolidityData() -> Data { +// var data = Data(capacity: 32 * (count+1)) +// data.append(count.solidityData) +// for element in self { +// data.append(element.solidityData) +// } +// return data +// } +// func staticSolidityData(count: Int) -> Data { +// let capacity = 32 * count +// var data = Data(capacity: capacity) +// for element in self { +// data.append(element.solidityData) +// } +// if data.count < capacity { +// data.append(Data(count: capacity - data.count)) +// } +// return data +// } func data(function: String) -> Data { - return reduce(into: function.keccak256()[0..<4], { $0.append($1.solidityData) }) + var data = Data(capacity: count * 32 + 4) + data.append(function.keccak256()[0..<4]) + for element in self { + data.append(element.solidityData) + } + return data } } @@ -47,9 +87,8 @@ extension EthereumAddress { public func assemble(_ function: String, _ arguments: [Any], web3: Web3 = .default, options: Web3Options? = nil, onBlock: String = "pending") -> Promise { let options = web3.options.merge(with: options) - let data = arguments.compactMap { value in - return value as? SolidityDataRepresentable - }.data(function: function) + let function = try! SolidityFunction(function: function) + let data = function.encode(arguments as! [SolidityDataRepresentable]) var assembledTransaction = EthereumTransaction(to: self, data: data, options: options) let queue = web3.requestDispatcher.queue let returnPromise = Promise { seal in @@ -106,7 +145,8 @@ extension EthereumAddress { } public func call(_ function: String, _ arguments: [Any], web3: Web3 = .default, options: Web3Options? = nil, onBlock: String = "latest") -> Promise { let options = web3.options.merge(with: options) - let data = arguments.compactMap { $0 as? SolidityDataRepresentable }.data(function: function) + let function = try! SolidityFunction(function: function) + let data = function.encode(arguments as! [SolidityDataRepresentable]) let assembledTransaction = EthereumTransaction(to: self, data: data, options: options) let queue = web3.requestDispatcher.queue return Promise { seal in @@ -125,7 +165,8 @@ extension EthereumAddress { } public func estimateGas(_ function: String, _ arguments: [Any], web3: Web3 = .default, options: Web3Options? = nil, onBlock: String = "latest") -> Promise { let options = web3.options.merge(with: options) - let data = arguments.compactMap { $0 as? SolidityDataRepresentable }.data(function: function) + let function = try! SolidityFunction(function: function) + let data = function.encode(arguments as! [SolidityDataRepresentable]) let assembledTransaction = EthereumTransaction(to: self, data: data, options: options) let queue = web3.requestDispatcher.queue return Promise { seal in @@ -139,76 +180,3 @@ extension EthereumAddress { } } } - -//public class UnsafeSolidityFunction { -// public let name: String -// public init(_ name: String) { -// self.name = name -// } -// public var hash: Data { -// return name.data.sha3(.keccak256)[0..<4] -// } -// public func data(with arguments: [SolidityDataRepresentable]) -> Data { -// return arguments.reduce(into: hash, { $0.append($1.solidityData) }) -// } -// public func data(with arguments: SolidityDataRepresentable...) -> Data { -// return data(with: arguments) -// } -//} - -//protocol SolidityConvertable { -// func write(type: SolidityType) throws -//} -//enum SolidityType { -// case address -// case -//} - -//extension String { -// var solidityType: String { -// switch self { -// case "uint": -// return "uint256" -// case "int": -// return "int256" -// default: -// return self -// } -// } -//} -// - -//class SafeSolidityFunction: CustomStringConvertible { -// enum Error: Swift.Error { -// case corrupted -// case emptyFunctionName -// } -// let name: String -// let arguments: [String] -// init(function: String) throws { -// var function = function.trimmingCharacters(in: .whitespacesAndNewlines) -// guard let index = function.index(of: "(") else { throw Error.corrupted } -// name = function[.. 0 else { throw Error.emptyFunctionName } -// guard function.hasSuffix(")") else { throw Error.corrupted } -// function.removeLast() -// let arguments = function[function.index(after: index)...] -// self.arguments = arguments.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } -// } -// var description: String { -// return "\(name)(\(arguments.joined(separator: ",")))" -// } -//} - -/* - converts: - "balanceOf(address)" - "transfer(address,address,uint256)" - "transfer(address, address, uint256)" - "transfer(address, address, uint256)" - "transfer (address, address, uint)" - " transfer ( address , address , uint256 ) " - to - .name: String - .arguments: [String] - */ diff --git a/web3swift/ABIv2/SolidityTypes.swift b/web3swift/ABIv2/SolidityTypes.swift index 27c77c5..7b1af31 100644 --- a/web3swift/ABIv2/SolidityTypes.swift +++ b/web3swift/ABIv2/SolidityTypes.swift @@ -9,7 +9,10 @@ import Foundation import BigInt -/* abandoned for some period +public enum AbiError: Error { + /// Unsupported types: function, tuple + case unsupportedType +} /* types: @@ -44,9 +47,10 @@ public class SolidityType: Equatable, CustomStringConvertible { public var arraySize: ArraySize { return .notArray } public var subtype: SolidityType? { return nil } public var memoryUsage: Int { return 32 } - public var `default`: Any { return Data() } + public var `default`: Data { return Data(count: memoryUsage) } public var description: String { return "" } public var isValid: Bool { return true } + public var isSupported: Bool { return true } public static func == (lhs: SolidityType, rhs: SolidityType) -> Bool { return lhs.description == rhs.description } @@ -61,7 +65,6 @@ public class SolidityType: Equatable, CustomStringConvertible { super.init() } public override var description: String { return "uint\(bits)" } - public override var `default`: Any { return BigUInt(0) } public override var isValid: Bool { switch bits { case 8,16,32,64,128,256: return true @@ -74,19 +77,18 @@ public class SolidityType: Equatable, CustomStringConvertible { } public class SolidityAddress: SolidityType { public override var description: String { return "address" } - public override var `default`: Any { return EthereumAddress("0x0000000000000000000000000000000000000000") } } + + /// Unsupported public class SolidityFunctionType: SolidityType { public override var description: String { return "function" } - public override var `default`: Any { return Data(repeating: 0, count: 24) } + public override var isSupported: Bool { return false } } public class SolidityBool: SolidityType { public override var description: String { return "bool" } - public override var `default`: Any { return false } } public class SolidityBytes: SolidityType { public override var description: String { return "bytes\(count)" } - public override var `default`: Any { return Data(repeating: 0, count: count) } public override var isValid: Bool { return count > 0 && count <= 32 } var count: Int init(count: Int) { @@ -96,14 +98,12 @@ public class SolidityType: Equatable, CustomStringConvertible { } public class SolidityStaticArray: SolidityType { public override var description: String { return "\(type)[\(count)]" } - public override var `default`: Any { return Array(repeating: type.default, count: count) } public override var isStatic: Bool { return type.isStatic } public override var isArray: Bool { return true } public override var subtype: SolidityType? { return type } public override var arraySize: ArraySize { return .static(count) } public override var isValid: Bool { return type.isValid } public override var memoryUsage: Int { - guard isStatic else { return 32 } return 32 * count } var count: Int @@ -116,17 +116,17 @@ public class SolidityType: Equatable, CustomStringConvertible { } public class SolidityDynamicBytes: SolidityType { public override var description: String { return "bytes" } - public override var `default`: Any { return Data() } + public override var memoryUsage: Int { return 0 } public override var isStatic: Bool { return false } } public class SolidityString: SolidityType { public override var description: String { return "string" } - public override var `default`: Any { return "" } public override var isStatic: Bool { return false } + public override var memoryUsage: Int { return 0 } } public class SolidityDynamicArray: SolidityType { public override var description: String { return "\(type)[]" } - public override var `default`: Any { return [] } + public override var memoryUsage: Int { return 0 } public override var isStatic: Bool { return type.isStatic } public override var isArray: Bool { return true } public override var subtype: SolidityType? { return type } @@ -138,11 +138,13 @@ public class SolidityType: Equatable, CustomStringConvertible { super.init() } } + + /// Unsupported public class SolidityTuple: SolidityType { public override var description: String { return "tuple(\(types.map { $0.description }.joined(separator: ",")))" } - public override var `default`: Any { return [] } public override var isStatic: Bool { return types.allSatisfy { $0.isStatic } } public override var isTuple: Bool { return true } + public override var isSupported: Bool { return false } public override var memoryUsage: Int { guard isStatic else { return 32 } return types.reduce(0, { $0 + $1.memoryUsage }) @@ -232,7 +234,6 @@ extension SolidityType { return type } public static func scan(type string: String) throws -> SolidityType { - let string = string.trimmingCharacters(in: .whitespacesAndNewlines) for (index,character) in string.enumerated() { switch character { case "(": @@ -258,5 +259,62 @@ extension SolidityType { } } } - -*/ + + +/// Converts: +/// "balanceOf(address)" +/// "transfer(address,address,uint256)" +/// "transfer(address, address, uint256)" +/// "transfer(address, address, uint256)" +/// "transfer (address, address, uint)" +/// " transfer ( address , address , uint256 ) " +/// To: +/// function.name: String +/// function.types: [SolidityType] +/// +/// Mainthread-friendly +/// Performance: +/// transfer(uint256,address) // ~184k ops +/// transfer(uint256,address,address,bytes32,uint256[32]) // performance ~100k ops + +public class SolidityFunction: CustomStringConvertible { + public enum Error: Swift.Error { + case corrupted + case emptyFunctionName + } + public let name: String + public let types: [SolidityType] + public let function: String + public lazy var hash: Data = self.function.keccak256()[0..<4] + public init(function: String) throws { + let function = function.replacingOccurrences(of: " ", with: "") + self.function = function + guard let index = function.index(of: "(") else { throw Error.corrupted } + name = String(function[.. 0 else { throw Error.emptyFunctionName } + guard function.hasSuffix(")") else { throw Error.corrupted } + let arguments = function[function.index(after: index).. Data { + return encode(arguments) + } + public func encode(_ arguments: [SolidityDataRepresentable]) -> Data { + let data = SolidityDataWriter() + for i in 0.. BigUInt { + return try address.call("allowance(address,address)", owner, spender, options: options).wait().uint256() } diff --git a/web3swift/Convenience/Array+Extension.swift b/web3swift/Convenience/Array+Extension.swift index bf06268..3f91d50 100644 --- a/web3swift/Convenience/Array+Extension.swift +++ b/web3swift/Convenience/Array+Extension.swift @@ -15,4 +15,9 @@ extension Array { return Array(self[$0 ..< $0.advanced(by: endIndex)]) } } + func safe(_ index: Int) -> Element? { + guard (0..) in + for i in 0.. String { + var string = "" + string.reserveCapacity(count*2+count/separateEvery*separator.count) + var separateCount = separateEvery + withUnsafeBytes { (bytes: UnsafePointer) in + for i in 0.. Data? { let string = hex.lowercased().withoutHex let array = Array(hex: string) @@ -87,3 +113,13 @@ public extension Data { return uintRepresentation } } + +extension UInt8 { + public var hex: String { + if self < 0x10 { + return "0" + String(self, radix: 16) + } else { + return String(self, radix: 16) + } + } +} diff --git a/web3swift/Convenience/Int+Sequence.swift b/web3swift/Convenience/Int+Sequence.swift new file mode 100644 index 0000000..c3860aa --- /dev/null +++ b/web3swift/Convenience/Int+Sequence.swift @@ -0,0 +1,24 @@ +// +// Int+Sequence.swift +// web3swift +// +// Created by Dmitry on 25/10/2018. +// Copyright © 2018 Bankex Foundation. All rights reserved. +// + +import Foundation + +extension Range: IteratorProtocol where Bound == Int { + public mutating func next() -> Bound? { + guard lowerBound + 1 < upperBound else { return nil } + self = lowerBound+1.. Range { + return -1.. +} diff --git a/web3swift/DataManagers/SolidityDataWriter.swift b/web3swift/DataManagers/SolidityDataWriter.swift new file mode 100644 index 0000000..010a1e2 --- /dev/null +++ b/web3swift/DataManagers/SolidityDataWriter.swift @@ -0,0 +1,90 @@ +// +// SolidityDataWriter.swift +// web3swift +// +// Created by Dmitry on 25/10/2018. +// Copyright © 2018 Bankex Foundation. All rights reserved. +// + +import Foundation +import BigInt + +private struct SolidityDataPointer { + var data: Data + var position: Int + var arraySize: Int +} +private extension Data { + mutating func write(data: Data, at position: Int) { + replaceSubrange(position.. size { + replaceSubrange(size..., with: Data()) + } else if count < size { + append(Data(count: size-count)) + } + } + mutating func append(count: Int) { + append(Data(count: count)) + } + mutating func normalizeSize() { + let expectedSize = (count-1) / 0x20 * 0x20 + 0x20 + guard count < expectedSize else { return } + append(count: expectedSize - count) + } +} +class SolidityDataWriter { + private var data: Data + private var dynamicData = [SolidityDataPointer]() + var offset = 0 + init() { + self.data = Data() + } + init(data: Data) { + self.data = data + } + init(count: Int) { + data = Data(count: count) + } + func write(type: SolidityType) { + var data = type.default + data.extend(to: type.memoryUsage) + if !type.isStatic { + let arraySize = ceil(Double(data.count) / Double(type.memoryUsage)) + let pointer = SolidityDataPointer(data: data, position: self.data.count, arraySize: Int(arraySize)) + self.data.append(count: 32) + dynamicData.append(pointer) + } else { + self.data.append(data) + } + } + func write(value: SolidityDataRepresentable, type: SolidityType) { + var data = value.solidityData + if type.memoryUsage > 0 { + data.extend(to: type.memoryUsage) + } + if !type.isStatic { + let arraySize = value.isSolidityBinaryType ? data.count : data.count / 32 + data.normalizeSize() + let pointer = SolidityDataPointer(data: data, position: self.data.count, arraySize: Int(arraySize)) + self.data.append(count: 32) + dynamicData.append(pointer) + } else { + self.data.append(data) + } + } + func done() -> Data { + for pointer in dynamicData { + data.write(data: data.count.solidityData, at: pointer.position-offset) + if pointer.data.count == 0 { + data.append(0.solidityData) + } else { + data.append(pointer.arraySize.solidityData) + data.append(pointer.data) + } + } + dynamicData.removeAll() + return data + } +} diff --git a/web3swift/Utils/RLP.swift b/web3swift/Utils/RLP.swift index acab9c2..b390675 100644 --- a/web3swift/Utils/RLP.swift +++ b/web3swift/Utils/RLP.swift @@ -291,7 +291,7 @@ struct RLP { } else if prefixByte <= 0xF7 && length > BigUInt(prefixByte - 0xC0) { let listLen = BigUInt(prefixByte - 0xC0) return (1, listLen, .list) - } else if try prefixByte <= 0xFF && length > BigUInt(prefixByte - 0xF7) && length > BigUInt(prefixByte - 0xF7) + toBigUInt(slice(data: input, offset: BigUInt(1), length: BigUInt(prefixByte - 0xF7))) { + } else if try prefixByte >= 0xF7 && length > BigUInt(prefixByte - 0xF7) && length > BigUInt(prefixByte - 0xF7) + toBigUInt(slice(data: input, offset: BigUInt(1), length: BigUInt(prefixByte - 0xF7))) { let lengthOfListLength = BigUInt(prefixByte - 0xF7) let listLength = try toBigUInt(slice(data: input, offset: BigUInt(1), length: BigUInt(prefixByte - 0xF7))) return (1 + lengthOfListLength, listLength, .list) diff --git a/web3swift/Web3/Web3+TransactionIntermediate.swift b/web3swift/Web3/Web3+TransactionIntermediate.swift index 02194b5..d546938 100644 --- a/web3swift/Web3/Web3+TransactionIntermediate.swift +++ b/web3swift/Web3/Web3+TransactionIntermediate.swift @@ -13,6 +13,13 @@ import PromiseKit public enum Web3ResponseError: Error { case notFound case wrongType + case overflows +} + +private extension Int { + var solidityFormatted: Int { + return (self / 32 + 1) * 32 + } } public class Web3DataResponse { @@ -22,22 +29,23 @@ public class Web3DataResponse { self.data = data } public func uint256() throws -> BigUInt { - return try BigUInt(data(32)) + return try BigUInt(next(32)) } public func address() throws -> EthereumAddress { try skip(12) - return try EthereumAddress(data(20)) + return try EthereumAddress(next(20)) } public func bool() throws -> Bool { - let value = try BigUInt(data(32)) + let value = try BigUInt(next(32)) guard value < 2 else { throw Web3ResponseError.wrongType } return value == 1 } public func string() throws -> String { return try pointer { - let length = try uint256() - guard length <= Int.max else { throw Web3ResponseError.wrongType } - guard let string = try String(data: data(Int(length)), encoding: .utf8) else { throw Web3ResponseError.wrongType } + let length = try intCount() + guard length > 0 else { return "" } + let data = try self.next(length) + guard let string = String(data: data, encoding: .utf8) else { throw Web3ResponseError.wrongType } return string } } @@ -47,19 +55,65 @@ public class Web3DataResponse { guard end <= data.count else { throw Web3ResponseError.notFound } position = end } - public func data(_ size: Int) throws -> Data { + public func next(_ size: Int) throws -> Data { let range = position..(block: ()throws->T) throws -> T { - let pointer = try uint256() + let pointer = try intCount() let pos = position + position = pointer defer { position = pos } return try block() } } +public extension Web3DataResponse { + private func unsigned(max: BigUInt) throws -> T { + let number = try uint256() + guard number <= max else { throw Web3ResponseError.overflows } + return T(number) + } + private func signed(min: BigInt, max: BigInt) throws -> T { + let number = try uint256() + guard number >= min && number <= max else { throw Web3ResponseError.overflows } + return T(number) + } + func uint8() throws -> UInt8 { + return try unsigned(max: 0xff) + } + func uint16() throws -> UInt16 { + return try unsigned(max: 0xffff) + } + func uint32() throws -> UInt32 { + return try unsigned(max: 0xffffffff) + } + func uint64() throws -> UInt64 { + return try unsigned(max: 0xffffffffffffffff) + } + func uint() throws -> Int64 { + return try unsigned(max: BigUInt(UInt.max)) + } + func int8() throws -> Int8 { + return try signed(min: -0x80, max: 0x7f) + } + func int16() throws -> Int16 { + return try signed(min: -0x8000, max: 0x7fff) + } + func int32() throws -> Int32 { + return try signed(min: -0x80000000, max: 0x7fffffff) + } + func int64() throws -> Int64 { + return try signed(min: -0x8000000000000000, max: 0x7fffffffffffffff) + } + func int() throws -> Int64 { + return try signed(min: BigInt(Int.min), max: BigInt(Int.max)) + } + func intCount() throws -> Int { + return try signed(min: 0, max: BigInt(Int.max)) + } +} public class Web3Response { let dictionary: [String: Any] diff --git a/web3swiftTests/ABITests.swift b/web3swiftTests/ABITests.swift index 2a693e4..70494d4 100644 --- a/web3swiftTests/ABITests.swift +++ b/web3swiftTests/ABITests.swift @@ -40,6 +40,7 @@ class ABITests: XCTestCase { try record.parse() }) print(abiNative) + XCTAssert(true, "Failed to parse ABI") } diff --git a/web3swiftTests/BetterABI/SolidityFunctionTests.swift b/web3swiftTests/BetterABI/SolidityFunctionTests.swift new file mode 100644 index 0000000..3547a08 --- /dev/null +++ b/web3swiftTests/BetterABI/SolidityFunctionTests.swift @@ -0,0 +1,128 @@ +// +// SolidityFunctionTests.swift +// web3swift-iOS_Tests +// +// Created by Dmitry on 25/10/2018. +// Copyright © 2018 Bankex Foundation. All rights reserved. +// + +import XCTest +import BigInt +@testable import web3swift_iOS + +private func scan(type: String) throws -> SolidityType { + return try SolidityType.scan(type: type) +} + +class SolidityFunctionTests: XCTestCase { + func testTypes() { + try XCTAssertNoThrow(scan(type: "uint256")) + try XCTAssertNoThrow(scan(type: "uint256")) + try XCTAssertNoThrow(scan(type: "uint128")) + try XCTAssertNoThrow(scan(type: "uint64")) + try XCTAssertNoThrow(scan(type: "uint32")) + try XCTAssertNoThrow(scan(type: "uint16")) + try XCTAssertNoThrow(scan(type: "uint8")) + try XCTAssertThrowsError(scan(type: "uint")) + try XCTAssertThrowsError(scan(type: "uint0")) + try XCTAssertThrowsError(scan(type: "uint1")) + try XCTAssertThrowsError(scan(type: "uint257")) + try XCTAssertThrowsError(scan(type: "uint512")) + + try XCTAssertNoThrow(scan(type: "int256")) + try XCTAssertNoThrow(scan(type: "int128")) + try XCTAssertNoThrow(scan(type: "int64")) + try XCTAssertNoThrow(scan(type: "int32")) + try XCTAssertNoThrow(scan(type: "int16")) + try XCTAssertNoThrow(scan(type: "int8")) + try XCTAssertThrowsError(scan(type: "int")) + try XCTAssertThrowsError(scan(type: "int0")) + try XCTAssertThrowsError(scan(type: "int1")) + try XCTAssertThrowsError(scan(type: "int257")) + try XCTAssertThrowsError(scan(type: "int512")) + + // array + try XCTAssertNoThrow(scan(type: "uint256[]")) + try XCTAssertNoThrow(scan(type: "uint256[1]")) + try XCTAssertNoThrow(scan(type: "uint256[32]")) + try XCTAssertNoThrow(scan(type: "uint256[33]")) + try XCTAssertThrowsError(scan(type: "uint256]")) + try XCTAssertThrowsError(scan(type: "uint256[")) + try XCTAssertThrowsError(scan(type: "uint256[0]")) + + + try XCTAssertNoThrow(scan(type: "function")) + try XCTAssertNoThrow(scan(type: "address")) + try XCTAssertNoThrow(scan(type: "bool")) + try XCTAssertNoThrow(scan(type: "string")) + try XCTAssertThrowsError(scan(type: "aksjdjalksd")) + try XCTAssertThrowsError(scan(type: "")) + try XCTAssertThrowsError(scan(type: "tuple(")) + + try XCTAssertNoThrow(scan(type: "bytes")) + try XCTAssertNoThrow(scan(type: "bytes1")) + try XCTAssertNoThrow(scan(type: "bytes32")) + try XCTAssertThrowsError(scan(type: "bytes33")) + try XCTAssertThrowsError(scan(type: "bytes[")) + try XCTAssertThrowsError(scan(type: "bytes]")) + try XCTAssertThrowsError(scan(type: "bytes[0]")) + try XCTAssertThrowsError(scan(type: "bytes[33]")) + + try XCTAssertNoThrow(scan(type: "tuple(uint256)")) + try XCTAssertNoThrow(scan(type: "tuple(uint256,uint256)")) + try XCTAssertNoThrow(scan(type: "tuple(address,string)")) + try XCTAssertNoThrow(scan(type: "tuple(uint256,address,address,bytes32,uint256[64])")) + try XCTAssertThrowsError(scan(type: "uint256(uint256,address,tuple(address,bytes32,uint256[64]))")) + try XCTAssertThrowsError(scan(type: "string(uint256,address,tuple(address,bytes32,uint256[64]))")) + + try XCTAssertThrowsError(scan(type: "tuple(tuple))")) + } + + func testEncodeAndDecode() throws { + let user: EthereumAddress = "0x6394b37Cf80A7358b38068f0CA4760ad49983a1B" + let function = try SolidityFunction(function: "transfer(address,uint256)") + let data = function.encode(user, 800) + + let reader = Web3DataResponse(data) + let a = try reader.address() + let b = try reader.uint256() + XCTAssertEqual(a, user) + XCTAssertEqual(b, 800) + } + + func pr(_ value: BigInt) { + print(value.solidityData.hex, value) + } + + func testEncodeAndDecodeString() throws { + let user: EthereumAddress = "0x6394b37Cf80A7358b38068f0CA4760ad49983a1B" + + let function = try! SolidityFunction(function: "send(address,string,uint256)") + let data = function.encode(user, "hello world", 800) + + let reader = Web3DataResponse(data) + let a = try! reader.address() + let message = try! reader.string() + let b = try! reader.uint256() + XCTAssertEqual(a, user) + XCTAssertEqual(message, "hello world") + XCTAssertEqual(b, 800) + } + + + func testEncodeAndDecodeBigString() throws { + let user: EthereumAddress = "0x6394b37Cf80A7358b38068f0CA4760ad49983a1B" + let bigString = "mfiejrh9183yrnv190y3r0m9x17yc90172ymr093 daosjdokamsfl vopadsjnfmowedfoiwlfknlkvkdmf omfp qejfpiqwejfop[w mfewfm qmef fe" + + let function = try! SolidityFunction(function: " send ( address, string ,uint256 ) ") + let data = function.encode(user, bigString, 800) + + let reader = Web3DataResponse(data) + let a = try! reader.address() + let message = try! reader.string() + let b = try! reader.uint256() + XCTAssertEqual(a, user) + XCTAssertEqual(message, bigString) + XCTAssertEqual(b, 800) + } +} diff --git a/web3swiftTests/TransactionTests.swift b/web3swiftTests/TransactionTests.swift index 3eac671..b57788e 100644 --- a/web3swiftTests/TransactionTests.swift +++ b/web3swiftTests/TransactionTests.swift @@ -111,6 +111,12 @@ class TransactionsTests: XCTestCase { let contract = try web3.contract(Web3.Utils.erc20ABI, at: contractAddress) try contract.method("transfer", args: coldWalletAddress, BigUInt(1), options: options).call(options: nil) } + + func testRawTransaction() { + let transactionString = "0xa9059cbb00000000000000000000000083b0b52a887a4c05429ee6d4619afeb8007c1a330000000000000000000000000000000000000000000000000001c6bf52634000" + let transactionData = Data.fromHex(transactionString)! + let rawTransaction = EthereumTransaction.fromRaw(transactionData) + } // func testTokenBalanceTransferOnMainNetUsingConvenience() throws { // // BKX TOKEN