Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement eth_getLogs #830

Merged
merged 5 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Sources/Web3Core/Contract/ContractProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,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: - Function input/output decoding
Expand Down
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
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
}
46 changes: 46 additions & 0 deletions Tests/web3swiftTests/remoteTests/EventFilterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// EventFilterTests.swift
//
//
// Created by liugang zhang on 2023/8/24.
//

import XCTest
import Web3Core
import BigInt
import CryptoSwift
@testable import web3swift

class EventFilerTests: XCTestCase {

/// This test tx can be found at here:
/// https://etherscan.io/tx/0x1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004
func testErc20Transfer() async throws {
let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken)
let address = EthereumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")!
let erc20 = ERC20(web3: web3, provider: web3.provider, address: address)

let topics = erc20.contract.contract.event("Transfer", parameters: [
"0x003e36550908907c2a2da960fd19a419b9a774b7"
])

let parameters = EventFilterParameters(fromBlock: .exact(17983395), toBlock: .exact(17983395), address: [address], topics: topics)
let result = try await web3.eth.getLogs(eventFilter: parameters)

XCTAssertEqual(result.count, 1)

let log = result.first!
XCTAssertEqual(log.address.address.lowercased(), "0xdac17f958d2ee523a2206206994597c13d831ec7")
XCTAssertEqual(log.transactionHash.toHexString().lowercased(), "1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004")

let logTopics = log.topics.map { $0.toHexString() }
topics.compactMap { t -> String? in
if let t = t, case EventFilterParameters.Topic.string(let topic) = t {
return topic
}
return nil
}.forEach { t in
XCTAssertTrue(logTopics.contains(t.stripHexPrefix()))
}
}
}