Skip to content

Commit

Permalink
Merge branch 'develop' into feat/decode-error
Browse files Browse the repository at this point in the history
  • Loading branch information
JeneaVranceanu authored Nov 26, 2023
2 parents 71e8902 + bab86ca commit e66dd35
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 32 deletions.
7 changes: 7 additions & 0 deletions Sources/Web3Core/Contract/ContractProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,13 @@ extension DefaultContractProtocol {
return encodedData
}

public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] {
guard let event = events[event] else {
return []
}
return event.encodeParameters(parameters)
}

public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) {
for (eName, ev) in self.events {
if !ev.anonymous {
Expand Down
74 changes: 73 additions & 1 deletion Sources/Web3Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,85 @@ extension ABI.Element.Function {
}
}

// MARK: - Event logs decoding
// MARK: - Event logs decoding & encoding

extension ABI.Element.Event {
public func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? {
guard let eventContent = ABIDecoder.decodeLog(event: self, eventLogTopics: eventLogTopics, eventLogData: eventLogData) else { return nil }
return eventContent
}

public static func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? {
switch input.type {
case .string:
guard let string = value as? String else {
return nil
}
return .string(string.sha3(.keccak256).addHexPrefix())
case .dynamicBytes:
guard let data = ABIEncoder.convertToData(value) else {
return nil
}
return .string(data.sha3(.keccak256).toHexString().addHexPrefix())
case .bytes(length: _):
guard let data = ABIEncoder.convertToData(value), let data = data.setLengthLeft(32) else {
return nil
}
return .string(data.toHexString().addHexPrefix())
case .address, .uint(bits: _), .int(bits: _), .bool:
guard let encoded = ABIEncoder.encodeSingleType(type: input.type, value: value) else {
return nil
}
return .string(encoded.toHexString().addHexPrefix())
default:
guard let data = try? ABIEncoder.abiEncode(value).setLengthLeft(32) else {
return nil
}
return .string(data.toHexString().addHexPrefix())
}
}

public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] {
guard parameters.count <= inputs.count else {
// too many arguments for fragment
return []
}
var topics: [EventFilterParameters.Topic?] = []

if !anonymous {
topics.append(.string(topic.toHexString().addHexPrefix()))
}

for (i, p) in parameters.enumerated() {
let input = inputs[i]
if !input.indexed {
// cannot filter non-indexed parameters; must be null
return []
}
if p == nil {
topics.append(nil)
} else if input.type.isArray || input.type.isTuple {
// filtering with tuples or arrays not supported
return []
} else if let p = p as? Array<Any> {
topics.append(.strings(p.map { Self.encodeTopic(input: input, value: $0) }))
} else {
topics.append(Self.encodeTopic(input: input, value: p!))
}
}

// Trim off trailing nulls
while let last = topics.last {
if last == nil {
topics.removeLast()
} else if case .string(let string) = last, string == nil {
topics.removeLast()
} else {
break
}
}
return topics
}
}

// MARK: - Decode custom error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ extension BigInt: LiteralInitiableFromString { }
extension BigUInt: LiteralInitiableFromString { }

extension Data: LiteralInitiableFromString {
/// Converts hexadecimal string representation of some bytes into actual bytes.
/// Notes:
/// - empty string will return `nil`;
/// - empty hex string, meaning it's equal to `"0x"`, will return empty `Data` object.
/// - Parameter hex: bytes represented as string.
/// - Returns: optional raw bytes.
public static func fromHex(_ hex: String) -> Data? {
let string = hex.lowercased().stripHexPrefix()
let array = [UInt8](hex: string)
if array.count == 0 {
if hex == "0x" || hex == "" {
return Data()
} else {
return nil
}
}
return Data(array)
let hex = hex.lowercased().trim()
guard !hex.isEmpty else { return nil }
guard hex != "0x" else { return Data() }
let bytes = [UInt8](hex: hex.stripHexPrefix())
return bytes.isEmpty ? nil : Data(bytes)
}
}
15 changes: 13 additions & 2 deletions Sources/Web3Core/Transaction/EventfilterParameters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ extension EventFilterParameters {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(fromBlock.description, forKey: .fromBlock)
try container.encode(toBlock.description, forKey: .toBlock)
try container.encode(address.description, forKey: .address)
try container.encode(topics.textRepresentation, forKey: .topics)
try container.encode(address, forKey: .address)
try container.encode(topics, forKey: .topics)
}
}

