diff --git a/web3.swift.podspec b/web3.swift.podspec index b5073b00..094aeeaa 100644 --- a/web3.swift.podspec +++ b/web3.swift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'web3.swift' - s.version = '0.7.2' + s.version = '0.8.0' s.license = 'MIT' s.summary = 'Ethereum API for Swift' s.homepage = 'https://github.com/argentlabs/web3.swift' diff --git a/web3sTests/Account/EthereumAccount+SignTransactionTests.swift b/web3sTests/Account/EthereumAccount+SignTransactionTests.swift index 98da2944..40a6b8ad 100644 --- a/web3sTests/Account/EthereumAccount+SignTransactionTests.swift +++ b/web3sTests/Account/EthereumAccount+SignTransactionTests.swift @@ -33,7 +33,7 @@ class EthereumAccount_SignTransactionTests: XCTestCase { let tx = EthereumTransaction(from: nil, to: to, value: value, data: nil, nonce: nonce, gasPrice: gasPrice, gasLimit: gasLimit, chainId: chainID) let account = try! EthereumAccount.init(keyStorage: TestEthereumKeyStorage(privateKey: "0x4646464646464646464646464646464646464646464646464646464646464646")) - let signed = try! account.sign(tx) + let signed = try! account.sign(transaction: tx) let v = signed.v.web3.hexString let r = signed.r.web3.hexString diff --git a/web3sTests/Contract/ABIDecoderTests.swift b/web3sTests/Contract/ABIDecoderTests.swift index a791f73e..715707fb 100644 --- a/web3sTests/Contract/ABIDecoderTests.swift +++ b/web3sTests/Contract/ABIDecoderTests.swift @@ -309,5 +309,71 @@ class ABIDecoderTests: XCTestCase { print(error.localizedDescription) } } + + func test_GivenSimpleTuple_ThenDecodesCorrectly() { + do { + let value = try ABIDecoder.decodeData("0x00000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe56793000000000000000000000000000000000000000000000000000000000000001e", types: [SimpleTuple.self]) + + XCTAssertEqual(try value[0].decoded(), SimpleTuple(address: EthereumAddress("0x64d0ea4fc60f27e74f1a70aa6f39d403bbe56793"), amount: BigUInt(30))) + } catch { + print(error.localizedDescription) + XCTFail() + } + } + + func test_testGivenDynamicContentTuple_ThenDecodesCorrectly() { + do { + let value = try ABIDecoder.decodeData("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000036162630000000000000000000000000000000000000000000000000000000000", types: [DynamicContentTuple.self]) + + XCTAssertEqual(try value[0].decoded(), DynamicContentTuple(message: "abc")) + } catch { + print(error.localizedDescription) + XCTFail() + } + } + + func test_testGivenLongTuple_ThenDecodesCorrectly() { + do { + let value = try ABIDecoder.decodeData("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001007efef35dcd300eec8819c4ce5cb6b57be685254d583954273c5cc16edee83790ba42a7d804d9eff383efb1864514f5f15c82f1c333a777dd8f76dba1c1977029000000000000000000000000000000000000000000000000000000000000005668747470733a2f2f697066732e666c65656b2e636f2f697066732f62616679626569623774726c746c66353637647171336a766f6b37336b3776706e6b7864713367663665766a326c74657a7a7a7a6871756336656100000000000000000000000000000000000000000000000000000000000000000000000000000000005668747470733a2f2f697066732e666c65656b2e636f2f697066732f62616679626569657a706165676378796c74707733716a6d78746678716961646471627264727a6f79766d62616768716468776875756c6963697900000000000000000000", types: [LongTuple.self]) + + XCTAssertEqual(try value[0].decoded(), LongTuple(value1: "https://ipfs.fleek.co/ipfs/bafybeib7trltlf567dqq3jvok73k7vpnkxdq3gf6evj2ltezzzzhquc6ea", + value2: "https://ipfs.fleek.co/ipfs/bafybeiezpaegcxyltpw3qjmxtfxqiaddqbrdrzoyvmbaghqdhwhuuliciy", + value3: Data(hex: "0x7efef35dcd300eec8819c4ce5cb6b57be685254d583954273c5cc16edee83790")!, + value4: Data(hex: "0xba42a7d804d9eff383efb1864514f5f15c82f1c333a777dd8f76dba1c1977029")!)) + } catch { + print(error.localizedDescription) + XCTFail() + } + } + + func test_testGivenArrayOfTuples_ThenDecodesCorrectly() { + do { + let value = try ABIDecoder.decodeData("0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000064d0ea4fc60f27e74f1a70aa6f39d403bbe56793000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000003c1bd6b420448cf16a389c8b0115ccb3660bb8540000000000000000000000000000000000000000000000000000000000000078", types: [ABIArray.self]) + + XCTAssertEqual(try value[0].decodedTupleArray(), [ + SimpleTuple(address: EthereumAddress("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56793"), amount: 30), + SimpleTuple(address: EthereumAddress("0x3C1Bd6B420448Cf16A389C8b0115CCB3660bB854"), amount: 120)]) + } catch { + print(error.localizedDescription) + XCTFail() + } + } + + func test_GivenArrayOfLongTuples_WhenHasOneEntry_ThenDecodesCorrectly() { + do { + let value = try ABIDecoder.decodeData("0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001007efef35dcd300eec8819c4ce5cb6b57be685254d583954273c5cc16edee83790ba42a7d804d9eff383efb1864514f5f15c82f1c333a777dd8f76dba1c1977029000000000000000000000000000000000000000000000000000000000000005668747470733a2f2f697066732e666c65656b2e636f2f697066732f62616679626569623774726c746c66353637647171336a766f6b37336b3776706e6b7864713367663665766a326c74657a7a7a7a6871756336656100000000000000000000000000000000000000000000000000000000000000000000000000000000005668747470733a2f2f697066732e666c65656b2e636f2f697066732f62616679626569657a706165676378796c74707733716a6d78746678716961646471627264727a6f79766d62616768716468776875756c6963697900000000000000000000", types: [ABIArray.self]) + + XCTAssertEqual(try value[0].decodedTupleArray(), + [ + LongTuple(value1: "https://ipfs.fleek.co/ipfs/bafybeib7trltlf567dqq3jvok73k7vpnkxdq3gf6evj2ltezzzzhquc6ea", + value2: "https://ipfs.fleek.co/ipfs/bafybeiezpaegcxyltpw3qjmxtfxqiaddqbrdrzoyvmbaghqdhwhuuliciy", + value3: Data(hex: "0x7efef35dcd300eec8819c4ce5cb6b57be685254d583954273c5cc16edee83790")!, + value4: Data(hex: "0xba42a7d804d9eff383efb1864514f5f15c82f1c333a777dd8f76dba1c1977029")!) + + ]) + } catch { + print(error.localizedDescription) + XCTFail() + } + } } - diff --git a/web3sTests/Contract/ABIFunctionEncoderTests.swift b/web3sTests/Contract/ABIFunctionEncoderTests.swift index b49434d8..3b2dfba4 100644 --- a/web3sTests/Contract/ABIFunctionEncoderTests.swift +++ b/web3sTests/Contract/ABIFunctionEncoderTests.swift @@ -283,7 +283,7 @@ class ABIFunctionEncoderTests: XCTestCase { } } -fileprivate struct SimpleTuple: ABITuple { +struct SimpleTuple: ABITuple, Equatable { static var types: [ABIType.Type] { [EthereumAddress.self, BigUInt.self] } var address: EthereumAddress @@ -308,7 +308,7 @@ fileprivate struct SimpleTuple: ABITuple { var encodableValues: [ABIType] { [address, amount] } } -fileprivate struct LongTuple: ABITuple { +struct LongTuple: ABITuple, Equatable { static var types: [ABIType.Type] { [String.self, String.self, Data32.self, Data32.self] } var value1: String @@ -343,7 +343,7 @@ fileprivate struct LongTuple: ABITuple { var encodableValues: [ABIType] { [value1, value2, value3, value4] } } -fileprivate struct DynamicContentTuple: ABITuple { +struct DynamicContentTuple: ABITuple, Equatable { static var types: [ABIType.Type] { [String.self] } var message: String @@ -425,7 +425,7 @@ fileprivate struct RelayerExecute: ABIFunction { } } -fileprivate struct NumberTuple: ABITuple { +struct NumberTuple: ABITuple, Equatable { func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(value) } @@ -445,7 +445,7 @@ fileprivate struct NumberTuple: ABITuple { var encodableValues: [ABIType] { [value] } } -fileprivate struct TupleOfTuples: ABITuple { +struct TupleOfTuples: ABITuple { func encode(to encoder: ABIFunctionEncoder) throws { try encoder.encode(value1) try encoder.encode(value2) diff --git a/web3sTests/ERC721/ERC721Tests.swift b/web3sTests/ERC721/ERC721Tests.swift index 8ee578f7..6b6722ca 100644 --- a/web3sTests/ERC721/ERC721Tests.swift +++ b/web3sTests/ERC721/ERC721Tests.swift @@ -11,6 +11,7 @@ import BigInt @testable import web3 let tokenOwner = EthereumAddress("0x69F84b91E7107206E841748C2B52294A1176D45e") +let previousOwner = EthereumAddress("0x64d0ea4fc60f27e74f1a70aa6f39d403bbe56793") let nonOwner = EthereumAddress("0x64d0eA4FC60f27E74f1a70Aa6f39D403bBe56792") let nftImageURL = URL(string: "https://ipfs.io/ipfs/QmUDJMmiJEsueLbr6jxh7vhSSFAvjfYTLC64hgkQm1vH2C/graph.svg")! let nftURL = URL(string: "https://ipfs.io/ipfs/QmUtKP7LnZnL2pWw2ERvNDndP9v5EPoJH7g566XNdgoRfE")! @@ -71,7 +72,7 @@ class ERC721Tests: XCTestCase { waitForExpectations(timeout: 10) } - func test_GivenAddressWithTransfer_FindsTransferEvent() { + func test_GivenAddressWithTransfer_FindsInTransferEvent() { let expect = expectation(description: "Events") erc721.transferEventsTo(recipient: tokenOwner, @@ -80,7 +81,7 @@ class ERC721Tests: XCTestCase { toBlock: .Number( 6948276), completion: { (error, events) in - XCTAssertEqual(events?.first?.from, EthereumAddress("0x64d0ea4fc60f27e74f1a70aa6f39d403bbe56793")) + XCTAssertEqual(events?.first?.from, previousOwner) XCTAssertEqual(events?.first?.to, tokenOwner) XCTAssertEqual(events?.first?.tokenId, 23) expect.fulfill() @@ -88,6 +89,24 @@ class ERC721Tests: XCTestCase { waitForExpectations(timeout: 10) } + + func test_GivenAddressWithTransfer_FindsOutTransferEvent() { + let expect = expectation(description: "Events") + + erc721.transferEventsFrom(sender: previousOwner, + fromBlock: .Number( + 6948276), + toBlock: .Number( + 6948276), + completion: { (error, events) in + XCTAssertEqual(events?.first?.to, tokenOwner) + XCTAssertEqual(events?.first?.from, previousOwner) + XCTAssertEqual(events?.first?.tokenId, 23) + expect.fulfill() + }) + + waitForExpectations(timeout: 10) + } } class ERC721MetadataTests: XCTestCase { diff --git a/web3swift/src/Account/EthereumAccount+SignTransaction.swift b/web3swift/src/Account/EthereumAccount+SignTransaction.swift index 91eadb63..2a1eacbe 100644 --- a/web3swift/src/Account/EthereumAccount+SignTransaction.swift +++ b/web3swift/src/Account/EthereumAccount+SignTransaction.swift @@ -16,14 +16,14 @@ enum EthereumSignerError: Error { public extension EthereumAccount { func signRaw(_ transaction: EthereumTransaction) throws -> Data { - let signed: SignedTransaction = try sign(transaction) + let signed: SignedTransaction = try sign(transaction: transaction) guard let raw = signed.raw else { throw EthereumSignerError.unknownError } return raw } - internal func sign(_ transaction: EthereumTransaction) throws -> SignedTransaction { + internal func sign(transaction: EthereumTransaction) throws -> SignedTransaction { guard let raw = transaction.raw else { throw EthereumSignerError.emptyRawTransaction diff --git a/web3swift/src/Account/EthereumAccount.swift b/web3swift/src/Account/EthereumAccount.swift index 880eb30a..2200e549 100644 --- a/web3swift/src/Account/EthereumAccount.swift +++ b/web3swift/src/Account/EthereumAccount.swift @@ -23,7 +23,7 @@ protocol EthereumAccountProtocol { func sign(hex: String) throws -> Data func sign(message: Data) throws -> Data func sign(message: String) throws -> Data - func sign(_ transaction: EthereumTransaction) throws -> SignedTransaction + func sign(transaction: EthereumTransaction) throws -> SignedTransaction } public enum EthereumAccountError: Error { diff --git a/web3swift/src/Client/EthereumClient.swift b/web3swift/src/Client/EthereumClient.swift index ba9a51d3..7ec20c59 100644 --- a/web3swift/src/Client/EthereumClient.swift +++ b/web3swift/src/Client/EthereumClient.swift @@ -246,7 +246,7 @@ public class EthereumClient: EthereumClientProtocol { transaction.chainId = network.intValue } - guard let _ = transaction.chainId, let signedTx = (try? account.sign(transaction)), let transactionHex = signedTx.raw?.web3.hexString else { + guard let _ = transaction.chainId, let signedTx = (try? account.sign(transaction: transaction)), let transactionHex = signedTx.raw?.web3.hexString else { group.leave() return completion(EthereumClientError.encodeIssue, nil) } diff --git a/web3swift/src/Client/Models/EthereumTransaction.swift b/web3swift/src/Client/Models/EthereumTransaction.swift index af41d3e4..a29d6acb 100644 --- a/web3swift/src/Client/Models/EthereumTransaction.swift +++ b/web3swift/src/Client/Models/EthereumTransaction.swift @@ -132,8 +132,8 @@ public struct EthereumTransaction: EthereumTransactionProtocol, Equatable, Codab } } -struct SignedTransaction { - let transaction: EthereumTransaction +public struct SignedTransaction { + public let transaction: EthereumTransaction let v: Int let r: Data let s: Data @@ -145,13 +145,13 @@ struct SignedTransaction { self.s = s.web3.strippingZeroesFromBytes } - var raw: Data? { + public var raw: Data? { let txArray: [Any?] = [transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to.value.web3.noHexPrefix, transaction.value, transaction.data, self.v, self.r, self.s] return RLP.encode(txArray) } - var hash: Data? { + public var hash: Data? { return raw?.web3.keccak256 } } diff --git a/web3swift/src/Contract/ABIDecoder.swift b/web3swift/src/Contract/ABIDecoder.swift index aae30503..54181409 100644 --- a/web3swift/src/Contract/ABIDecoder.swift +++ b/web3swift/src/Contract/ABIDecoder.swift @@ -147,18 +147,45 @@ public class ABIDecoder { try deepDecode(data: data, type: arrayType, result: &result, offset: &newOffset, size: &size) return result - case .Tuple: - throw ABIError.notCurrentlySupported + case .Tuple(let types): + var result: [String] = [] + + if type.isDynamic { + guard let offsetHex = (try decode(data, forType: ABIRawType.FixedUInt(256), offset: offset)).first else { + throw ABIError.invalidValue + } + + let tail = Array(data.dropFirst(Int(hex: offsetHex) ?? offset)) + var newOffset = 0 + for type in types { + result += try decode( + tail, + forType: type, + offset: newOffset) + newOffset += type.memory + } + + return result + } else { + var newOffset = offset + for type in types { + result += try decode( + Array(data.dropFirst(newOffset)), + forType: type, + offset: 0) + newOffset += type.memory + } + + return result + } } } private static func deepDecode(data: [UInt8], type: ABIRawType, result: inout [String], offset: inout Int, size: inout Int) throws -> Void { if size < 1 { return } - guard let stringValue = (try decode(data, forType: type, offset: offset)).first else { - throw ABIError.invalidValue - } - result.append(stringValue) + let decoded = try decode(data, forType: type, offset: offset) + result.append(contentsOf: decoded) offset += type.memory size -= 1 diff --git a/web3swift/src/Contract/ABIRawType.swift b/web3swift/src/Contract/ABIRawType.swift index 43432d39..26d4ebe6 100644 --- a/web3swift/src/Contract/ABIRawType.swift +++ b/web3swift/src/Contract/ABIRawType.swift @@ -125,6 +125,15 @@ extension ABIRawType: RawRepresentable { } } + var isTuple: Bool { + switch self { + case .Tuple: + return true + default: + return false + } + } + var isPaddedInDynamic: Bool { switch self { case .FixedUInt, .FixedInt: @@ -155,6 +164,8 @@ extension ABIRawType: RawRepresentable { switch self { case .FixedArray(let type, let size): return type.memory * size + case .Tuple(let types): + return types.map(\.memory).reduce(0, +) default: return 32 } diff --git a/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift b/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift index a583d854..4ea04d2d 100644 --- a/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIDecoder+Static.swift @@ -35,6 +35,29 @@ extension ABIDecoder { return parsed } + + public func decodedTupleArray() throws -> [T] { + let parse = T.parser + + let tupleElements = T.types.count + let size = entry.count / tupleElements + + var parsed = [T]() + var leftElements = entry + while leftElements.count >= tupleElements { + let slice = Array(leftElements[0.. [DecodedValue] { diff --git a/web3swift/src/Contract/Statically Typed/ABIRawType+Static.swift b/web3swift/src/Contract/Statically Typed/ABIRawType+Static.swift index 724a343d..ae8720d6 100644 --- a/web3swift/src/Contract/Statically Typed/ABIRawType+Static.swift +++ b/web3swift/src/Contract/Statically Typed/ABIRawType+Static.swift @@ -123,8 +123,12 @@ extension ABITuple { } public static var parser: ParserFunction { return { data in - let first = data.first ?? "" - return try ABIDecoder.decode(first, to: String.self) + let values = data.map { ABIDecoder.DecodedValue(entry: [$0]) } + guard let decoded = try? self.init(values: values) else { + throw ABIError.invalidValue + } + + return decoded } } } diff --git a/web3swift/src/ERC721/ERC721.swift b/web3swift/src/ERC721/ERC721.swift index e72af364..5cdb45ec 100644 --- a/web3swift/src/ERC721/ERC721.swift +++ b/web3swift/src/ERC721/ERC721.swift @@ -52,6 +52,29 @@ public class ERC721: ERC165 { } } } + + public func transferEventsFrom(sender: EthereumAddress, + fromBlock: EthereumBlock, + toBlock: EthereumBlock, + completion: @escaping((Error?, [ERC721Events.Transfer]?) -> Void)) { + guard let result = try? ABIEncoder.encode(sender).bytes, let sig = try? ERC721Events.Transfer.signature() else { + completion(EthereumSignerError.unknownError, nil) + return + } + + client.getEvents(addresses: nil, + topics: [ sig, String(hexFromBytes: result)], + fromBlock: fromBlock, + toBlock: toBlock, + eventTypes: [ERC721Events.Transfer.self]) { (error, events, unprocessedLogs) in + + if let events = events as? [ERC721Events.Transfer] { + return completion(error, events) + } else { + return completion(error ?? EthereumClientError.decodeIssue, nil) + } + } + } } public class ERC721Metadata: ERC721 {