Skip to content

Commit

Permalink
Add EIP3668 CCIP Read support
Browse files Browse the repository at this point in the history
  • Loading branch information
hboon committed Apr 4, 2022
1 parent 155772a commit e41e4da
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 2 deletions.
6 changes: 6 additions & 0 deletions web3swift.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@
81FB2207207BCFF9007F9A83 /* bankex-foundation-logo-coin.png in Resources */ = {isa = PBXBuildFile; fileRef = 81FB2206207BCFF9007F9A83 /* bankex-foundation-logo-coin.png */; };
81FB2208207BCFF9007F9A83 /* bankex-foundation-logo-coin.png in Resources */ = {isa = PBXBuildFile; fileRef = 81FB2206207BCFF9007F9A83 /* bankex-foundation-logo-coin.png */; };
B350A445E5DB35C60E59AD70 /* libPods-web3swift-macOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F8C9C48884592DCF561393 /* libPods-web3swift-macOS.a */; };
C840E57127FADA610045B616 /* CcipRead.swift in Sources */ = {isa = PBXBuildFile; fileRef = C840E57027FADA610045B616 /* CcipRead.swift */; };
C840E57227FADA610045B616 /* CcipRead.swift in Sources */ = {isa = PBXBuildFile; fileRef = C840E57027FADA610045B616 /* CcipRead.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -358,6 +360,7 @@
A9ADDE40292A17C21B8D5516 /* Pods-web3swift-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swift-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3swift-iOS/Pods-web3swift-iOS.release.xcconfig"; sourceTree = "<group>"; };
B48CA58D134401D3C4E8CCC5 /* Pods_Web3Swift_osx.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Web3Swift_osx.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B5AFAFC5440E52BE57C7BA13 /* Pods_web3swiftTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_web3swiftTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C840E57027FADA610045B616 /* CcipRead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CcipRead.swift; sourceTree = "<group>"; };
CA3F7E825AEBF3455D00150A /* Pods-web3swift-macOS_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swift-macOS_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-web3swift-macOS_Tests/Pods-web3swift-macOS_Tests.debug.xcconfig"; sourceTree = "<group>"; };
CDCB852B5E2E84636B80BB99 /* Pods-web3swift-iOS_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swift-iOS_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-web3swift-iOS_Tests/Pods-web3swift-iOS_Tests.release.xcconfig"; sourceTree = "<group>"; };
FB43EC035C593F9E5A3644B6 /* Pods-web3swift-macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-web3swift-macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-web3swift-macOS/Pods-web3swift-macOS.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -523,6 +526,7 @@
isa = PBXGroup;
children = (
81FB21F8207BA78B007F9A83 /* EIP67Code.swift */,
C840E57027FADA610045B616 /* CcipRead.swift */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -1045,6 +1049,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C840E57127FADA610045B616 /* CcipRead.swift in Sources */,
81C146F71FF274B200AA943E /* Web3+Structures.swift in Sources */,
8104E2281FE82BDC00F952CB /* Web3+Utils.swift in Sources */,
818ABD5D1FE95FC9002657BB /* Web3+Contract.swift in Sources */,
Expand Down Expand Up @@ -1236,6 +1241,7 @@
81FB21F5207814F7007F9A83 /* Web3+EventOperations.swift in Sources */,
8103BBCD2077B84400499769 /* PlainKeystore.swift in Sources */,
4194813D203630530065A83B /* RIPEMD160+StackOveflow.swift in Sources */,
C840E57227FADA610045B616 /* CcipRead.swift in Sources */,
4194813E203630530065A83B /* Data+Extension.swift in Sources */,
81A1822F20D67BC30016741F /* Promise+Web3+Eth+GetTransactionReceipt.swift in Sources */,
81A1824620D7B91B0016741F /* Promise+Web3+Intermediate+Send.swift in Sources */,
Expand Down
7 changes: 5 additions & 2 deletions web3swift/Promises/Classes/Promise+Web3+Eth+Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ extension web3.Eth {
throw Web3Error.processingError("Transaction is invalid")
}
let rp = web3.dispatch(request)
return rp.map(on: queue ) { response in
return rp.then(on: queue ) { response -> Promise<Data> in
guard let value: Data = response.getValue() else {
if response.error != nil {
if let ccipRead = CcipRead(web3: self.web3, options: self.options, onBlock: onBlock, fromDataString: response.error?.data) {
return ccipRead.process()
}
throw Web3Error.nodeError(response.error!.message)
}
throw Web3Error.nodeError("Invalid value from Ethereum node")
}
return value
return Promise.value(value)
}
} catch {
let returnPromise = Promise<Data>.pending()
Expand Down
263 changes: 263 additions & 0 deletions web3swift/Utils/Classes/CcipRead.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//
// CcipRead.swift
// web3swift
//
// Created by Hwee-Boon Yar on Apr/4/22.
//

import Foundation
import PromiseKit

//https://eips.ethereum.org/EIPS/eip-3668#client-lookup-protocol
class CcipRead {
private let web3: web3
private let options: Web3Options
private let onBlock: String
private let urls: [String]
private let sender: EthereumAddress
private let callbackSelector: String
private let callData: String
private let extraData: String

init?(web3: web3, options: Web3Options, onBlock: String, fromDataString dataString: String?) {
if let dataString = dataString, let (urls, sender, callbackSelector, callData, extraData) = Self.extractCcipRead(fromDataString: dataString) {
self.web3 = web3
self.options = options
self.onBlock = onBlock
self.urls = urls
self.sender = sender
self.callbackSelector = callbackSelector
self.callData = callData
self.extraData = extraData
} else {
return nil
}
}

func process() -> Promise<Data> {
firstly {
fetchCcipJsonRpcCallbackPayloadHexString(urls: urls)
}.then { payload in
CcipRead.ethCall(web3: self.web3, options: self.options, onBlock: self.onBlock, address: self.sender, payload: payload)
}
}

private func fetchCcipJsonRpcCallbackPayloadHexString(urls: [String]) -> Promise<String> {
struct NoValidResultsFromAllCcipReadGateWayUrlsError: Error {}
guard !urls.isEmpty else { return Promise(error: NoValidResultsFromAllCcipReadGateWayUrlsError()) }
var urls = urls
let url = urls.removeFirst()
return firstly {
_fetchCcipJsonRpcCallbackPayloadHexString(url: url)
}.recover { error -> Promise<String> in
return self.fetchCcipJsonRpcCallbackPayloadHexString(urls: urls)
}
}

private func _fetchCcipJsonRpcCallbackPayloadHexString(url rawUrl: String) -> Promise<String> {
//url eg = "https://offchain-resolver-example.uc.r.appspot.com/{sender}/{data}.json"
let senderString = sender.address.lowercased().addHexPrefix()
let dataString = callData.lowercased().addHexPrefix()
guard let url = URL(string: rawUrl.replacingOccurrences(of: "{sender}", with: senderString).replacingOccurrences(of: "{data}", with: dataString)) else {
struct InvalidCcipReadGateWayUrl: Error {}
return Promise(error: InvalidCcipReadGateWayUrl())
}
//CCIP Read, GET or POST accordingly
var request = URLRequest(url: url)
if rawUrl.contains("{data}") {
request.httpMethod = "GET"
} else {
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = [
"sender": senderString,
"data": dataString
]
do {
let jsonData = try JSONEncoder().encode(body)
request.httpBody = jsonData
} catch {
struct EncodeCCIPReadGatewayPayloadAsJsonError: Error {}
return Promise(error: EncodeCCIPReadGatewayPayloadAsJsonError())
}
}

let session = URLSession(configuration: .default)
return Promise { seal in
let dataTask = session.dataTask(with: request) { data, response, error in
if let error = error {
seal.reject(error)
} else if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
if let dict = json as? [String: Any] {
if let result = dict["data"] as? String {
let payload = Self.buildCcipJsonRpcCallbackPayloadHexString(callbackSelector: self.callbackSelector, ccipGatewayCallResult: result, extraData: self.extraData)
seal.fulfill(payload)
}
}
}
struct InvalidCcipReadGatewayFetchResultError: Error {}
seal.reject(InvalidCcipReadGatewayFetchResultError())
}
}
dataTask.resume()
}
}

//TODO this might trigger a CCIP Read recursively too, needs a counter to limit infinite recursion
private static func ethCall(web3: web3, options: Web3Options, onBlock: String, address: EthereumAddress, payload: String) -> Promise<Data> {
let eth = SafeWeb3.Eth(provider : web3.provider, web3: web3)
//Empty `Web3Options()` so `gasLimit` is not passed in
let options = Web3Options()
let transaction = EthereumTransaction(to: address, data: Data(hex: payload), options: options)
return eth.callPromise(transaction, options: options, onBlock: onBlock)
}

//Must not convert `urls` to `[URL]` since URLs can't contain "{sender}" and "{data}"
private static func extractCcipRead(fromDataString dataString: String?) -> (urls: [String], sender: EthereumAddress, callbackSelector: String, callData: String, extraData: String)? {
guard let dataString = dataString?.addHexPrefix() else { return nil }
//OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)
let hashInterfaceForOffChainLookup = "0x556f1830"
//count - 2 to exclude "0x"
if dataString.hasPrefix(hashInterfaceForOffChainLookup), (dataString.count - 2) / 2 % 32 == 4 {
let (urls, sender, callbackSelector, callData, extraData) = _extractCcipRead(dataString: dataString)
if let sender = EthereumAddress(sender.addHexPrefix(), ignoreChecksum: true) {
return (urls, sender, callbackSelector, callData, extraData)
} else {
return nil
}
} else {
return nil
}
}

private static func _extractCcipRead(dataString: String) -> (urls: [String], sender: String, callbackSelector: String, callData: String, extraData: String) {
let dataString: String = {
//8 characters for 4 bytes interface hash and +2 for "0x"
let result = String(dataString.dropFirst(8 + 2))
//Defensive
if result.count % 2 == 0 {
return result
} else {
return "0\(result)"
}
}()

var start = 0
var end = start + 32*2
let rawSender: String = dataString[start..<end]
//20 byte address -> 40 in hex
let sender: String = String(rawSender.dropFirst(rawSender.count - 40))

start = 32*2
end = start + 32*2
let urlsOffsetRaw = dataString[start..<end]
let urlsOffset = Int(urlsOffsetRaw, radix: 16)!

start = urlsOffset * 2
end = start + 32*2
let urlsLengthRaw = dataString[start..<end]
let urlsLength = Int(urlsLengthRaw, radix: 16)!

start = urlsOffset*2 + 32*2
end = dataString.count
let urlsData = dataString[start..<end]

var urls: [String?] = []
for i in 0..<urlsLength {
let offsetStart = i*32*2
let urlRaw = parseBytes(data: urlsData, start: offsetStart)
let url = String(data: Data(hex: urlRaw), encoding: .utf8)
urls.append(url)
}

let callDataOffsetStart = 64*2
let callData = parseBytes(data: dataString, start: callDataOffsetStart)

start = 96*2
end = start + 4*2
let callbackSelector = dataString[start..<end]

let extraData = parseBytes(data: dataString, start: 128*2)

return (urls.compactMap { $0 }, sender, callbackSelector, callData, extraData)
}

private static func buildCcipJsonRpcCallbackPayloadHexString(callbackSelector: String, ccipGatewayCallResult ccipGatewayCallResultRaw: String, extraData: String) -> String {
let ccipGatewayCallResult = ccipGatewayCallResultRaw.stripHexPrefix()
//CCIP callback function has args (bytes,bytes)
let d: Data = Data(hex: callbackSelector) + encodeBytes(datas: [ccipGatewayCallResult, extraData])
return d.toHexString()
}

private static func parseBytes(data: String, start: Int) -> String {
let offsetStart = start
let offSetEnd = offsetStart + 32*2
let offset = data[offsetStart..<offSetEnd]

let lengthStartRaw = offset
let lengthStart = Int(lengthStartRaw, radix: 16)! * 2
let lengthEnd = lengthStart + 32*2
let lengthRaw = data[lengthStart..<lengthEnd]

let length = Int(lengthRaw, radix: 16)! * 2
let raw = data[lengthEnd..<(lengthEnd+length)]
return raw
}

private static func encodeBytes(datas: [String]) -> Data {
var result: [Data] = []
var byteCount = 0
//Placeholders for pointers to items
for _ in datas {
result.append(Data())
byteCount += 32
}

for (i, each) in datas.enumerated() {
let data = Data(hex: datas[i])
result[i] = byteCount.numberToPaddedBytes()
let count = data.count.numberToPaddedBytes()
result.append(count)
let paddedData = data.bytesPadded()
result.append(paddedData)
byteCount += 32 + paddedData.count
}
return Data(result.joined())
}
}

fileprivate extension Data {
func leftPaddedDataWithZero(toLength: Int) -> Data {
let paddingCount = toLength - count
if paddingCount > 0 {
return Data(repeating: 0, count: paddingCount) + self
} else {
return self
}
}

func rightPaddedDataWithZero(toLength: Int) -> Data {
let paddingCount = toLength - count
if paddingCount > 0 {
return self + Data(repeating: 0, count: paddingCount)
} else {
return self
}
}

func bytesPadded() -> Data {
let paddingCount = 32 - (count % 32)
if paddingCount > 0 {
return rightPaddedDataWithZero(toLength: count + paddingCount)
} else {
return self
}
}
}

fileprivate extension Int {
func numberToPaddedBytes() -> Data {
withUnsafeBytes(of: bigEndian) { Data($0) }.leftPaddedDataWithZero(toLength: 32)
}
}
1 change: 1 addition & 0 deletions web3swift/Web3/Classes/Web3+JSONRPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public struct JSONRPCresponse: Decodable{
public struct ErrorMessage: Decodable {
public var code: Int
public var message: String
public var data: String?
}

internal var decodableTypes: [Decodable.Type] = [[EventLog].self,
Expand Down

0 comments on commit e41e4da

Please sign in to comment.