diff --git a/Package.swift b/Package.swift index c9d7ff1..9f2f094 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,5 @@ // swift-tools-version:5.0 // The swift-tools-version declares the minimum version of Swift required to build this package. - import PackageDescription let package = Package( diff --git a/Sources/Relayer/main.swift b/Sources/Relayer/main.swift index 1d423ae..0a524ff 100644 --- a/Sources/Relayer/main.swift +++ b/Sources/Relayer/main.swift @@ -1,6 +1,6 @@ import Foundation -import UB import RelayerFramework +import UB let handler = Handler() @@ -9,5 +9,17 @@ node.delegate = handler node.add(transport: CoreBluetoothTransport()) // @todo handle CLI input, repl +let service = EthereumService(url: URL(string: "https://rinkeby.infura.io/f7a08ae0242843f1b1cf480454a6bba5")!) + +// Get Balance +let msg = Message( + proto: UBID(repeating: 1, count: 1), + recipient: Addr(repeating: 1, count: 1), + from: Addr(repeating: 1, count: 1), + origin: UBID(repeating: 1, count: 1), + message: "000F64928EcA02147075c7614A7d67B0C3Cb37D5DA".hexDecodedData() +) + +service.handle(message: msg, node: node) RunLoop.current.run() diff --git a/Sources/RelayerFramework/Extensions/Data+HexEncodedString.swift b/Sources/RelayerFramework/Extensions/Data+HexEncodedString.swift new file mode 100644 index 0000000..8a6b3ce --- /dev/null +++ b/Sources/RelayerFramework/Extensions/Data+HexEncodedString.swift @@ -0,0 +1,18 @@ +import Foundation + +extension Data { + /// A hexadecimal string representation of the bytes. + func hexEncodedString() -> String { + let hexDigits = Array("0123456789abcdef".utf16) + var hexChars = [UTF16.CodeUnit]() + hexChars.reserveCapacity(count * 2) + + for byte in self { + let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16) + hexChars.append(hexDigits[index1]) + hexChars.append(hexDigits[index2]) + } + + return String(utf16CodeUnits: hexChars, count: hexChars.count) + } +} diff --git a/Sources/RelayerFramework/Extensions/String+HexDecodedData.swift b/Sources/RelayerFramework/Extensions/String+HexDecodedData.swift new file mode 100644 index 0000000..3b17113 --- /dev/null +++ b/Sources/RelayerFramework/Extensions/String+HexDecodedData.swift @@ -0,0 +1,30 @@ +import Foundation + +extension String { + /// A data representation of the hexadecimal bytes in this string. + func hexDecodedData() -> Data { + // Get the UTF8 characters of this string + let chars = Array(utf8) + + // Keep the bytes in an UInt8 array and later convert it to Data + var bytes = [UInt8]() + bytes.reserveCapacity(count / 2) + + // It is a lot faster to use a lookup map instead of strtoul + let map: [UInt8] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567 + 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>? + 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x00, // @ABCDEFG + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // HIJKLMNO + ] + + // Grab two characters at a time, map them and turn it into a byte + for i in stride(from: 0, to: count, by: 2) { + let index1 = Int(chars[i] & 0x1F ^ 0x10) + let index2 = Int(chars[i + 1] & 0x1F ^ 0x10) + bytes.append(map[index1] << 4 | map[index2]) + } + + return Data(bytes) + } +} diff --git a/Sources/RelayerFramework/Handler.swift b/Sources/RelayerFramework/Handler.swift index 811ad4a..3f8e72e 100644 --- a/Sources/RelayerFramework/Handler.swift +++ b/Sources/RelayerFramework/Handler.swift @@ -7,18 +7,18 @@ public class Handler: UB.NodeDelegate { fileprivate var services = [UB.UBID: Service]() /// Initializes a new Handler. - public init() { } + public init() {} // @todo there probably should be more stuff here at one point. /// :nodoc: - public func node(_: Node, didReceiveMessage message: Message) { + public func node(_ node: Node, didReceiveMessage message: Message) { // @todo check if the message was just sent to us if message.proto.count == 0 { return } guard let service = services[message.proto] else { return } - service.handle(message: message) + service.handle(message: message, node: node) } } diff --git a/Sources/RelayerFramework/Services/EthereumService.swift b/Sources/RelayerFramework/Services/EthereumService.swift new file mode 100644 index 0000000..13affe9 --- /dev/null +++ b/Sources/RelayerFramework/Services/EthereumService.swift @@ -0,0 +1,135 @@ +import Foundation +import UB + +public class EthereumService: Service { + let url: URL + + /// Initializes an Ethereum Service with a RPC HTTP url + /// + /// - Parameters: + /// - url: Ethereum JSONRPC endpoint + public init(url: URL) { + self.url = url + } + + public func handle(message: Message, node: Node) { + // The first btye of the message is the JSONRPC method + let methodID = message.message[0] + + switch methodID { + case 0: + let address = "0x" + message.message[1 ..< message.message.count].hexEncodedString() + getBalance(address: address) { result in + switch result { + case let .success(balance): + let messageResponse = Message( + proto: UBID(repeating: 1, count: 1), + recipient: message.origin, + from: message.recipient, + origin: message.recipient, + message: balance + ) + node.send(messageResponse) + case let .failure(error): + print(error) + } + } + case 1: + let address = "0x" + message.message[1 ..< message.message.count].hexEncodedString() + getTransactionCount(address: address) { result in + switch result { + case let .success(nonce): + let messageResponse = Message( + proto: UBID(repeating: 1, count: 1), + recipient: message.origin, + from: message.recipient, + origin: message.recipient, + message: nonce + ) + node.send(messageResponse) + case let .failure(error): + print(error) + } + } + case 2: + let signedTransaction = "0x" + message.message[1 ..< message.message.count].hexEncodedString() + sendRawTransaction(signedTransaction: signedTransaction) { result in + switch result { + case let .success(balance): + let messageResponse = Message( + proto: UBID(repeating: 1, count: 1), + recipient: message.origin, + from: message.recipient, + origin: message.recipient, + message: balance + ) + node.send(messageResponse) + case let .failure(error): + print(error) + } + } + return + default: + print("default") + return + } + } + + internal func httpRequest(method: String, params: [String], completion: @escaping (Result) -> Void) { + var request = URLRequest(url: url) + request.httpMethod = "POST" + let payload: [String: Any] = [ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": 1, + ] + guard let jsonPayload = (try? JSONSerialization.data(withJSONObject: payload)) else { + print("Error: JSON could not serialize") + return + } + request.httpBody = jsonPayload + + _ = URLSession.shared.dataTask(with: request) { data, _, error in + guard let data = data, error == nil else { + print(error?.localizedDescription ?? "No data") + completion(.failure(error!)) + return + } + completion(.success(data)) + }.resume() + } + + internal func getBalance(address: String, completion: @escaping (Result) -> Void) { + httpRequest(method: "eth_getBalance", params: [address, "latest"]) { result in + switch result { + case let .success(balance): + completion(.success(balance)) + case let .failure(error): + completion(.failure(error)) + } + } + } + + internal func getTransactionCount(address: String, completion: @escaping (Result) -> Void) { + httpRequest(method: "eth_getTransactionCount", params: [address, "latest"]) { result in + switch result { + case let .success(nonce): + completion(.success(nonce)) + case let .failure(error): + completion(.failure(error)) + } + } + } + + internal func sendRawTransaction(signedTransaction: String, completion: @escaping (Result) -> Void) { + httpRequest(method: "eth_sendRawTransaction", params: [signedTransaction]) { result in + switch result { + case let .success(txHash): + completion(.success(txHash)) + case let .failure(error): + completion(.failure(error)) + } + } + } +} diff --git a/Sources/RelayerFramework/Services/Service.swift b/Sources/RelayerFramework/Services/Service.swift index be3b18b..9b3b21b 100644 --- a/Sources/RelayerFramework/Services/Service.swift +++ b/Sources/RelayerFramework/Services/Service.swift @@ -7,5 +7,5 @@ public protocol Service: AnyObject { /// /// - Parameters: /// - message: The received message to handle. - func handle(message: UB.Message) + func handle(message: UB.Message, node: UB.Node) } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 3c6f89c..efe4c82 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,6 +1,5 @@ import XCTest - var tests = [XCTestCaseEntry]() XCTMain(tests) diff --git a/Tests/RelayerFrameworkTests/EthereumServiceTests.swift b/Tests/RelayerFrameworkTests/EthereumServiceTests.swift new file mode 100644 index 0000000..3bcfa8c --- /dev/null +++ b/Tests/RelayerFrameworkTests/EthereumServiceTests.swift @@ -0,0 +1,27 @@ +@testable import RelayerFramework +import XCTest + +final class EthereumServiceTests: XCTestCase { + func testGetBalance() { + let didFinish = expectation(description: #function) + let url: URL = URL(string: "https://rinkeby.infura.io/f7a08ae0242843f1b1cf480454a6bba5")! + let ethService = EthereumService(url: url) + var bal = "" + ethService.getBalance(address: "0x0F64928EcA02147075c7614A7d67B0C3Cb37D5DA") { result in + switch result { + case let .success(balance): + bal = String(data: balance, encoding: .utf8)! + didFinish.fulfill() + case let .failure(error): + print(error) + } + } + + wait(for: [didFinish], timeout: 5) + XCTAssertEqual(bal, "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x1a55734bf06dc800\"}") + } + + static var allTests = [ + ("testGetBalance", testGetBalance), + ] +} diff --git a/Tests/RelayerFrameworkTests/RelayerTests.swift b/Tests/RelayerFrameworkTests/RelayerTests.swift deleted file mode 100644 index 98d930f..0000000 --- a/Tests/RelayerFrameworkTests/RelayerTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import class Foundation.Bundle -import XCTest - -final class RelayerTests: XCTestCase { -// func testExample() throws { -// // This is an example of a functional test case. -// // Use XCTAssert and related functions to verify your tests produce the correct -// // results. -// -// // Some of the APIs that we use below are available in macOS 10.13 and above. -// guard #available(macOS 10.13, *) else { -// return -// } -// -// let fooBinary = productsDirectory.appendingPathComponent("Relayer") -// -// let process = Process() -// process.executableURL = fooBinary -// -// let pipe = Pipe() -// process.standardOutput = pipe -// -// try process.run() -// process.waitUntilExit() -// -// let data = pipe.fileHandleForReading.readDataToEndOfFile() -// let output = String(data: data, encoding: .utf8) -// -// XCTAssertEqual(output, "Hello, world!\n") -// } -// -// /// Returns path to the built products directory. -// var productsDirectory: URL { -// #if os(macOS) -// for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { -// return bundle.bundleURL.deletingLastPathComponent() -// } -// fatalError("couldn't find the products directory") -// #else -// return Bundle.main.bundleURL -// #endif -// } -// -// static var allTests = [ -// ("testExample", testExample), -// ] -}