Expand Down Expand Up @@ -96,6 +96,17 @@ extension EventFilterParameters {
case string(String?)
case strings([Topic?]?)

public func encode(to encoder: Encoder) throws {
switch self {
case let .string(s):
var container = encoder.singleValueContainer()
try container.encode(s)
case let .strings(ss):
var container = encoder.unkeyedContainer()
try container.encode(contentsOf: ss ?? [])
}
}

var rawValue: String {
switch self {
case let .string(string):
Expand Down
39 changes: 21 additions & 18 deletions Sources/Web3Core/Utility/Data+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import Foundation

extension Data {
public extension Data {

init<T>(fromArray values: [T]) {
let values = values
let ptrUB = values.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) in return ptr }
Expand Down Expand Up @@ -33,32 +34,34 @@ extension Data {
return difference == UInt8(0x00)
}

public static func zero(_ data: inout Data) {
static func zero(_ data: inout Data) {
let count = data.count
data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) in
body.baseAddress?.assumingMemoryBound(to: UInt8.self).initialize(repeating: 0, count: count)
}
}

public static func randomBytes(length: Int) -> Data? {
for _ in 0...1024 {
var data = Data(repeating: 0, count: length)
let result = data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) -> Int32? in
if let bodyAddress = body.baseAddress, body.count > 0 {
let pointer = bodyAddress.assumingMemoryBound(to: UInt8.self)
return SecRandomCopyBytes(kSecRandomDefault, length, pointer)
} else {
return nil
}
}
if let notNilResult = result, notNilResult == errSecSuccess {
return data
}
/**
Generates an array of random bytes of the specified length.
This function uses `SecRandomCopyBytes` to generate random bytes returning it as a `Data` object.
If an error occurs during random bytes generation, the function returns `nil`.
Error occurs only if `SecRandomCopyBytes` returns status that is not `errSecSuccess`.
See [all status codes](https://developer.apple.com/documentation/security/1542001-security_framework_result_codes) for possible error reasons.
Note: in v4 of web3swift this function will be deprecated and a new implementation will be provided that will throw occurred error.
- Parameter length: The number of random bytes to generate.

- Returns: optional `Data` object containing the generated random bytes, or `nil` if an error occurred during generation.
*/
static func randomBytes(length: Int) -> Data? {
var entropyBytes = [UInt8](repeating: 0, count: length)
let status = SecRandomCopyBytes(kSecRandomDefault, entropyBytes.count, &entropyBytes)
guard status == errSecSuccess else {
return nil
}
return nil
return Data(entropyBytes)
}

public func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public
func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public
if startingBit + length / 8 > self.count, length > 64, startingBit > 0, length >= 1 { return nil }
let bytes = self[(startingBit/8) ..< (startingBit+length+7)/8]
let padding = Data(repeating: 0, count: 8 - bytes.count)
Expand Down
22 changes: 21 additions & 1 deletion Sources/Web3Core/Utility/String+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ extension String {
let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
let from = from16.samePosition(in: self),
let to = to16.samePosition(in: self)
else { return nil }
else { return nil }
return from ..< to
}

Expand All @@ -139,6 +139,26 @@ extension String {
public var isHex: Bool {
stripHexPrefix().reduce(true, { $0 && $1.isHexDigit } )
}

/// Splits a string into groups of `every` n characters, grouping from left-to-right by default. If `backwards` is true, right-to-left.
public func split(every: Int, backwards: Bool = false) -> [String] {
var result = [String]()

for i in stride(from: 0, to: self.count, by: every) {
switch backwards {
case true:
let endIndex = self.index(self.endIndex, offsetBy: -i)
let startIndex = self.index(endIndex, offsetBy: -every, limitedBy: self.startIndex) ?? self.startIndex
result.insert(String(self[startIndex..<endIndex]), at: 0)
case false:
let startIndex = self.index(self.startIndex, offsetBy: i)
let endIndex = self.index(startIndex, offsetBy: every, limitedBy: self.endIndex) ?? self.endIndex
result.append(String(self[startIndex..<endIndex]))
}
}

return result
}
}

extension Character {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ public extension IEth {
}
}

public extension IEth {
func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] {
try await APIRequest.sendRequest(with: self.provider, for: .getLogs(eventFilter)).result
}
}

public extension IEth {
func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult {
let request = APIRequest.sendTransaction(transaction)
Expand Down
2 changes: 2 additions & 0 deletions Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public protocol IEth {

func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash

func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog]

func gasPrice() async throws -> BigUInt

func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt
Expand Down
90 changes: 90 additions & 0 deletions Tests/web3swiftTests/localTests/EventTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// EventTests.swift
//
//
// Created by liugang zhang on 2023/8/24.
//

import XCTest
import Web3Core
import BigInt

@testable import web3swift

class EventTests: XCTestCase {

/// Solidity event allows up to 3 indexed field, this is just for test.
let testEvent = """
[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"string","name":"a","type":"string"},{"indexed":true,"internalType":"bool","name":"b","type":"bool"},{"indexed":true,"internalType":"bytes","name":"c","type":"bytes"},{"indexed":true,"internalType":"uint256","name":"d","type":"uint256"}],"name":"UserOperationEvent","type":"event"}]
"""

func testEncodeTopicToJSON() throws {
let encoder = JSONEncoder()
let t1: [EventFilterParameters.Topic] = []
let t2: [EventFilterParameters.Topic] = [.string(nil)]
let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])]
let t4: [EventFilterParameters.Topic] = [.strings([nil, .string("1")])]
XCTAssertNoThrow(try encoder.encode(t1))
XCTAssertNoThrow(try encoder.encode(t2))
XCTAssertNoThrow(try encoder.encode(t3))
XCTAssertNoThrow(try encoder.encode(t4))

let topics: [EventFilterParameters.Topic] = [
.string("1"),
.strings([
.string("2"),
.string("3"),
]
)]
let encoded = try encoder.encode(topics)
let json = try JSONSerialization.jsonObject(with: encoded)
XCTAssertEqual(json as? NSArray, ["1", ["2", "3"]])
}

func testEncodeLogs() throws {
let contract = try EthereumContract(testEvent)
let topic = contract.events["UserOperationEvent"]!.topic
let logs = contract.events["UserOperationEvent"]!.encodeParameters(
[
"0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042",
"0x581074D2d9e50913eB37665b07CAFa9bFFdd1640",
"hello,world",
true,
"0x02c16c07e1c68d50",
nil
]
)
XCTAssertEqual(logs.count, 6)

XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix())
XCTAssertTrue(logs[1] == "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042")
XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640")
XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7")
XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001")
XCTAssertTrue(logs[5] == "0x56f5a6cba57d26b32db8dc756fda960dcd3687770a300575a5f8107591eff63f")
}

func testEncodeTopic() throws {
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .string, indexed: true), value: "hello,world") == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: "0x003e36550908907c2a2da960fd19a419b9a774b7") == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: EthereumAddress("0x003e36550908907c2a2da960fd19a419b9a774b7")!) == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: true) == "0x0000000000000000000000000000000000000000000000000000000000000001")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: false) == "0x0000000000000000000000000000000000000000000000000000000000000000")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: BigUInt("dbe20a", radix: 16)!) == "0x0000000000000000000000000000000000000000000000000000000000dbe20a")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: "dbe20a") == "0x0000000000000000000000000000000000000000000000000000000000dbe20a")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .int(bits: 32), indexed: true), value: 100) == "0x0000000000000000000000000000000000000000000000000000000000000064")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .dynamicBytes, indexed: true), value: Data(hex: "6761766f66796f726b")) == "0xe0859ceea0a2fd2474deef2b2183f10f4c741ebba702e9a07d337522c0af55fb")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: Data(hex: "6761766f66796f726b")) == "0x00000000000000000000000000000000000000000000006761766f66796f726b")
XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: "0x6761766f66796f726b") == "0x00000000000000000000000000000000000000000000006761766f66796f726b")
}
}

private func ==(lhs: EventFilterParameters.Topic?, rhs: String?) -> Bool {
if let lhs = lhs, case .string(let string) = lhs {
return string == rhs
}
if lhs == nil && rhs == nil {
return true
}
return false
}
11 changes: 11 additions & 0 deletions Tests/web3swiftTests/localTests/UncategorizedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ class UncategorizedTests: LocalTestCase {
XCTAssert(biguint == BigUInt("126978086000000000"))
}

func testStringSplit() {
XCTAssertEqual("abcdefgh".split(every: 3), ["abc", "def", "gh"])
XCTAssertEqual("abcdefgh".split(every: 3, backwards: true), ["ab", "cde", "fgh"])

XCTAssertEqual("abcdefgh".split(every: 10), ["abcdefgh"])
XCTAssertEqual("".split(every: 3), [])

XCTAssertEqual("abcdefgh".split(every: 1), ["a", "b", "c", "d", "e", "f", "g", "h"])
XCTAssertEqual("abcdefgh".split(every: 1, backwards: true), ["a", "b", "c", "d", "e", "f", "g", "h"]) // should be the same as from the front
}

func testBloom() throws {
let positive = [
"testtest",
Expand Down
Loading

0 comments on commit e66dd35

Please sign in to comment.