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 3 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
56 changes: 55 additions & 1 deletion Sources/Web3Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,67 @@ 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
}

func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? {
if case .string = input.type {
guard let string = value as? String else {
return nil
}
return .string(string.sha3(.keccak256).addHexPrefix())
} else if case .dynamicBytes = input.type {
guard let data = ABIEncoder.convertToData(value) else {
return nil
}
return .string(data.sha3(.keccak256).toHexString().addHexPrefix())
} else if case .address = input.type {
guard let encoded = ABIEncoder.encode(types: [input.type], values: [value]) else {
return nil
}
return .string(encoded.toHexString().addHexPrefix())
}
guard let data = ABIEncoder.convertToData(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(.string(nil))
} else if input.type.isArray {
// filtering with tuples or arrays not supported
return []
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: move it up and potentially combine into a guard statement with input.indexed.

guard input.indexed, !input.type.isArray else { return [] }

This comment was marked as resolved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it out. If we have a event with 3 fields like

event TestEvent(string indexed f1, int[] indexed f2, int32 indexed f3);

When we want to build a filter to query this event with first and third fields, we have to pass nil to second field, and the topicFilter will be like

[
  '0x07c9b1ca6e62a19a2af4d11a151945afc7d47c89c716a74973b49572c1a715f4',
  '0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7',
  null,
  '0x0000000000000000000000000000000000000000000000000000000000000064'
]

If combine input.indexed and input.type.isArray together, we can only build a filter with first one field.

} else if let p = p as? Array<Any> {
topics.append(.strings(p.map { encodeTopic(input: input, value: $0) }))
} else {
topics.append(encodeTopic(input: input, value: p!))
}
}
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
54 changes: 54 additions & 0 deletions Tests/web3swiftTests/localTests/EventTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// EventTests.swift
//
//
// Created by liugang zhang on 2023/8/24.
//

import XCTest
import Web3Core

@testable import web3swift

class EventTests: XCTestCase {
func testEncodeTopc() throws {
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved
let encoder = JSONEncoder()
let t1: [EventFilterParameters.Topic] = []
let t2: [EventFilterParameters.Topic] = [.string(nil)]
let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])]
XCTAssertNoThrow(try encoder.encode(t1))
XCTAssertNoThrow(try encoder.encode(t2))
XCTAssertNoThrow(try encoder.encode(t3))

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

func testEncodeLogs() throws {
let contract = try EthereumContract(TestEvent)
let logs = contract.events["UserOperationEvent"]?.encodeParameters(
[
"0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042",
"0x581074D2d9e50913eB37665b07CAFa9bFFdd1640",
"hello,world",
true,
"0x02c16c07e1c68d50",
nil
]
)

XCTAssert(logs?.count == 7)
}

let TestEvent = """
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved
[{"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"}]
"""
}
30 changes: 30 additions & 0 deletions Tests/web3swiftTests/remoteTests/EventFilterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// EventFilterTests.swift
//
//
// Created by liugang zhang on 2023/8/24.
//

import XCTest
import Web3Core

@testable import web3swift

class EventFilerTests: XCTestCase {

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 block = try await web3.eth.block(by: .latest)
let parameters = EventFilterParameters(fromBlock: .exact(block.number - 1000), address: [address], topics: topics)
let result = try await web3.eth.getLogs(eventFilter: parameters)

// result not always has a log in it.
print(result)
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved
}
}