├── Cartfile ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── WalletConnectSwift.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── WalletConnectSwift.xcscheme └── project.pbxproj ├── Package.resolved ├── .gitignore ├── WalletConnectSwift ├── WalletConnectSwift.h └── Info.plist ├── Tests ├── Info.plist ├── WalletConnectTests.swift ├── WCURLTests.swift ├── Helpers.swift ├── ResponseTests.swift ├── RequestTests.swift ├── ClientTests.swift ├── SessionTests.swift └── JSONRPC_2_0_Tests.swift ├── WalletConnectSwiftTests └── Info.plist ├── Package.swift ├── LICENSE ├── WalletConnectSwift.podspec ├── Sources ├── Internal │ ├── PubSubMessage.swift │ ├── HandshakeHandler.swift │ ├── UpdateSessionHandler.swift │ ├── BridgeTransport.swift │ ├── AES_256_CBC_HMAC_SHA256_Codec.swift │ ├── Serializer.swift │ ├── Communicator.swift │ ├── WebSocketConnection.swift │ └── JSONRPC_2_0.swift └── PublicInterface │ ├── Logger.swift │ ├── WCURL.swift │ ├── Session.swift │ ├── Response.swift │ ├── WalletConnect.swift │ ├── Request.swift │ ├── Server.swift │ └── Client.swift └── README.md /Cartfile: -------------------------------------------------------------------------------- 1 | github "krzyzanowskim/CryptoSwift" ~> 1.5.1 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WalletConnectSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WalletConnectSwift.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CryptoSwift", 6 | "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", 10 | "version": "1.5.1" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /WalletConnectSwift.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cryptoswift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", 7 | "state" : { 8 | "revision" : "12f2389aca4a07e0dd54c86ec23d0721ed88b8db", 9 | "version" : "1.4.3" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Xcode ### 2 | .swiftpm/ 3 | .build/ 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.xcuserstate 18 | timeline.xctimeline 19 | Index 20 | 21 | /Framework 22 | .DS_Store 23 | Carthage/Build 24 | 25 | .idea 26 | .vscode 27 | 28 | # Remove Package.resolved for example apps 29 | ExampleApps/ExampleApps.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 30 | -------------------------------------------------------------------------------- /WalletConnectSwift/WalletConnectSwift.h: -------------------------------------------------------------------------------- 1 | // 2 | // WalletConnectSwift.h 3 | // WalletConnectSwift 4 | // 5 | // Created by Dmitry Bespalov on 20.08.19. 6 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for WalletConnectSwift. 12 | FOUNDATION_EXPORT double WalletConnectSwiftVersionNumber; 13 | 14 | //! Project version string for WalletConnectSwift. 15 | FOUNDATION_EXPORT const unsigned char WalletConnectSwiftVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /WalletConnectSwiftTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.7.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "WalletConnectSwift", 6 | platforms: [ 7 | .macOS(.v10_15), .iOS(.v13), 8 | ], 9 | products: [ 10 | .library( 11 | name: "WalletConnectSwift", 12 | targets: ["WalletConnectSwift"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.5.1")) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "WalletConnectSwift", 20 | dependencies: ["CryptoSwift"], 21 | path: "Sources"), 22 | .testTarget(name: "WalletConnectSwiftTests", dependencies: ["WalletConnectSwift"], path: "Tests"), 23 | ], 24 | swiftLanguageVersions: [.v5] 25 | ) 26 | -------------------------------------------------------------------------------- /WalletConnectSwift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.7.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gnosis Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /WalletConnectSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "WalletConnectSwift" 3 | spec.version = "1.7.0" 4 | spec.summary = "A delightful way to integrate WalletConnect into your app." 5 | spec.description = <<-DESC 6 | WalletConnect protocol implementation for enabling communication between dapps and 7 | wallets. This library provides both client and server parts so that you can integrate 8 | it in your wallet, or in your dapp - whatever you are working on. 9 | DESC 10 | spec.homepage = "https://github.com/WalletConnect/WalletConnectSwift" 11 | spec.license = { :type => "MIT", :file => "LICENSE" } 12 | spec.author = { "Andrey Scherbovich" => "andrey@gnosis.io", "Dmitry Bespalov" => "dmitry.bespalov@gnosis.io" } 13 | spec.cocoapods_version = '>= 1.4.0' 14 | spec.platform = :ios, "13.0" 15 | spec.swift_version = "5.0" 16 | spec.source = { :git => "https://github.com/WalletConnect/WalletConnectSwift.git", :tag => "#{spec.version}" } 17 | spec.source_files = "Sources/**/*.swift" 18 | spec.requires_arc = true 19 | spec.dependency "CryptoSwift", "~> 1.5" 20 | end 21 | -------------------------------------------------------------------------------- /Sources/Internal/PubSubMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | enum DataConversionError: Error { 8 | case stringToDataFailed 9 | case dataToStringFailed 10 | } 11 | 12 | struct PubSubMessage: Codable { 13 | /// WalletConnect topic 14 | var topic: String 15 | /// pub/sub message type 16 | var type: MessageType 17 | /// encoded JSONRPC data. 18 | var payload: String 19 | 20 | enum MessageType: String, Codable { 21 | case pub 22 | case sub 23 | } 24 | 25 | static func message(from string: String) throws -> PubSubMessage { 26 | guard let data = string.data(using: .utf8) else { 27 | throw DataConversionError.stringToDataFailed 28 | } 29 | return try JSONDecoder().decode(PubSubMessage.self, from: data) 30 | } 31 | 32 | func json() throws -> String { 33 | let data = try JSONEncoder.encoder().encode(self) 34 | guard let string = String(data: data, encoding: .utf8) else { 35 | throw DataConversionError.dataToStringFailed 36 | } 37 | return string 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Internal/HandshakeHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol HandshakeHandlerDelegate: AnyObject { 8 | // TODO: instead of IDType, use RequestID 9 | func handler(_ handler: HandshakeHandler, didReceiveRequestToCreateSession: Session, requestId: RequestID) 10 | } 11 | 12 | class HandshakeHandler: RequestHandler { 13 | private weak var delegate: HandshakeHandlerDelegate? 14 | 15 | init(delegate: HandshakeHandlerDelegate) { 16 | self.delegate = delegate 17 | } 18 | 19 | func canHandle(request: Request) -> Bool { 20 | return request.method == "wc_sessionRequest" 21 | } 22 | 23 | func handle(request: Request) { 24 | do { 25 | let dappInfo = try request.parameter(of: Session.DAppInfo.self, at: 0) 26 | let session = Session(url: request.url, dAppInfo: dappInfo, walletInfo: nil) 27 | guard let requestID = request.id else { 28 | // TODO: request ID is required 29 | return 30 | } 31 | delegate?.handler(self, didReceiveRequestToCreateSession: session, requestId: requestID) 32 | } catch { 33 | // TODO: handle error 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Internal/UpdateSessionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol UpdateSessionHandlerDelegate: AnyObject { 8 | func handler(_ handler: UpdateSessionHandler, didUpdateSessionByURL: WCURL, sessionInfo: SessionInfo) 9 | } 10 | 11 | class UpdateSessionHandler: RequestHandler { 12 | private weak var delegate: UpdateSessionHandlerDelegate? 13 | 14 | init(delegate: UpdateSessionHandlerDelegate) { 15 | self.delegate = delegate 16 | } 17 | 18 | func canHandle(request: Request) -> Bool { 19 | return request.method == "wc_sessionUpdate" 20 | } 21 | 22 | func handle(request: Request) { 23 | do { 24 | let sessionInfo = try request.parameter(of: SessionInfo.self, at: 0) 25 | delegate?.handler(self, didUpdateSessionByURL: request.url, sessionInfo: sessionInfo) 26 | } catch { 27 | LogService.shared.error("WC: wrong format of wc_sessionUpdate request: \(error)") 28 | // TODO: send error response 29 | } 30 | } 31 | } 32 | 33 | /// https://docs.walletconnect.org/tech-spec#session-update 34 | struct SessionInfo: Decodable { 35 | var approved: Bool 36 | var accounts: [String]? 37 | var chainId: Int? 38 | } 39 | 40 | enum ChainID { 41 | static let mainnet = 1 42 | } 43 | -------------------------------------------------------------------------------- /Tests/WalletConnectTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class WalletConnectTests: XCTestCase { 9 | var wc = WalletConnect() 10 | let mockCommunicator = MockCommunicator() 11 | 12 | override func setUp() { 13 | super.setUp() 14 | wc.communicator = mockCommunicator 15 | } 16 | 17 | func test_whenConnectingToNewURL_thenConnects() { 18 | XCTAssertFalse(mockCommunicator.didListen) 19 | try? wc.connect(to: WCURL.testURL) 20 | XCTAssertTrue(mockCommunicator.didListen) 21 | } 22 | 23 | func test_whenConnectingToExistingURL_thenThrows() { 24 | mockCommunicator.addOrUpdateSession(Session.testSession) 25 | XCTAssertThrowsError(try wc.connect(to: WCURL.testURL)) 26 | } 27 | 28 | func test_whenReconnecting_thenConnects() { 29 | XCTAssertFalse(mockCommunicator.didListen) 30 | try? wc.reconnect(to: Session.testSession) 31 | XCTAssertTrue(mockCommunicator.didListen) 32 | let addedSession = mockCommunicator.session(by: Session.testSession.url) 33 | XCTAssertEqual(addedSession, Session.testSession) 34 | } 35 | 36 | func test_whenReconnectingToSessionWithoutWalletInfo_thenThrows() { 37 | XCTAssertThrowsError(try wc.reconnect(to: Session.testSessionWithoutWalletInfo)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/WCURLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class WCURLTests: XCTestCase { 9 | let string = "wc:topic@1?bridge=https%3A%2F%2Fexample.org%2F&key=key" 10 | 11 | func test_absoluteString() { 12 | let url = WCURL(topic: "topic", version: "1", bridgeURL: URL(string: "https://example.org/")!, key: "key") 13 | XCTAssertEqual(url.absoluteString, string) 14 | } 15 | 16 | func test_init() { 17 | XCTAssertNil(WCURL("gs://")) 18 | 19 | let emptyTopic = WCURL("wc:@1?bridge=https%3A%2F%2Fexample.org%2F&key=key") 20 | XCTAssertEqual(emptyTopic?.topic, "") 21 | 22 | let noTopic = WCURL("wc:1?bridge=https%3A%2F%2Fexample.org%2F&key=key") 23 | XCTAssertNil(noTopic) 24 | 25 | let noVersion = WCURL("wc:topic?bridge=https%3A%2F%2Fexample.org%2F&key=key") 26 | XCTAssertNil(noVersion) 27 | 28 | let withSlashes = WCURL("wc://topic@1?bridge=https%3A%2F%2Fexample.org%2F&key=key") 29 | XCTAssertEqual(withSlashes?.absoluteString, string) 30 | 31 | let noQuery = WCURL("wc:topic@1") 32 | XCTAssertNil(noQuery) 33 | 34 | let noBridgeKey = WCURL("wc:topic@1?key=key") 35 | XCTAssertNil(noBridgeKey) 36 | 37 | let bridgeNotURL = WCURL("wc:topic@1?bridge=&key=key") 38 | XCTAssertNil(bridgeNotURL) 39 | 40 | let noKey = WCURL("wc:topic@1?bridge=https%3A%2F%2Fexample.org%2F") 41 | XCTAssertNil(noKey) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // WalletConnectSwift 4 | // 5 | // Created by Andrey Scherbovich on 17.07.20. 6 | // Copyright © 2020 Gnosis Ltd. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import os.log 11 | 12 | public enum LoggerLevel: Int { 13 | case none = 0 14 | case error = 100 15 | case info = 200 16 | case detailed = 300 17 | case verbose = 400 18 | } 19 | 20 | public protocol Logger { 21 | func error(_ message: String) 22 | func info(_ message: String) 23 | func detailed(_ message: String) 24 | func verbose(_ message: String) 25 | } 26 | 27 | public class ConsoleLogger: Logger { 28 | static var consoleLogger: OSLog = OSLog(subsystem: "com.walletconnect", category: "WalletConnectSwift") 29 | 30 | public func error(_ message: String) { 31 | log(message, level: .error) 32 | } 33 | 34 | public func info(_ message: String) { 35 | log(message, level: .info) 36 | } 37 | 38 | public func detailed(_ message: String) { 39 | log(message, level: .detailed) 40 | } 41 | 42 | public func verbose(_ message: String) { 43 | log(message, level: .verbose) 44 | } 45 | 46 | func log(_ message: String, level: LoggerLevel) { 47 | #if DEBUG 48 | guard level.rawValue <= LogService.level.rawValue else { return } 49 | 50 | os_log( 51 | "%{private}@", 52 | log: ConsoleLogger.consoleLogger, 53 | type: OSLogType.debug, 54 | message 55 | ) 56 | #endif 57 | } 58 | } 59 | 60 | /// Determine the log level by changing the `level` property. 61 | public class LogService { 62 | static var shared: Logger = ConsoleLogger() 63 | 64 | /// Defines which logs are presented. Defaults to ``LoggerLevel/verbose`` 65 | /// - Note: Logs are only available in debug mode 66 | public static var level: LoggerLevel = .verbose 67 | } 68 | -------------------------------------------------------------------------------- /Sources/PublicInterface/WCURL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct WCURL: Hashable, Codable { 8 | // topic is used for handshake only 9 | public var topic: String 10 | public var version: String 11 | public var bridgeURL: URL 12 | public var key: String 13 | 14 | public var absoluteString: String { 15 | let bridge = bridgeURL.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "" 16 | return "wc:\(topic)@\(version)?bridge=\(bridge)&key=\(key)" 17 | } 18 | 19 | public init(topic: String, 20 | version: String = "1", 21 | bridgeURL: URL, 22 | key: String) { 23 | self.topic = topic 24 | self.version = version 25 | self.bridgeURL = bridgeURL 26 | self.key = key 27 | } 28 | 29 | public init?(_ str: String) { 30 | guard str.hasPrefix("wc:") else { 31 | return nil 32 | } 33 | let urlStr = !str.hasPrefix("wc://") ? str.replacingOccurrences(of: "wc:", with: "wc://") : str 34 | guard let url = URL(string: urlStr), 35 | let topic = url.user, 36 | let version = url.host, 37 | let components = NSURLComponents(url: url, resolvingAgainstBaseURL: false) else { 38 | return nil 39 | } 40 | var dict = [String: String]() 41 | for query in components.queryItems ?? [] { 42 | if let value = query.value { 43 | dict[query.name] = value 44 | } 45 | } 46 | guard let bridge = dict["bridge"], 47 | let bridgeUrl = URL(string: bridge), 48 | let key = dict["key"] else { 49 | return nil 50 | } 51 | self.topic = topic 52 | self.version = version 53 | self.bridgeURL = bridgeUrl 54 | self.key = key 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Each session is a communication channel between dApp and Wallet on dAppInfo.peerId topic 8 | public struct Session: Codable { 9 | // TODO: handle protocol version 10 | public let url: WCURL 11 | public let dAppInfo: DAppInfo 12 | public var walletInfo: WalletInfo? 13 | 14 | public init(url: WCURL, dAppInfo: DAppInfo, walletInfo: WalletInfo?) { 15 | self.url = url 16 | self.dAppInfo = dAppInfo 17 | self.walletInfo = walletInfo 18 | } 19 | 20 | public struct DAppInfo: Codable, Equatable { 21 | public let peerId: String 22 | public let peerMeta: ClientMeta 23 | public let chainId: Int? 24 | public let approved: Bool? 25 | 26 | public init(peerId: String, peerMeta: ClientMeta, chainId: Int? = nil, approved: Bool? = nil) { 27 | self.peerId = peerId 28 | self.peerMeta = peerMeta 29 | self.chainId = chainId 30 | self.approved = approved 31 | } 32 | 33 | func with(approved: Bool) -> DAppInfo { 34 | return DAppInfo(peerId: self.peerId, 35 | peerMeta: self.peerMeta, 36 | chainId: self.chainId, 37 | approved: approved) 38 | } 39 | } 40 | 41 | public struct ClientMeta: Codable, Equatable { 42 | public let name: String 43 | public let description: String? 44 | public let icons: [URL] 45 | public let url: URL 46 | public let scheme: String? 47 | 48 | public init(name: String, description: String?, icons: [URL], url: URL, scheme: String? = nil) { 49 | self.name = name 50 | self.description = description 51 | self.icons = icons 52 | self.url = url 53 | self.scheme = scheme 54 | } 55 | } 56 | 57 | public struct WalletInfo: Codable, Equatable { 58 | public let approved: Bool 59 | public let accounts: [String] 60 | public let chainId: Int 61 | public let peerId: String 62 | public let peerMeta: ClientMeta 63 | 64 | public init(approved: Bool, accounts: [String], chainId: Int, peerId: String, peerMeta: ClientMeta) { 65 | self.approved = approved 66 | self.accounts = accounts 67 | self.chainId = chainId 68 | self.peerId = peerId 69 | self.peerMeta = peerMeta 70 | } 71 | 72 | public func with(approved: Bool) -> WalletInfo { 73 | return WalletInfo(approved: approved, 74 | accounts: self.accounts, 75 | chainId: self.chainId, 76 | peerId: self.peerId, 77 | peerMeta: self.peerMeta) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Internal/BridgeTransport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol Transport { 8 | func send(to url: WCURL, text: String) 9 | func listen(on url: WCURL, 10 | onConnect: @escaping ((WCURL) -> Void), 11 | onDisconnect: @escaping ((WCURL, Error?) -> Void), 12 | onTextReceive: @escaping (String, WCURL) -> Void) 13 | func isConnected(by url: WCURL) -> Bool 14 | func disconnect(from url: WCURL) 15 | } 16 | 17 | // future: if we received response from another peer - then we call request.completion() for pending request. 18 | // future: if request is not notification - then it will be pending for response 19 | 20 | class Bridge: Transport { 21 | private var connections: [WebSocketConnection] = [] 22 | private let syncQueue = DispatchQueue(label: "org.walletconnect.swift.transport") 23 | 24 | // TODO: if no connection found, then what? 25 | func send(to url: WCURL, text: String) { 26 | dispatchPrecondition(condition: .notOnQueue(syncQueue)) 27 | syncQueue.sync { [weak self] in 28 | guard let `self` = self else { return } 29 | if let connection = self.findConnection(url: url) { 30 | connection.send(text) 31 | } 32 | } 33 | } 34 | 35 | func listen(on url: WCURL, 36 | onConnect: @escaping ((WCURL) -> Void), 37 | onDisconnect: @escaping ((WCURL, Error?) -> Void), 38 | onTextReceive: @escaping (String, WCURL) -> Void) { 39 | dispatchPrecondition(condition: .notOnQueue(syncQueue)) 40 | syncQueue.sync { [weak self] in 41 | guard let `self` = self else { return } 42 | var connection: WebSocketConnection 43 | if let existingConnection = self.findConnection(url: url) { 44 | connection = existingConnection 45 | } else { 46 | connection = WebSocketConnection(url: url, 47 | onConnect: { onConnect(url) }, 48 | onDisconnect: { [weak self] error in 49 | self?.releaseConnection(by: url) 50 | onDisconnect(url, error) }, 51 | onTextReceive: { text in onTextReceive(text, url) }) 52 | self.connections.append(connection) 53 | } 54 | if !connection.isOpen { 55 | connection.open() 56 | } 57 | } 58 | } 59 | 60 | func isConnected(by url: WCURL) -> Bool { 61 | var connection: WebSocketConnection? 62 | dispatchPrecondition(condition: .notOnQueue(syncQueue)) 63 | syncQueue.sync { [weak self] in 64 | guard let `self` = self else { return } 65 | connection = self.findConnection(url: url) 66 | } 67 | return connection?.isOpen ?? false 68 | } 69 | 70 | func disconnect(from url: WCURL) { 71 | dispatchPrecondition(condition: .notOnQueue(syncQueue)) 72 | syncQueue.sync { [weak self] in 73 | guard let `self` = self else { return } 74 | if let connection = self.findConnection(url: url) { 75 | connection.close() 76 | } 77 | } 78 | } 79 | 80 | private func releaseConnection(by url: WCURL) { 81 | dispatchPrecondition(condition: .notOnQueue(syncQueue)) 82 | syncQueue.sync { [weak self] in 83 | guard let `self` = self else { return } 84 | if let connection = self.findConnection(url: url) { 85 | self.connections.removeAll { $0 === connection } 86 | } 87 | } 88 | } 89 | 90 | // this method left thread-unsafe on purpose, because guarding the connections in this method is too granular 91 | private func findConnection(url: WCURL) -> WebSocketConnection? { 92 | dispatchPrecondition(condition: .onQueue(syncQueue)) 93 | return connections.first { $0.url == url } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | @testable import WalletConnectSwift 7 | 8 | class MockCommunicator: Communicator { 9 | private var sessions = [Session]() 10 | 11 | var didListen = false 12 | override func listen(on url: WCURL, 13 | onConnect: @escaping ((WCURL) -> Void), 14 | onDisconnect: @escaping ((WCURL, Error?) -> Void), 15 | onTextReceive: @escaping (String, WCURL) -> Void) { 16 | didListen = true 17 | } 18 | 19 | override func addOrUpdateSession(_ session: Session) { 20 | sessions.append(session) 21 | } 22 | 23 | override func session(by url: WCURL) -> Session? { 24 | return sessions.first { $0.url == url } 25 | } 26 | 27 | var sentRequest: Request? 28 | override func send(_ request: Request, topic: String) { 29 | sentRequest = request 30 | } 31 | 32 | var subscribedOn: (topic: String, url: WCURL)? 33 | override func subscribe(on topic: String, url: WCURL) { 34 | subscribedOn = (topic: topic, url: url) 35 | } 36 | } 37 | 38 | extension WCURL { 39 | static let testURL = WCURL("wc:test123@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=46d8847bdbca255a98ba7d79d4f4d77daebbbeb53b5aea6e9f39fa848b177bb7")! 40 | } 41 | 42 | extension Session: Equatable { 43 | public static func == (lhs: Session, rhs: Session) -> Bool { 44 | return lhs.dAppInfo == rhs.dAppInfo && 45 | lhs.url == rhs.url && 46 | lhs.walletInfo == rhs.walletInfo 47 | } 48 | 49 | static let testSession = Session(url: WCURL.testURL, 50 | dAppInfo: Session.DAppInfo.testDappInfo, 51 | walletInfo: Session.WalletInfo.testWalletInfo) 52 | 53 | static let testSessionWithoutWalletInfo = Session(url: WCURL.testURL, 54 | dAppInfo: Session.DAppInfo.testDappInfo, 55 | walletInfo: nil) 56 | } 57 | 58 | extension Session.DAppInfo { 59 | static let testDappInfo = Session.DAppInfo(peerId: "test", peerMeta: Session.ClientMeta.testMeta, approved: true) 60 | } 61 | 62 | extension Session.WalletInfo { 63 | static let testWalletInfo = Session.WalletInfo(approved: true, 64 | accounts: ["0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95"], 65 | chainId: 4, 66 | peerId: "test234", 67 | peerMeta: Session.ClientMeta.testMeta) 68 | } 69 | 70 | extension Session.ClientMeta { 71 | static let testMeta = Session.ClientMeta(name: "Test Meta", 72 | description: nil, 73 | icons: [], 74 | url: URL(string: "https://walletconnect.org")!) 75 | } 76 | 77 | extension Request { 78 | static let testRequest = Request(url: WCURL.testURL, method: "personal_sign", id: 1) 79 | static let testRequestWithoutId = Request(url: WCURL.testURL, method: "personal_sign", id: nil) 80 | } 81 | 82 | extension Client.Transaction { 83 | static let testTransaction = Client.Transaction(from: "0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95", 84 | to: nil, 85 | data: "0x0", 86 | gas: nil, 87 | gasPrice: nil, 88 | value: nil, 89 | nonce: nil, 90 | type: nil, 91 | accessList: nil, 92 | chainId: nil, 93 | maxPriorityFeePerGas: nil, 94 | maxFeePerGas: nil) 95 | } 96 | -------------------------------------------------------------------------------- /Sources/Internal/AES_256_CBC_HMAC_SHA256_Codec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import CryptoSwift 7 | import Security 8 | 9 | struct HexString: Codable { 10 | var data: Data 11 | 12 | var string: String { 13 | return data.toHexString() 14 | } 15 | 16 | init(_ data: Data) { 17 | self.data = data 18 | } 19 | 20 | func encode(to encoder: Encoder) throws { 21 | var container = encoder.singleValueContainer() 22 | try container.encode(string) 23 | } 24 | 25 | init(from decoder: Decoder) throws { 26 | let container = try decoder.singleValueContainer() 27 | let value = try container.decode(String.self) 28 | self.init(value) 29 | } 30 | 31 | init(_ string: String) { 32 | data = Data(hex: string) 33 | } 34 | } 35 | 36 | class AES_256_CBC_HMAC_SHA256_Codec: Codec { 37 | struct EncryptionPayload: Codable { 38 | var data: HexString 39 | var hmac: HexString 40 | var iv: HexString 41 | } 42 | 43 | enum CodecError: Error { 44 | case stringToDataFailed(String) 45 | case dataToStringFailed(Data) 46 | case authenticationFailed(String) 47 | } 48 | 49 | func encode(plainText: String, key: String) throws -> String { 50 | let keyData = HexString(key).data 51 | let plainTextData = try data(string: plainText) 52 | let (cipherText, iv) = try encrypt(key: keyData, data: plainTextData) 53 | let hmac = try authenticationCode(key: keyData, data: cipherText + iv) 54 | let payload = EncryptionPayload(data: HexString(cipherText), hmac: HexString(hmac), iv: HexString(iv)) 55 | let payloadData = try data(from: payload) 56 | let result = try string(data: payloadData) 57 | return result 58 | } 59 | 60 | func decode(cipherText: String, key: String) throws -> String { 61 | let cipherTextData = try data(string: cipherText) 62 | let payload = try self.payload(from: cipherTextData) 63 | let keyData = HexString(key).data 64 | let hmac = try authenticationCode(key: keyData, data: payload.data.data + payload.iv.data) 65 | guard hmac == payload.hmac.data else { 66 | throw CodecError.authenticationFailed(cipherText) 67 | } 68 | let plainTextData = try decrypt(key: keyData, data: payload.data.data, iv: payload.iv.data) 69 | let plainText = try string(data: plainTextData) 70 | return plainText 71 | } 72 | 73 | private func encrypt(key: Data, data: Data) throws -> (cipherText: Data, iv: Data) { 74 | let iv = AES.randomIV(AES.blockSize) 75 | let cipher = try AES(key: key.bytes, blockMode: CBC(iv: iv)) 76 | let cipherText = try cipher.encrypt(data.bytes) 77 | return (Data(cipherText), Data(iv)) 78 | } 79 | 80 | private func decrypt(key: Data, data: Data, iv: Data) throws -> Data { 81 | let cipher = try AES(key: key.bytes, blockMode: CBC(iv: iv.bytes)) 82 | let plainText = try cipher.decrypt(data.bytes) 83 | return Data(plainText) 84 | } 85 | 86 | private func authenticationCode(key: Data, data: Data) throws -> Data { 87 | let algo = HMAC(key: key.bytes, variant: .sha2(.sha256)) 88 | let digest = try algo.authenticate(data.bytes) 89 | return Data(digest) 90 | } 91 | 92 | private func data(string: String) throws -> Data { 93 | if let data = string.data(using: .utf8) { 94 | return data 95 | } else { 96 | throw CodecError.stringToDataFailed(string) 97 | } 98 | } 99 | 100 | private func string(data: Data) throws -> String { 101 | if let string = String(data: data, encoding: .utf8) { 102 | return string 103 | } else { 104 | throw CodecError.dataToStringFailed(data) 105 | } 106 | } 107 | 108 | private func payload(from data: Data) throws -> EncryptionPayload { 109 | return try JSONDecoder().decode(EncryptionPayload.self, from: data) 110 | } 111 | 112 | private func data(from payload: EncryptionPayload) throws -> Data { 113 | return try JSONEncoder.encoder().encode(payload) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/ResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class ResponseTests: XCTestCase { 9 | let url = WCURL(topic: "topic", version: "1", bridgeURL: URL(string: "https://example.org/")!, key: "key") 10 | let method = "some_rpc_method_name" 11 | 12 | struct A: Codable, Equatable { 13 | var a: String 14 | } 15 | 16 | func test_withResult() throws { 17 | let string = try Response(url: url, value: "1", id: "1") 18 | XCTAssertEqual(string.internalID, .string("1")) 19 | XCTAssertEqual(string.url, url) 20 | XCTAssertEqual(try string.result(as: String.self), "1") 21 | XCTAssertNil(string.error) 22 | 23 | let object = try Response(url: url, value: A(a: "1"), id: "1") 24 | XCTAssertEqual(try object.result(as: A.self), A(a: "1")) 25 | 26 | let array = try Response(url: url, value: ["1"], id: "1") 27 | XCTAssertEqual(try array.result(as: [String].self), ["1"]) 28 | 29 | let arrayOfObject = try Response(url: url, value: [A(a: "1")], id: "1") 30 | XCTAssertEqual(try arrayOfObject.result(as: [A].self), [A(a: "1")]) 31 | XCTAssertEqual(try arrayOfObject.json(), """ 32 | {"id":"1","jsonrpc":"2.0","result":[{"a":"1"}]} 33 | """) 34 | 35 | let int = try Response(url: url, value: 1, id: 1) 36 | XCTAssertEqual(try int.result(as: Int.self), 1) 37 | XCTAssertEqual(int.internalID, .int(1)) 38 | XCTAssertEqual(try int.json(), """ 39 | {"id":1,"jsonrpc":"2.0","result":1} 40 | """) 41 | 42 | let double = try Response(url: url, value: 1.0, id: 1.0) 43 | XCTAssertEqual(try double.result(as: Double.self), 1.0, accuracy: 0.1) 44 | XCTAssertEqual(double.internalID, .double(1.0)) 45 | } 46 | 47 | func test_withCustomError() throws { 48 | let custom = try Response(url: url, errorCode: 1, message: "a", value: "b", id: "1") 49 | XCTAssertEqual(custom.url, url) 50 | XCTAssertEqual(custom.internalID, .string("1")) 51 | XCTAssertThrowsError(try custom.result(as: String.self)) 52 | XCTAssertNotNil(custom.error) 53 | let error = custom.error! 54 | XCTAssertEqual(error.localizedDescription, "a") 55 | XCTAssertEqual(error.userInfo[WCResponseErrorDataKey] as? String, "\"b\"") 56 | XCTAssertEqual(try custom.json(), """ 57 | {"error":{"code":1,"data":"b","message":"a"},"id":"1","jsonrpc":"2.0"} 58 | """) 59 | } 60 | 61 | func test_withPredefinedError() throws { 62 | let request = Request(url: url, method: method, id: 1) 63 | let invalidJSON = try Response(request: request, error: .invalidJSON) 64 | XCTAssertNotNil(invalidJSON.error) 65 | XCTAssertEqual(invalidJSON.error?.code, ResponseError.invalidJSON.rawValue) 66 | XCTAssertEqual(invalidJSON.error?.localizedDescription, ResponseError.invalidJSON.message) 67 | XCTAssertEqual(invalidJSON.internalID, .int(1)) 68 | 69 | let invalidRequest = try Response(url: url, error: .invalidRequest) 70 | XCTAssertEqual(invalidRequest.internalID, .null) 71 | } 72 | 73 | func test_staticInitializers() { 74 | let request = Request(url: url, method: method, id: 1) 75 | 76 | let rejection = Response.reject(request) 77 | XCTAssertEqual(rejection.internalID, request.internalID) 78 | XCTAssertEqual(rejection.error?.code, ResponseError.requestRejected.rawValue) 79 | 80 | let invalid = Response.invalid(request) 81 | XCTAssertEqual(invalid.internalID, request.internalID) 82 | XCTAssertEqual(invalid.error?.code, ResponseError.invalidRequest.rawValue) 83 | } 84 | 85 | func test_withJsonString() throws { 86 | let jsonString = 87 | """ 88 | {"id":"1","jsonrpc":"2.0","result":[{"a":"1"}]} 89 | """ 90 | let response = try Response(url: url, jsonString: jsonString) 91 | XCTAssertEqual(try response.json(), """ 92 | {"id":"1","jsonrpc":"2.0","result":[{"a":"1"}]} 93 | """) 94 | XCTAssertEqual(response.url, url) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class Response { 8 | public var url: WCURL 9 | 10 | internal var internalID: JSONRPC_2_0.IDType { 11 | return payload.id 12 | } 13 | 14 | private var payload: JSONRPC_2_0.Response 15 | 16 | internal init(payload: JSONRPC_2_0.Response, url: WCURL) { 17 | self.payload = payload 18 | self.url = url 19 | } 20 | 21 | public convenience init(url: WCURL, value: T, id: RequestID) throws { 22 | let result = try JSONRPC_2_0.Response.Payload.value(JSONRPC_2_0.ValueType(value)) 23 | let response = JSONRPC_2_0.Response(result: result, id: JSONRPC_2_0.IDType(id)) 24 | self.init(payload: response, url: url) 25 | } 26 | 27 | public convenience init(url: WCURL, errorCode: Int, message: String, value: T?, id: RequestID?) throws { 28 | let code = try JSONRPC_2_0.Response.Payload.ErrorPayload.Code(errorCode) 29 | let data: JSONRPC_2_0.ValueType? = try value == nil ? nil : JSONRPC_2_0.ValueType(value!) 30 | let error = JSONRPC_2_0.Response.Payload.ErrorPayload(code: code, message: message, data: data) 31 | let result = JSONRPC_2_0.Response.Payload.error(error) 32 | let response = JSONRPC_2_0.Response(result: result, id: JSONRPC_2_0.IDType(id)) 33 | self.init(payload: response, url: url) 34 | } 35 | 36 | public convenience init(url: WCURL, errorCode: Int, message: String, id: RequestID?) throws { 37 | try self.init(url: url, errorCode: errorCode, message: message, value: Optional.none, id: id) 38 | } 39 | 40 | public convenience init(request: Request, error: ResponseError) throws { 41 | try self.init(url: request.url, 42 | errorCode: error.rawValue, 43 | message: error.message, 44 | id: request.id) 45 | } 46 | 47 | public convenience init(url: WCURL, error: ResponseError) throws { 48 | try self.init(url: url, 49 | errorCode: error.rawValue, 50 | message: error.message, 51 | id: nil) 52 | } 53 | 54 | public convenience init(url: WCURL, jsonString: String) throws { 55 | let payload = try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(jsonString)) 56 | self.init(payload: payload, url: url) 57 | } 58 | 59 | public func result(as type: T.Type) throws -> T { 60 | switch payload.result { 61 | case .error: 62 | throw ResponseError.errorResponse 63 | case .value(let value): 64 | return try value.decode(to: type) 65 | } 66 | } 67 | 68 | public var error: NSError? { 69 | switch payload.result { 70 | case .value: return nil 71 | case .error(let value): 72 | var userInfo: [String: Any] = [NSLocalizedDescriptionKey: value.message] 73 | if let data = value.data { 74 | userInfo[WCResponseErrorDataKey] = try? data.jsonString() 75 | } 76 | return NSError(domain: "org.walletconnect", code: value.code.code, userInfo: userInfo) 77 | } 78 | } 79 | 80 | internal func json() throws -> JSONRPC_2_0.JSON { 81 | return try payload.json() 82 | } 83 | 84 | public static func reject(_ request: Request) -> Response { 85 | return try! Response(request: request, error: .requestRejected) 86 | } 87 | 88 | public static func invalid(_ request: Request) -> Response { 89 | return try! Response(request: request, error: .invalidRequest) 90 | } 91 | } 92 | 93 | public let WCResponseErrorDataKey: String = "WCResponseErrorDataKey" 94 | 95 | public enum ResponseError: Int, Error { 96 | case invalidJSON = -32700 97 | case invalidRequest = -32600 98 | case methodNotFound = -32601 99 | case invalidParams = -32602 100 | case internalError = -32603 101 | 102 | case errorResponse = -32010 103 | case requestRejected = -32050 104 | 105 | public var message: String { 106 | switch self { 107 | case .invalidJSON: return "Parse error" 108 | case .invalidRequest: return "Invalid Request" 109 | case .methodNotFound: return "Method not found" 110 | case .invalidParams: return "Invalid params" 111 | case .internalError: return "Internal error" 112 | case .errorResponse: return "Error response" 113 | case .requestRejected: return "Request rejected" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /WalletConnectSwift.xcodeproj/xcshareddata/xcschemes/WalletConnectSwift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 53 | 59 | 60 | 61 | 62 | 63 | 73 | 74 | 80 | 81 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Sources/Internal/Serializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// We do not expect incomming responses as requests that we send are notifications. 8 | protocol ResponseSerializer { 9 | /// Serialize WalletConnect Response into text message. 10 | /// 11 | /// - Parameters: 12 | /// - response: Response object 13 | /// - topic: text message topic 14 | /// - Returns: text message 15 | /// - Throws: serialization errors 16 | func serialize(_ response: Response, topic: String) throws -> String 17 | 18 | /// Deserialize incoming WalletConnet text message. 19 | /// 20 | /// - Parameters: 21 | /// - text: encoded text message 22 | /// - url: WalletConnect session URL data (required for text decoding). 23 | /// - Returns: response object 24 | /// - Throws: deserialization errors 25 | func deserialize(_ text: String, url: WCURL) throws -> Response 26 | } 27 | 28 | protocol RequestSerializer { 29 | /// Serialize WalletConnect Request into text message. 30 | /// 31 | /// - Parameters: 32 | /// - request: Request object 33 | /// - topic: text message topic 34 | /// - Returns: text message 35 | /// - Throws: serialization errors 36 | func serialize(_ request: Request, topic: String) throws -> String 37 | 38 | /// Deserialize incoming WalletConnet text message. 39 | /// 40 | /// - Parameters: 41 | /// - text: encoded text message 42 | /// - url: WalletConnect session URL data (required for text decoding). 43 | /// - Returns: request object 44 | /// - Throws: deserialization errors 45 | func deserialize(_ text: String, url: WCURL) throws -> Request 46 | } 47 | 48 | protocol Codec { 49 | func encode(plainText: String, key: String) throws -> String 50 | func decode(cipherText: String, key: String) throws -> String 51 | } 52 | 53 | enum JSONRPCSerializerError: Error { 54 | case wrongIncommingDecodedTextFormat(String) 55 | } 56 | 57 | class JSONRPCSerializer: RequestSerializer, ResponseSerializer { 58 | private let codec: Codec = AES_256_CBC_HMAC_SHA256_Codec() 59 | private let jsonrpc = JSONRPCAdapter() 60 | 61 | // MARK: - RequestSerializer 62 | 63 | func serialize(_ request: Request, topic: String) throws -> String { 64 | let jsonText = try jsonrpc.json(from: request) 65 | let cipherText = try codec.encode(plainText: jsonText, key: request.url.key) 66 | let message = PubSubMessage(topic: topic, type: .pub, payload: cipherText) 67 | return try message.json() 68 | } 69 | 70 | func deserialize(_ text: String, url: WCURL) throws -> Request { 71 | let message = try PubSubMessage.message(from: text) 72 | let payloadText = try codec.decode(cipherText: message.payload, key: url.key) 73 | do { 74 | return try jsonrpc.request(from: payloadText, url: url) 75 | } catch { 76 | throw JSONRPCSerializerError.wrongIncommingDecodedTextFormat(payloadText) 77 | } 78 | } 79 | 80 | // MARK: - ResponseSerializer 81 | 82 | func serialize(_ response: Response, topic: String) throws -> String { 83 | let jsonText = try jsonrpc.json(from: response) 84 | let cipherText = try codec.encode(plainText: jsonText, key: response.url.key) 85 | let message = PubSubMessage(topic: topic, type: .pub, payload: cipherText) 86 | return try message.json() 87 | } 88 | 89 | func deserialize(_ text: String, url: WCURL) throws -> Response { 90 | let message = try PubSubMessage.message(from: text) 91 | let payloadText = try codec.decode(cipherText: message.payload, key: url.key) 92 | do { 93 | return try jsonrpc.response(from: payloadText, url: url) 94 | } catch { 95 | throw JSONRPCSerializerError.wrongIncommingDecodedTextFormat(payloadText) 96 | } 97 | } 98 | } 99 | 100 | fileprivate class JSONRPCAdapter { 101 | func json(from request: Request) throws -> String { 102 | return try request.json().string 103 | } 104 | 105 | func request(from json: String, url: WCURL) throws -> Request { 106 | let JSONRPCRequest = try JSONRPC_2_0.Request.create(from: JSONRPC_2_0.JSON(json)) 107 | return Request(payload: JSONRPCRequest, url: url) 108 | } 109 | 110 | func json(from response: Response) throws -> String { 111 | return try response.json().string 112 | } 113 | 114 | func response(from json: String, url: WCURL) throws -> Response { 115 | let JSONRPCResponse = try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(json)) 116 | return Response(payload: JSONRPCResponse, url: url) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/RequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class RequestTests: XCTestCase { 9 | let url = WCURL(topic: "topic", version: "1", bridgeURL: URL(string: "https://example.org/")!, key: "key") 10 | let method = "some_rpc_method_name" 11 | 12 | func test_noParameters() { 13 | let request = Request(url: url, method: method, id: "1") 14 | XCTAssertEqual(request.url, url) 15 | XCTAssertEqual(request.method, method) 16 | XCTAssertEqual(request.parameterCount, 0) 17 | XCTAssertThrowsError(try request.parameter(of: String.self, at: 0)) 18 | XCTAssertThrowsError(try request.parameter(of: String.self, key: "param")) 19 | XCTAssertEqual(try request.json(), """ 20 | {"id":"1","jsonrpc":"2.0","method":"\(method)"} 21 | """) 22 | } 23 | 24 | func test_idTypes() { 25 | let int = Request(url: url, method: method, id: 1) 26 | XCTAssertEqual(int.id as? Int, 1) 27 | XCTAssertEqual(int.internalID, .int(1)) 28 | 29 | let double = Request(url: url, method: method, id: 1.0) 30 | XCTAssertEqual(double.id as! Double, 1.0, accuracy: 0.1) 31 | XCTAssertEqual(double.internalID, .double(1.0)) 32 | 33 | let string = Request(url: url, method: method, id: "1") 34 | XCTAssertEqual(string.id as? String, "1") 35 | XCTAssertEqual(string.internalID, .string("1")) 36 | 37 | let null = Request(url: url, method: method, id: nil) 38 | XCTAssertNil(null.id as? String) 39 | XCTAssertEqual(null.internalID, .null) 40 | 41 | let `default` = Request(url: url, method: method) 42 | XCTAssertNotNil(`default`.id) 43 | } 44 | 45 | func test_positionalParameters_empty() throws { 46 | let request = try Request(url: url, method: method, params: [String](), id: "1") 47 | XCTAssertEqual(request.url, url) 48 | XCTAssertEqual(request.method, method) 49 | XCTAssertEqual(request.parameterCount, 0) 50 | XCTAssertThrowsError(try request.parameter(of: String.self, at: 0)) 51 | XCTAssertThrowsError(try request.parameter(of: String.self, key: "param")) 52 | XCTAssertEqual(try request.json(), """ 53 | {"id":"1","jsonrpc":"2.0","method":"\(method)","params":[]} 54 | """) 55 | } 56 | 57 | struct A: Codable, Equatable { 58 | var a: String 59 | } 60 | 61 | func test_positionalParameters_one() throws { 62 | let one = try Request(url: url, method: method, params: ["1"]) 63 | XCTAssertEqual(one.parameterCount, 1) 64 | XCTAssertEqual(try one.parameter(of: String.self, at: 0), "1") 65 | } 66 | 67 | func test_positionalParameters_object() throws { 68 | let object = try Request(url: url, method: method, params: [A(a: "1")]) 69 | XCTAssertEqual(object.parameterCount, 1) 70 | XCTAssertEqual(try object.parameter(of: A.self, at: 0), A(a: "1")) 71 | 72 | } 73 | 74 | func test_positionalParameters_manyObjects() throws { 75 | let many = try Request(url: url, method: method, params: [A(a: "1"), A(a: "2")], id: "1") 76 | XCTAssertEqual(many.parameterCount, 2) 77 | XCTAssertEqual(try many.parameter(of: A.self, at: 1), A(a: "2")) 78 | XCTAssertEqual(try many.json(), """ 79 | {"id":"1","jsonrpc":"2.0","method":"\(method)","params":[{"a":"1"},{"a":"2"}]} 80 | """) 81 | 82 | } 83 | 84 | struct B: Codable, Equatable { 85 | var a: String 86 | var b: String 87 | } 88 | 89 | func test_namedParameters_one() throws { 90 | let one = try Request(url: url, method: method, namedParams: A(a: "1")) 91 | XCTAssertEqual(one.parameterCount, 1) 92 | XCTAssertEqual(try one.parameter(of: String.self, key: "a"), "1") 93 | XCTAssertThrowsError(try one.parameter(of: A.self, at: 0)) 94 | } 95 | 96 | func test_namedParameterse_two() throws { 97 | let two = try Request(url: url, method: method, namedParams: B(a: "1", b: "2"), id: "1") 98 | XCTAssertEqual(two.parameterCount, 2) 99 | XCTAssertEqual(try two.parameter(of: String.self, key: "a"), "1") 100 | XCTAssertEqual(try two.parameter(of: String.self, key: "b"), "2") 101 | XCTAssertNil(try two.parameter(of: String.self, key: "c")) 102 | XCTAssertEqual(try two.json(), """ 103 | {"id":"1","jsonrpc":"2.0","method":"\(method)","params":{"a":"1","b":"2"}} 104 | """) 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /Sources/Internal/Communicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | class Communicator { 8 | // sessions are the approved connections between dApp and Wallet 9 | private let sessions: Sessions 10 | // triggered by Wallet or dApp to disconnect 11 | private let pendingDisconnectSessions: Sessions 12 | 13 | private let transport: Transport 14 | private let responseSerializer: ResponseSerializer 15 | private let requestSerializer: RequestSerializer 16 | 17 | init() { 18 | sessions = Sessions(queue: DispatchQueue(label: "org.walletconnect.swift.server.sessions")) 19 | pendingDisconnectSessions = Sessions(queue: DispatchQueue(label: "org.walletconnect.swift.server.pending")) 20 | transport = Bridge() 21 | let serializer = JSONRPCSerializer() 22 | responseSerializer = serializer 23 | requestSerializer = serializer 24 | } 25 | 26 | // MARK: - Sessions 27 | 28 | func session(by url: WCURL) -> Session? { 29 | return sessions.find(url: url) 30 | } 31 | 32 | func addOrUpdateSession(_ session: Session) { 33 | sessions.addOrUpdate(session) 34 | } 35 | 36 | func removeSession(by url: WCURL) { 37 | sessions.remove(url: url) 38 | } 39 | 40 | func openSessions() -> [Session] { 41 | return sessions.all().filter { transport.isConnected(by: $0.url) } 42 | } 43 | 44 | func isConnected(by url: WCURL) -> Bool { 45 | return transport.isConnected(by: url) 46 | } 47 | 48 | func disconnect(from url: WCURL) { 49 | transport.disconnect(from: url) 50 | } 51 | 52 | func pendingDisconnectSession(by url: WCURL) -> Session? { 53 | return pendingDisconnectSessions.find(url: url) 54 | } 55 | 56 | func addOrUpdatePendingDisconnectSession(_ session: Session) { 57 | pendingDisconnectSessions.addOrUpdate(session) 58 | } 59 | 60 | func removePendingDisconnectSession(by url: WCURL) { 61 | pendingDisconnectSessions.remove(url: url) 62 | } 63 | 64 | // MARK: - Transport 65 | 66 | func listen(on url: WCURL, 67 | onConnect: @escaping ((WCURL) -> Void), 68 | onDisconnect: @escaping ((WCURL, Error?) -> Void), 69 | onTextReceive: @escaping (String, WCURL) -> Void) { 70 | transport.listen(on: url, 71 | onConnect: onConnect, 72 | onDisconnect: onDisconnect, 73 | onTextReceive: onTextReceive) 74 | } 75 | 76 | func subscribe(on topic: String, url: WCURL) { 77 | let message = PubSubMessage(topic: topic, type: .sub, payload: "") 78 | transport.send(to: url, text: try! message.json()) 79 | } 80 | 81 | func send(_ response: Response, topic: String) { 82 | let text = try! responseSerializer.serialize(response, topic: topic) 83 | transport.send(to: response.url, text: text) 84 | } 85 | 86 | func send(_ request: Request, topic: String) { 87 | let text = try! requestSerializer.serialize(request, topic: topic) 88 | transport.send(to: request.url, text: text) 89 | } 90 | 91 | // MARK: - Serialization 92 | 93 | func request(from text: String, url: WCURL) throws -> Request { 94 | return try requestSerializer.deserialize(text, url: url) 95 | } 96 | 97 | func response(from text: String, url: WCURL) throws -> Response { 98 | return try responseSerializer.deserialize(text, url: url) 99 | } 100 | 101 | /// Thread-safe collection of Sessions 102 | private class Sessions { 103 | private var sessions: [WCURL: Session] = [:] 104 | private let queue: DispatchQueue 105 | 106 | init(queue: DispatchQueue) { 107 | self.queue = queue 108 | } 109 | 110 | func addOrUpdate(_ session: Session) { 111 | dispatchPrecondition(condition: .notOnQueue(queue)) 112 | queue.sync { [weak self] in 113 | self?.sessions[session.url] = session 114 | } 115 | } 116 | 117 | func all() -> [Session] { 118 | var result: [Session] = [] 119 | dispatchPrecondition(condition: .notOnQueue(queue)) 120 | queue.sync { [weak self] in 121 | guard let self = self else { return } 122 | result = Array(self.sessions.values) 123 | } 124 | return result 125 | } 126 | 127 | func find(url: WCURL) -> Session? { 128 | var result: Session? 129 | dispatchPrecondition(condition: .notOnQueue(queue)) 130 | queue.sync { [weak self] in 131 | guard let self = self else { return } 132 | result = self.sessions[url] 133 | } 134 | return result 135 | } 136 | 137 | func remove(url: WCURL) { 138 | dispatchPrecondition(condition: .notOnQueue(queue)) 139 | queue.sync { [weak self] in 140 | _ = self?.sessions.removeValue(forKey: url) 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Tests/ClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class ClientTests: XCTestCase { 9 | var client: Client! 10 | let delegate = MockClientDelegate() 11 | let communicator = MockCommunicator() 12 | 13 | override func setUp() { 14 | super.setUp() 15 | client = Client(delegate: delegate, dAppInfo: Session.DAppInfo.testDappInfo) 16 | client.communicator = communicator 17 | } 18 | 19 | func test_sendRequest_whenSessionNotFound_thenThrows() { 20 | XCTAssertThrowsError(try client.send(Request.testRequest, completion: nil), "sessionNotFound") { error in 21 | XCTAssertEqual(error as? Client.ClientError, .sessionNotFound) 22 | } 23 | } 24 | 25 | func test_sendRequest_whenNoWalletInfo_thenThrows() { 26 | communicator.addOrUpdateSession(Session.testSessionWithoutWalletInfo) 27 | XCTAssertThrowsError(try client.send(Request.testRequest, completion: nil), 28 | "missingWalletInfoInSession") { error in 29 | XCTAssertEqual(error as? Client.ClientError, .missingWalletInfoInSession) 30 | } 31 | } 32 | 33 | func test_sendRequest_callsCommunicator() { 34 | communicator.addOrUpdateSession(Session.testSession) 35 | try? client.send(Request.testRequest, completion: nil) 36 | XCTAssertNotNil(communicator.sentRequest) 37 | } 38 | 39 | func test_personal_sign_callsCommunicator() { 40 | let account = prepareAccountWithTestSession() 41 | try! client.personal_sign(url: WCURL.testURL, message: "Hi there", account: account) { _ in } 42 | XCTAssertNotNil(communicator.sentRequest) 43 | } 44 | 45 | func test_eth_sign_callsCommunicator() { 46 | let account = prepareAccountWithTestSession() 47 | try! client.eth_sign(url: WCURL.testURL, account: account, message: "smth") { _ in } 48 | XCTAssertNotNil(communicator.sentRequest) 49 | } 50 | 51 | func test_eth_signTypedData_callsCommunicator() { 52 | let account = prepareAccountWithTestSession() 53 | try! client.eth_signTypedData(url: WCURL.testURL, account: account, message: "smth") { _ in } 54 | XCTAssertNotNil(communicator.sentRequest) 55 | } 56 | 57 | func test_eth_sendTransaction_callsCommunicator() { 58 | prepareAccountWithTestSession() 59 | try! client.eth_sendTransaction(url: WCURL.testURL, transaction: Client.Transaction.testTransaction) { _ in } 60 | XCTAssertNotNil(communicator.sentRequest) 61 | } 62 | 63 | func test_eth_signTransaction_callsCommunicator() { 64 | prepareAccountWithTestSession() 65 | try! client.eth_signTransaction(url: WCURL.testURL, transaction: Client.Transaction.testTransaction) { _ in } 66 | XCTAssertNotNil(communicator.sentRequest) 67 | } 68 | 69 | @discardableResult 70 | private func prepareAccountWithTestSession() -> String { 71 | communicator.addOrUpdateSession(Session.testSession) 72 | return Session.testSession.walletInfo!.accounts[0] 73 | } 74 | 75 | func test_onConnect_whenSessionExists_thenSubscribesOnDappPeerIdTopic() { 76 | communicator.addOrUpdateSession(Session.testSession) 77 | client.onConnect(to: WCURL.testURL) 78 | XCTAssertEqual(communicator.subscribedOn?.topic, Session.testSession.dAppInfo.peerId) 79 | XCTAssertEqual(communicator.subscribedOn?.url, Session.testSession.url) 80 | } 81 | 82 | func test_onConnect_callsDelegate() { 83 | XCTAssertNil(delegate.connectedUrl) 84 | client.onConnect(to: WCURL.testURL) 85 | XCTAssertEqual(delegate.connectedUrl, WCURL.testURL) 86 | } 87 | 88 | func test_onConnect_whenSessionExists_thenCallsDelegate() { 89 | communicator.addOrUpdateSession(Session.testSession) 90 | XCTAssertNil(delegate.connectedSession) 91 | client.onConnect(to: WCURL.testURL) 92 | XCTAssertEqual(delegate.connectedSession, Session.testSession) 93 | } 94 | 95 | func test_onConnect_whenHandshakeInProcess_thenSubscribesOnDappPeerIdTopic() { 96 | client.onConnect(to: WCURL.testURL) 97 | XCTAssertEqual(communicator.subscribedOn?.topic, Session.DAppInfo.testDappInfo.peerId) 98 | XCTAssertEqual(communicator.subscribedOn?.url, WCURL.testURL) 99 | } 100 | 101 | func test_onConnect_whenHandshakeInProcess_thenCallsCommunicator() { 102 | client.onConnect(to: WCURL.testURL) 103 | XCTAssertNotNil(communicator.sentRequest) 104 | } 105 | } 106 | 107 | class MockClientDelegate: ClientDelegate { 108 | var didFailToConnect = false 109 | func client(_ client: Client, didFailToConnect url: WCURL) { 110 | didFailToConnect = true 111 | } 112 | 113 | var connectedUrl: WCURL? 114 | func client(_ client: Client, didConnect url: WCURL) { 115 | connectedUrl = url 116 | } 117 | 118 | var connectedSession: Session? 119 | func client(_ client: Client, didConnect session: Session) { 120 | connectedSession = session 121 | } 122 | 123 | var disconnectedSession: Session? 124 | func client(_ client: Client, didDisconnect session: Session) { 125 | disconnectedSession = session 126 | } 127 | 128 | var didUpdateSession: Session? 129 | func client(_ client: Client, didUpdate session: Session) { 130 | didUpdateSession = session 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Sources/PublicInterface/WalletConnect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | open class WalletConnect { 6 | var communicator = Communicator() 7 | 8 | public init() {} 9 | 10 | public enum WalletConnectError: Error { 11 | case tryingToConnectExistingSessionURL 12 | case tryingToDisconnectInactiveSession 13 | case missingWalletInfoInSession 14 | } 15 | 16 | /// Connect to WalletConnect url 17 | /// https://docs.walletconnect.org/tech-spec#requesting-connection 18 | /// 19 | /// - Parameter url: WalletConnect url 20 | /// - Throws: error on trying to connect to existing session url 21 | open func connect(to url: WCURL) throws { 22 | guard communicator.session(by: url) == nil else { 23 | throw WalletConnectError.tryingToConnectExistingSessionURL 24 | } 25 | listen(on: url) 26 | } 27 | 28 | /// Reconnect to the session 29 | /// 30 | /// - Parameter session: session object with wallet info. 31 | /// - Throws: error if wallet info is missing 32 | open func reconnect(to session: Session) throws { 33 | guard session.walletInfo != nil else { 34 | throw WalletConnectError.missingWalletInfoInSession 35 | } 36 | communicator.addOrUpdateSession(session) 37 | listen(on: session.url) 38 | } 39 | 40 | /// Disconnect from session. 41 | /// 42 | /// - Parameter session: Session object 43 | /// - Throws: error on trying to disconnect inacative sessoin. 44 | open func disconnect(from session: Session) throws { 45 | guard communicator.isConnected(by: session.url) else { 46 | throw WalletConnectError.tryingToDisconnectInactiveSession 47 | } 48 | try sendDisconnectSessionRequest(for: session) 49 | communicator.addOrUpdatePendingDisconnectSession(session) 50 | communicator.disconnect(from: session.url) 51 | } 52 | 53 | /// Get all sessions with active connection. 54 | /// 55 | /// - Returns: sessions list. 56 | open func openSessions() -> [Session] { 57 | return communicator.openSessions() 58 | } 59 | 60 | private func listen(on url: WCURL) { 61 | let onConnect: ((WCURL) -> Void) = { [weak self] url in 62 | self?.onConnect(to: url) 63 | } 64 | let onDisconnect: ((WCURL, Error?) -> Void) = { [weak self] (url, error) in 65 | self?.onDisconnect(from: url, error: error) 66 | } 67 | let onTextReceive: ((String, WCURL) -> Void) = { [weak self] (text, url) in 68 | self?.onTextReceive(text, from: url) 69 | } 70 | communicator.listen(on: url, 71 | onConnect: onConnect, 72 | onDisconnect: onDisconnect, 73 | onTextReceive: onTextReceive) 74 | } 75 | 76 | /// Confirmation from Transport layer that connection was successfully established. 77 | /// 78 | /// - Parameter url: WalletConnect url 79 | func onConnect(to url: WCURL) { 80 | preconditionFailure("Should be implemented in subclasses") 81 | } 82 | 83 | //// Confirmation from Transport layer that connection was dropped. 84 | /// 85 | /// - Parameters: 86 | /// - url: WalletConnect url 87 | /// - error: error that triggered the disconnection 88 | private func onDisconnect(from url: WCURL, error: Error?) { 89 | LogService.shared.info("WC: didDisconnect url: \(url.bridgeURL.absoluteString)") 90 | // check if disconnect happened during handshake 91 | guard let session = communicator.session(by: url) else { 92 | failedToConnect(url) 93 | return 94 | } 95 | // if a session was not initiated by the wallet or the dApp to disconnect, try to reconnect it. 96 | guard communicator.pendingDisconnectSession(by: url) != nil else { 97 | LogService.shared.info("WC: trying to reconnect session by url: \(url.bridgeURL.absoluteString)") 98 | willReconnect(session) 99 | try! reconnect(to: session) 100 | return 101 | } 102 | communicator.removeSession(by: url) 103 | communicator.removePendingDisconnectSession(by: url) 104 | didDisconnect(session) 105 | } 106 | 107 | /// Process incomming text messages from the transport layer. 108 | /// 109 | /// - Parameters: 110 | /// - text: incoming message 111 | /// - url: WalletConnect url 112 | func onTextReceive(_ text: String, from url: WCURL) { 113 | preconditionFailure("Should be implemented in subclasses") 114 | } 115 | 116 | func sendDisconnectSessionRequest(for session: Session) throws { 117 | preconditionFailure("Should be implemented in subclasses") 118 | } 119 | 120 | func failedToConnect(_ url: WCURL) { 121 | preconditionFailure("Should be implemented in subclasses") 122 | } 123 | 124 | func didDisconnect(_ session: Session) { 125 | preconditionFailure("Should be implemented in subclasses") 126 | } 127 | 128 | func willReconnect(_ session: Session) { 129 | preconditionFailure("Should be implemented in subclasses") 130 | } 131 | 132 | func log(_ request: Request) { 133 | guard let text = try? request.json().string else { return } 134 | LogService.shared.detailed("WC: <== [request] \(text)") 135 | } 136 | 137 | func log(_ response: Response) { 138 | guard let text = try? response.json().string else { return } 139 | 140 | if text.contains("\"error\"") { 141 | LogService.shared.error("WC: <== [response] \(text)") 142 | } else { 143 | LogService.shared.detailed("WC: <== [response] \(text)") 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/SessionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | class SessionTests: XCTestCase { 9 | let url = WCURL("wc:topic@1?bridge=https%3A%2F%2Ftest.com&key=key")! 10 | let walletId = UUID().uuidString 11 | 12 | func test_canCreateSessionFromRequest() throws { 13 | let session = try createServerSession() 14 | XCTAssertEqual(session.url, url) 15 | XCTAssertEqual(session.dAppInfo.peerId, "Slow.Trade ID") 16 | XCTAssertEqual(session.dAppInfo.peerMeta.description, "Good trades take time") 17 | XCTAssertEqual(session.dAppInfo.peerMeta.url, URL(string: "https://slow.trade")!) 18 | XCTAssertEqual(session.dAppInfo.peerMeta.icons, [URL(string: "https://example.com/1.png")!, 19 | URL(string: "https://example.com/2.png")!]) 20 | XCTAssertEqual(session.dAppInfo.peerMeta.name, "Slow Trade") 21 | XCTAssertNil(session.walletInfo) 22 | } 23 | 24 | func test_canCreateSessionFromResponse() throws { 25 | let session = try createClientSession() 26 | XCTAssertNotNil(session.dAppInfo) 27 | XCTAssertEqual(session.url, url) 28 | XCTAssertEqual(session.walletInfo?.approved, true) 29 | XCTAssertEqual(session.walletInfo?.accounts, ["0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95"]) 30 | XCTAssertEqual(session.walletInfo?.chainId, 4) 31 | XCTAssertEqual(session.walletInfo?.peerId, "Gnosis Safe ID") 32 | XCTAssertEqual(session.walletInfo?.peerMeta.name, "Gnosis Safe") 33 | XCTAssertEqual(session.walletInfo?.peerMeta.url, URL(string: "https://safe.gnosis.io")!) 34 | XCTAssertEqual(session.walletInfo?.peerMeta.icons, [URL(string: "https://example.com/1.png")!, 35 | URL(string: "https://example.com/2.png")!]) 36 | XCTAssertEqual(session.walletInfo?.peerMeta.description, "Secure 2FA Wallet") 37 | } 38 | 39 | func test_creationResponse() throws { 40 | let session = try createServerSession() 41 | let info = Session.WalletInfo(approved: true, 42 | accounts: ["0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95"], 43 | chainId: 1, 44 | peerId: walletId, 45 | peerMeta: Session.ClientMeta(name: "Gnosis Safe", 46 | description: "Secure Wallet", 47 | icons: [URL(string: "https://example.com/1.png")!], 48 | url: URL(string: "gnosissafe://")!)) 49 | let response = try Response(url: session.url, value: info, id: 100) 50 | XCTAssertEqual(response.url, session.url) 51 | XCTAssertEqual(response.internalID, .int(100)) 52 | 53 | let resultInfo = try response.result(as: Session.WalletInfo.self) 54 | XCTAssertEqual(resultInfo, info) 55 | } 56 | 57 | private func createServerSession() throws -> Session { 58 | let JSONPRCRequest = try JSONRPC_2_0.Request.create(from: JSONRPC_2_0.JSON(WCSessionRequest.json)) 59 | let request = Request(payload: JSONPRCRequest, url: url) 60 | let dappInfo = try! request.parameter(of: Session.DAppInfo.self, at: 0) 61 | let session = Session(url: request.url, dAppInfo: dappInfo, walletInfo: nil) 62 | return session 63 | } 64 | 65 | private func createClientSession() throws -> Session { 66 | let JSONPRCResponse = try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(WCSessionResponse.json)) 67 | let response = Response(payload: JSONPRCResponse, url: url) 68 | let dAppInfo = Session.DAppInfo(peerId: "Slow.Trade ID", 69 | peerMeta: Session.ClientMeta(name: "Slow Trade", 70 | description: "Good trades take time", 71 | icons: [URL(string: "https://example.com/1.png")!], 72 | url: URL(string: "https://slow.trade")!)) 73 | let walletInfo = try response.result(as: [Session.WalletInfo].self) 74 | return Session(url: url, dAppInfo: dAppInfo, walletInfo: walletInfo[0]) 75 | } 76 | } 77 | 78 | fileprivate enum WCSessionRequest { 79 | static let json = """ 80 | { 81 | "id": 100, 82 | "jsonrpc": "2.0", 83 | "method": "wc_sessionRequest", 84 | "params": [ 85 | { 86 | "peerId": "Slow.Trade ID", 87 | "peerMeta": { 88 | "description": "Good trades take time", 89 | "url": "https://slow.trade", 90 | "icons": ["https://example.com/1.png", "https://example.com/2.png"], 91 | "name": "Slow Trade" 92 | } 93 | } 94 | ] 95 | } 96 | """ 97 | } 98 | 99 | fileprivate enum WCSessionResponse { 100 | static let json = """ 101 | { 102 | "id": 100, 103 | "jsonrpc": "2.0", 104 | "result": [ 105 | { 106 | "peerId": "Gnosis Safe ID", 107 | "peerMeta": { 108 | "description": "Secure 2FA Wallet", 109 | "url": "https://safe.gnosis.io", 110 | "icons": ["https://example.com/1.png", "https://example.com/2.png"], 111 | "name": "Gnosis Safe" 112 | }, 113 | "approved": true, 114 | "chainId": 4, 115 | "accounts": ["0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95"] 116 | } 117 | ] 118 | } 119 | """ 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WalletConnectSwift 2 | 3 | Swift SDK implementing [WalletConnect 1.x.x](https://docs.walletconnect.org) protocol for native iOS Dapps and Wallets. 4 | 5 | # Features 6 | 7 | - Server (wallet side) 8 | - Create, reconnect, disconnect, and update session 9 | - Flexible, extendable request handling with `Codable` support via JSON RPC 2.0 10 | 11 | - Client (native dapp side) 12 | - Create, reconnect, disconnect, and update session 13 | - Default implementation of [WalletConnect SDK API](https://docs.walletconnect.org/json-rpc/ethereum) 14 | - `personal_sign` 15 | - `eth_sign` 16 | - `eth_signTypedData` 17 | - `eth_sendTransaction` 18 | - `eth_signTransaction` 19 | - `eth_sendRawTransaction` 20 | - Send custom RPC requests with `Codable` support via JSON RPC 2.0 21 | 22 | # Example Code 23 | Example code is in a separate repository: https://github.com/WalletConnect/WalletConnectSwift-Example 24 | 25 | - Wallet Example App: 26 | - Connecting via QR code reader 27 | - Connecting via deep link ("wc" scheme) 28 | - Reconnecting after restart 29 | - Examples of request handlers 30 | - Dapp Example App: 31 | - Connecting via QR code reader 32 | - Connecting via deep link ("wc" scheme) 33 | - Reconnecting after restart 34 | - Examples of request handlers 35 | 36 | ## Usage in a Wallet 37 | 38 | To start connections, you need to create and retain a `Server` object to which you provide a delegate: 39 | 40 | ```Swift 41 | let server = Server(delegate: self) 42 | ``` 43 | 44 | The library handles WalletConnect-specific session requests for you - `wc_sessionRequest` and `wc_sessionUpdate`. 45 | 46 | To register for the important session update events, implement the delegate methods `shouldStart`, `didConnect`, `didDisconnect` and `didFailToConnect`. 47 | 48 | By default, the server cannot handle any other reqeusts - you need to provide your implementation. 49 | 50 | You do this by registering request handlers. You have the flexibility to register one handler per request method, or a catch-all request handler. 51 | 52 | 53 | ```Swift 54 | server.register(handler: PersonalSignHandler(for: self, server: server, wallet: wallet)) 55 | ``` 56 | 57 | Handlers are asked (in order of registration) whether they can handle each request. First handler that returns `true` from `canHandle(request:)` method will get the `handle(request:)` call. All other handlers will be skipped. 58 | 59 | In the request handler, check the incoming request's method in `canHandle` implementation, and handle actual request in the `handle(request:)` implementation. 60 | 61 | ```Swift 62 | func canHandle(request: Request) -> Bool { 63 | return request.method == "eth_signTransaction" 64 | } 65 | ``` 66 | 67 | You can send back response for the request through the server using `send` method: 68 | 69 | ```Swift 70 | func handle(request: Request) { 71 | // do you stuff here ... 72 | 73 | // error response - rejected by user 74 | server.send(.reject(request)) 75 | 76 | // or send actual response - assuming the request.id exists, and MyCodableStruct type defined 77 | try server.send(Response(url: request.url, value: MyCodableStruct(value: "Something"), id: request.id!)) 78 | } 79 | ``` 80 | 81 | For more details, see the `ExampleApps/ServerApp` 82 | 83 | 84 | ## Usage in a Dapp 85 | 86 | To start connections, you need to create and keep alive a `Client` object to which you provide `DappInfo` and a delegate: 87 | 88 | ```Swift 89 | let client = Client(delegate: self, dAppInfo: dAppInfo) 90 | ``` 91 | 92 | The delegate then will receive calls when connection established, failed, or disconnected. 93 | 94 | Upon successful connection, you can invoke various API methods on the `Client`. 95 | 96 | ```Swift 97 | try? client.personal_sign(url: session.url, message: "Hi there!", account: session.walletInfo!.accounts[0]) { 98 | [weak self] response in 99 | // handle the response from Wallet here 100 | } 101 | ``` 102 | 103 | You can also send a custom request. The request ID required by JSON RPC is generated and handled by the library internally. 104 | 105 | ```Swift 106 | try? client.send(Request(url: url, method: "eth_gasPrice")) { [weak self] response in 107 | // handle the response 108 | } 109 | ``` 110 | 111 | You can convert the received response result to a `Decodable` type. 112 | 113 | ```Swift 114 | let nonceString = try response.result(as: String.self) 115 | ``` 116 | 117 | You can also check if the wallet responded with error: 118 | 119 | ```Swift 120 | if let error = response.error { // NSError 121 | // handle error 122 | } 123 | ``` 124 | 125 | For more details, see the `ExampleApps/ClientApp` 126 | 127 | ## Logs 128 | 129 | Filter the type of logs being printed by changing the log level `LogService.level = .info`. 130 | 131 | # Running Example Apps 132 | 133 | Please open `ExampleApps/ExampleApps.xcodeproj` 134 | 135 | # Installation 136 | 137 | ## Prerequisites 138 | 139 | - iOS 13.0 or macOS 10.14 140 | - Swift 5 141 | 142 | ## WalletConnectSwift dependencies 143 | 144 | - CryptoSwift - for cryptography operations 145 | 146 | ## Swift Package Manager 147 | 148 | In your `Package.swift`: 149 | 150 | dependencies: [ 151 | .package(url: "https://github.com/WalletConnect/WalletConnectSwift.git", .upToNextMinor(from: "1.2.0")) 152 | ] 153 | 154 | ## CocoaPods 155 | 156 | In your `Podfile`: 157 | 158 | platform :ios, '13.0' 159 | use_frameworks! 160 | 161 | target 'MyApp' do 162 | pod 'WalletConnectSwift' 163 | end 164 | 165 | ## Carthage 166 | 167 | In your `Cartfile`: 168 | 169 | github "WalletConnect/WalletConnectSwift" 170 | 171 | Run `carthage update` to build the framework and drag the WalletConnectSwift.framework in your Xcode project. 172 | 173 | # Acknowledgments 174 | 175 | We'd like to thank [Trust Wallet](https://github.com/trustwallet/wallet-connect-swift) team for inspiration in implementing this library. 176 | 177 | # Contributors 178 | 179 | * Andrey Scherbovich ([sche](https://github.com/sche)) 180 | * Dmitry Bespalov ([DmitryBespalov](https://github.com/DmitryBespalov)) 181 | 182 | # License 183 | 184 | MIT License (see the LICENSE file). 185 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public class Request { 8 | public var url: WCURL 9 | 10 | public var method: Method { 11 | return payload.method 12 | } 13 | public var id: RequestID? { 14 | return internalID?.requestID 15 | } 16 | public var jsonString: String { 17 | if let str = try? json().string { 18 | return str 19 | } 20 | return "" 21 | } 22 | 23 | internal var internalID: JSONRPC_2_0.IDType? { 24 | return payload.id 25 | } 26 | 27 | private var payload: JSONRPC_2_0.Request 28 | 29 | internal init(payload: JSONRPC_2_0.Request, url: WCURL) { 30 | self.payload = payload 31 | self.url = url 32 | } 33 | 34 | /// Generates new ID for a request that is compatible with JS clients. 35 | /// - Returns: new ID 36 | public static func payloadId() -> RequestID { 37 | // Port of the payloadId() from the JS library to make sure that 38 | // generated IDs fit into JS's number size (max integer size is 39 | // 2^53 - 1). 40 | // 41 | // NOTE: new Date().getTime() is time in milliseconds in JS, however, 42 | // in iOS it is time in seconds. So instead of 1000, we multiply by 1 million 43 | // 44 | // NOTE: the id may still be greater than JS max number, but 45 | // this would happen if the date is in year 2255 46 | // 47 | // JS code: 48 | // const datePart: number = new Date().getTime() * Math.pow(10, 3) 49 | // const extraPart: number = Math.floor(Math.random() * Math.pow(10, 3)) 50 | // const id: number = datePart + extraPart 51 | let datePart = Int(Date().timeIntervalSince1970 * 1000_000) 52 | let extraPart = Int.random(in: 0..<1000) 53 | let id = datePart + extraPart 54 | return id 55 | } 56 | 57 | public convenience init(url: WCURL, method: Method, id: RequestID? = payloadId()) { 58 | let payload = JSONRPC_2_0.Request(method: method, params: nil, id: JSONRPC_2_0.IDType(id)) 59 | self.init(payload: payload, url: url) 60 | } 61 | 62 | public convenience init(url: WCURL, method: Method, params: [T], id: RequestID? = payloadId()) throws { 63 | let data = try JSONEncoder.encoder().encode(params) 64 | let values = try JSONDecoder().decode([JSONRPC_2_0.ValueType].self, from: data) 65 | let parameters = JSONRPC_2_0.Request.Params.positional(values) 66 | let payload = JSONRPC_2_0.Request(method: method, params: parameters, id: JSONRPC_2_0.IDType(id)) 67 | self.init(payload: payload, url: url) 68 | } 69 | 70 | public convenience init(url: WCURL, method: Method, namedParams params: T, id: RequestID? = payloadId()) throws { 71 | let data = try JSONEncoder.encoder().encode(params) 72 | let values = try JSONDecoder().decode([String: JSONRPC_2_0.ValueType].self, from: data) 73 | let parameters = JSONRPC_2_0.Request.Params.named(values) 74 | let payload = JSONRPC_2_0.Request(method: method, params: parameters, id: JSONRPC_2_0.IDType(id)) 75 | self.init(payload: payload, url: url) 76 | } 77 | 78 | public var parameterCount: Int { 79 | guard let params = payload.params else { return 0 } 80 | switch params { 81 | case .named(let values): return values.count 82 | case .positional(let values): return values.count 83 | } 84 | } 85 | 86 | public func parameter(of type: T.Type, at position: Int) throws -> T { 87 | guard let params = payload.params else { 88 | throw RequestError.parametersDoNotExist 89 | } 90 | switch params { 91 | case .named: 92 | throw RequestError.positionalParametersDoNotExist 93 | case .positional(let values): 94 | if position >= values.count { 95 | throw RequestError.parameterPositionOutOfBounds 96 | } 97 | return try values[position].decode(to: type) 98 | } 99 | } 100 | 101 | public func parameter(of type: T.Type, key: String) throws -> T? { 102 | guard let params = payload.params else { 103 | throw RequestError.parametersDoNotExist 104 | } 105 | 106 | switch params { 107 | case .positional: 108 | throw RequestError.namedParametersDoNotExist 109 | case .named(let values): 110 | guard let value = values[key] else { 111 | return nil 112 | } 113 | return try value.decode(to: type) 114 | } 115 | } 116 | 117 | internal func json() throws -> JSONRPC_2_0.JSON { 118 | return try payload.json() 119 | } 120 | } 121 | 122 | 123 | public enum RequestError: Error { 124 | case positionalParametersDoNotExist 125 | case parametersDoNotExist 126 | case parameterPositionOutOfBounds 127 | case namedParametersDoNotExist 128 | } 129 | 130 | /// RPC method names are Strings 131 | public typealias Method = String 132 | 133 | /// Protocol marker for request identifier type. It is any value of types String, Int, Double, or nil 134 | public protocol RequestID {} 135 | 136 | extension String: RequestID {} 137 | extension Int: RequestID {} 138 | extension Double: RequestID {} 139 | 140 | 141 | internal extension JSONRPC_2_0.ValueType { 142 | init(_ value: T) throws { 143 | // Encodable types can be primitives (i.e. String), which encode to invalid JSON (root must be Array or Dict) 144 | let wrapped = try JSONEncoder.encoder().encode([value]) 145 | let unwrapped = try JSONDecoder().decode([JSONRPC_2_0.ValueType].self, from: wrapped) 146 | self = unwrapped[0] 147 | } 148 | 149 | func decode(to type: T.Type) throws -> T { 150 | let data = try JSONEncoder.encoder().encode([self]) // wrap in array because values can be primitive types which is invalid json 151 | let result = try JSONDecoder().decode([T].self, from: data) 152 | return result[0] 153 | } 154 | } 155 | 156 | internal extension JSONRPC_2_0.IDType { 157 | init(_ value: RequestID?) { 158 | switch value { 159 | case .none: self = .null 160 | case .some(let wrapped): 161 | if wrapped is String { 162 | self = .string((wrapped as! String)) 163 | } else if wrapped is Int { 164 | self = .int(wrapped as! Int) 165 | } else if wrapped is Double { 166 | self = .double(wrapped as! Double) 167 | } else { 168 | preconditionFailure("Unknown Request ID IDType") 169 | } 170 | } 171 | } 172 | 173 | var requestID: RequestID? { 174 | switch self { 175 | case .string(let value): return value 176 | case .int(let value): return value 177 | case .double(let value): return value 178 | case .null: return nil 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tests/JSONRPC_2_0_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import XCTest 6 | @testable import WalletConnectSwift 7 | 8 | // swiftlint:disable literal_expression_end_indentation line_length 9 | class JSONRPC_2_0_Tests: XCTestCase { 10 | let decoder = JSONDecoder() 11 | let encoder = JSONEncoder.encoder() 12 | 13 | func test_JSONRPC_ValueType_Serialization_Decerialization() throws { 14 | let data = JSONRPC_StubRequest.json1_Text.data(using: .utf8)! 15 | let res = try decoder.decode(JSONRPC_2_0.ValueType.self, from: data) 16 | XCTAssertEqual(res, JSONRPC_StubRequest.json1_ValueType) 17 | let data2 = try encoder.encode(JSONRPC_StubRequest.json1_ValueType) 18 | let res2 = try decoder.decode(JSONRPC_2_0.ValueType.self, from: data2) 19 | XCTAssertEqual(res2, JSONRPC_StubRequest.json1_ValueType) 20 | } 21 | 22 | func test_JSONRPC_Params_Serialization_Decerializatio() throws { 23 | let walletInfo = Session.WalletInfo(approved: false, 24 | accounts: [], 25 | chainId: 1, 26 | peerId: "1", 27 | peerMeta: Session.ClientMeta(name: "test", 28 | description: "test", 29 | icons: [], 30 | url: URL(string: "test")!)) 31 | let walletInfoData = try encoder.encode(walletInfo) 32 | let paramsObject = try decoder.decode(JSONRPC_2_0.Request.Params.self, from: walletInfoData) 33 | let paramsData = try encoder.encode(paramsObject) 34 | let restoredWalletInfo = try! decoder.decode(Session.WalletInfo.self, from: paramsData) 35 | XCTAssertEqual(walletInfo.approved, restoredWalletInfo.approved) 36 | XCTAssertEqual(walletInfo.accounts, restoredWalletInfo.accounts) 37 | XCTAssertEqual(walletInfo.chainId, restoredWalletInfo.chainId) 38 | } 39 | 40 | func test_JSONRPC_Request_Serialization_Decerialization() throws { 41 | let req = try JSONRPC_2_0.Request.create(from: JSONRPC_2_0.JSON(JSONRPC_StubRequest.json1_Text)) 42 | XCTAssertEqual(req, JSONRPC_StubRequest.json1_Request) 43 | let json = try JSONRPC_StubRequest.json1_Request.json() 44 | let req2 = try JSONRPC_2_0.Request.create(from: json) 45 | XCTAssertEqual(req, req2) 46 | } 47 | 48 | func test_JSONRPC_Response_Serialization_Decerialization() throws { 49 | // value payload 50 | let resp = try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(JSONRPC_StubResposne.json1_Text)) 51 | XCTAssertEqual(resp, JSONRPC_StubResposne.json1_Response) 52 | let json = try JSONRPC_StubResposne.json1_Response.json() 53 | let resp2 = try JSONRPC_2_0.Response.create(from: json) 54 | XCTAssertEqual(resp, resp2) 55 | 56 | // error payload 57 | let errResp = try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(JSONRPC_StubResposne.json2_Text)) 58 | XCTAssertEqual(errResp, JSONRPC_StubResposne.json2_Response) 59 | let json2 = try JSONRPC_StubResposne.json2_Response.json() 60 | XCTAssertTrue(json2.string.contains("error")) 61 | let errResp2 = try JSONRPC_2_0.Response.create(from: json2) 62 | XCTAssertEqual(errResp, errResp2) 63 | } 64 | 65 | func test_null_result() throws { 66 | let string = """ 67 | {"jsonrpc":"2.0","id":1563895837415317,"result":null} 68 | """ 69 | XCTAssertNoThrow(try JSONRPC_2_0.Response.create(from: JSONRPC_2_0.JSON(string))) 70 | } 71 | 72 | } 73 | 74 | fileprivate enum JSONRPC_StubRequest { 75 | 76 | static let json1_Text = """ 77 | { 78 | "id": 100, 79 | "jsonrpc": "2.0", 80 | "method": "wc_sessionRequest", 81 | "params": { 82 | "peerId": "peerId", 83 | "peerMeta": { 84 | "description": "Good trades take time.", 85 | "url": "https://slow.trade", 86 | "icons": ["https://test.com/icon1.png", "https://test.com/icon2.png"], 87 | "name": "Slow Trade" 88 | } 89 | } 90 | } 91 | """ 92 | static let json1_ValueType = JSONRPC_2_0.ValueType.object([ 93 | "id": .int(100), 94 | "jsonrpc": .string("2.0"), 95 | "method": .string("wc_sessionRequest"), 96 | "params": .object([ 97 | "peerId": .string("peerId"), 98 | "peerMeta": .object([ 99 | "description": .string("Good trades take time."), 100 | "url": .string("https://slow.trade"), 101 | "icons": .array([.string("https://test.com/icon1.png"), .string("https://test.com/icon2.png")]), 102 | "name": .string("Slow Trade") 103 | ]) 104 | ]) 105 | ]) 106 | 107 | static let json1_Request = 108 | JSONRPC_2_0.Request(method: "wc_sessionRequest", 109 | params: JSONRPC_2_0.Request.Params.named([ 110 | "peerId": .string("peerId"), 111 | "peerMeta": .object([ 112 | "description": .string("Good trades take time."), 113 | "url": .string("https://slow.trade"), 114 | "icons": .array([.string("https://test.com/icon1.png"), 115 | .string("https://test.com/icon2.png")]), 116 | "name": .string("Slow Trade") 117 | ]) 118 | ]), 119 | id: JSONRPC_2_0.IDType.int(100)) 120 | 121 | } 122 | 123 | fileprivate enum JSONRPC_StubResposne { 124 | 125 | static let json1_Text = """ 126 | { 127 | "id": 100, 128 | "jsonrpc": "2.0", 129 | "result": { 130 | "approved": true, 131 | "chainId": 1, 132 | "accounts": ["0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95", "0x46F228b5eFD19Be20952152c549ee478Bf1bf36b"] 133 | } 134 | } 135 | """ 136 | 137 | static let json1_Response = 138 | JSONRPC_2_0.Response(result: .value(.object(["approved": .bool(true), 139 | "chainId": .int(1), 140 | "accounts": .array([.string("0xCF4140193531B8b2d6864cA7486Ff2e18da5cA95"), 141 | .string("0x46F228b5eFD19Be20952152c549ee478Bf1bf36b")])])), 142 | id: .int(100)) 143 | 144 | static let json2_Text = """ 145 | { 146 | "id": null, 147 | "jsonrpc": "2.0", 148 | "error": { 149 | "code": -30000, 150 | "message": "failure", 151 | "data": [1, 2, 3] 152 | } 153 | } 154 | """ 155 | 156 | static let json2_Response = 157 | JSONRPC_2_0.Response(result: .error(JSONRPC_2_0.Response.Payload 158 | .ErrorPayload(code: try! JSONRPC_2_0.Response.Payload.ErrorPayload.Code(-30_000), 159 | message: "failure", 160 | data: .array([.int(1), .int(2), .int(3)]))), 161 | id: .null) 162 | } 163 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Server.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol RequestHandler: AnyObject { 8 | func canHandle(request: Request) -> Bool 9 | func handle(request: Request) 10 | } 11 | 12 | public protocol ServerDelegate: AnyObject { 13 | /// Websocket connection was dropped during handshake. The connectoin process should be initiated again. 14 | func server(_ server: Server, didFailToConnect url: WCURL) 15 | 16 | /// The handshake will be established based on "approved" property of WalletInfo. 17 | func server(_ server: Server, shouldStart session: Session, completion: @escaping (Session.WalletInfo) -> Void) 18 | 19 | /// Called when the session is connected or reconnected. 20 | /// Reconnection may happen as a result of Wallet intention to reconnect, or as a result of 21 | /// the server trying to restore lost connection. 22 | func server(_ server: Server, didConnect session: Session) 23 | 24 | /// Called only when the session is disconnect with intention of the dApp or the Wallet. 25 | func server(_ server: Server, didDisconnect session: Session) 26 | 27 | /// Called only when the session is updated with intention of the dAppt. 28 | func server(_ server: Server, didUpdate session: Session) 29 | } 30 | 31 | public protocol ServerDelegateV2: ServerDelegate { 32 | /// Replacement for the `server(_:shouldStart:completion:) method that makes possible async approval process. 33 | /// When the approval is ready, call the `Server.sendCreateSessionResponse` method. 34 | /// If you implement this protocol, the other `shouldStart` method will not be called 35 | /// 36 | /// 37 | /// - Parameters: 38 | /// - server: the server object 39 | /// - requestId: connection request's id. Can be Int, Double, or String 40 | /// - session: the session to create. Contains dapp info received in the connection request. 41 | func server(_ server: Server, didReceiveConnectionRequest requestId: RequestID, for session: Session) 42 | 43 | /// Called when the session is being reconnected as part of the retry mechanism after the connection 44 | /// has been lost due to e.g. bad connectivity. 45 | func server(_ server: Server, willReconnect session: Session) 46 | } 47 | 48 | open class Server: WalletConnect { 49 | private let handlers: Handlers 50 | public private(set) weak var delegate: ServerDelegate? 51 | 52 | public enum ServerError: Error { 53 | case missingWalletInfoInSession 54 | case failedToCreateSessionResponse 55 | } 56 | 57 | public init(delegate: ServerDelegate) { 58 | self.delegate = delegate 59 | handlers = Handlers(queue: DispatchQueue(label: "org.walletconnect.swift.server.handlers")) 60 | super.init() 61 | register(handler: HandshakeHandler(delegate: self)) 62 | register(handler: UpdateSessionHandler(delegate: self)) 63 | } 64 | 65 | open func register(handler: RequestHandler) { 66 | handlers.add(handler) 67 | } 68 | 69 | open func unregister(handler: RequestHandler) { 70 | handlers.remove(handler) 71 | } 72 | 73 | /// Update session with new wallet info. 74 | /// 75 | /// - Parameters: 76 | /// - session: Session object 77 | /// - walletInfo: WalletInfo object 78 | /// - Throws: error if wallet info is missing 79 | open func updateSession(_ session: Session, with walletInfo: Session.WalletInfo) throws { 80 | guard session.walletInfo != nil else { 81 | throw ServerError.missingWalletInfoInSession 82 | } 83 | let request = try Request(url: session.url, method: "wc_sessionUpdate", params: [walletInfo], id: nil) 84 | send(request) 85 | } 86 | 87 | // TODO: where to handle error? 88 | open func send(_ response: Response) { 89 | guard let session = communicator.session(by: response.url) else { return } 90 | communicator.send(response, topic: session.dAppInfo.peerId) 91 | } 92 | 93 | // TODO: where to handle error? 94 | open func send(_ request: Request) { 95 | guard let session = communicator.session(by: request.url) else { return } 96 | communicator.send(request, topic: session.dAppInfo.peerId) 97 | } 98 | 99 | override func onTextReceive(_ text: String, from url: WCURL) { 100 | do { 101 | // we handle only properly formed JSONRPC 2.0 requests. JSONRPC 2.0 responses are ignored. 102 | let request = try communicator.request(from: text, url: url) 103 | log(request) 104 | handle(request) 105 | } catch { 106 | LogService.shared.error( 107 | "WC: incomming text deserialization to JSONRPC 2.0 requests error: \(error.localizedDescription)" 108 | ) 109 | // TODO: handle error 110 | try! send(Response(url: url, error: .invalidJSON)) 111 | } 112 | } 113 | 114 | override func onConnect(to url: WCURL) { 115 | LogService.shared.info("WC: didConnect url: \(url.bridgeURL.absoluteString)") 116 | if let session = communicator.session(by: url) { // reconnecting existing session 117 | communicator.subscribe(on: session.walletInfo!.peerId, url: session.url) 118 | delegate?.server(self, didConnect: session) 119 | } else { // establishing new connection, handshake in process 120 | communicator.subscribe(on: url.topic, url: url) 121 | } 122 | } 123 | 124 | private func handle(_ request: Request) { 125 | if let handler = handlers.find(by: request) { 126 | handler.handle(request: request) 127 | } else { 128 | // TODO: error handling 129 | let response = try! Response(request: request, error: .methodNotFound) 130 | send(response) 131 | } 132 | } 133 | 134 | override func sendDisconnectSessionRequest(for session: Session) throws { 135 | guard let walletInfo = session.walletInfo else { 136 | throw ServerError.missingWalletInfoInSession 137 | } 138 | try updateSession(session, with: walletInfo.with(approved: false)) 139 | } 140 | 141 | override func failedToConnect(_ url: WCURL) { 142 | delegate?.server(self, didFailToConnect: url) 143 | } 144 | 145 | override func didDisconnect(_ session: Session) { 146 | delegate?.server(self, didDisconnect: session) 147 | } 148 | 149 | override func willReconnect(_ session: Session) { 150 | if let delegate = delegate as? ServerDelegateV2 { 151 | delegate.server(self, willReconnect: session) 152 | } 153 | } 154 | 155 | /// Sends response for the create session request. 156 | /// Use this method together with `ServerDelegate.server(_ server:didReceiveConnectionRequest:for:)`. 157 | /// 158 | /// - Parameters: 159 | /// - requestId: pass the request id that was received before 160 | /// - session: session with dapp info populated. 161 | /// - walletInfo: the response from the wallet. 162 | /// If approved, you need to create peerId with UUID().uuidString and put it inside the wallet info. 163 | public func sendCreateSessionResponse(for requestId: RequestID, session: Session, walletInfo: Session.WalletInfo) { 164 | let response: Response 165 | do { 166 | response = try Response(url: session.url, value: walletInfo, id: requestId) 167 | } catch { 168 | LogService.shared.error("WC: failed to compose SessionRequest response: \(error)") 169 | delegate?.server(self, didFailToConnect: session.url) 170 | return 171 | } 172 | communicator.send(response, topic: session.dAppInfo.peerId) 173 | if walletInfo.approved { 174 | let updatedSession = Session(url: session.url, dAppInfo: session.dAppInfo, walletInfo: walletInfo) 175 | communicator.addOrUpdateSession(updatedSession) 176 | communicator.subscribe(on: walletInfo.peerId, url: updatedSession.url) 177 | delegate?.server(self, didConnect: updatedSession) 178 | } 179 | } 180 | 181 | /// thread-safe collection of RequestHandlers 182 | private class Handlers { 183 | private var handlers: [RequestHandler] = [] 184 | private var queue: DispatchQueue 185 | 186 | init(queue: DispatchQueue) { 187 | self.queue = queue 188 | } 189 | 190 | func add(_ handler: RequestHandler) { 191 | dispatchPrecondition(condition: .notOnQueue(queue)) 192 | queue.sync { [weak self] in 193 | guard let `self` = self else { return } 194 | guard self.handlers.first(where: { $0 === handler }) == nil else { return } 195 | self.handlers.append(handler) 196 | } 197 | } 198 | 199 | func remove(_ handler: RequestHandler) { 200 | dispatchPrecondition(condition: .notOnQueue(queue)) 201 | queue.sync { [weak self] in 202 | guard let `self` = self else { return } 203 | if let index = self.handlers.firstIndex(where: { $0 === handler }) { 204 | self.handlers.remove(at: index) 205 | } 206 | } 207 | } 208 | 209 | func find(by request: Request) -> RequestHandler? { 210 | var result: RequestHandler? 211 | dispatchPrecondition(condition: .notOnQueue(queue)) 212 | queue.sync { [weak self] in 213 | guard let `self` = self else { return } 214 | result = self.handlers.first { $0.canHandle(request: request) } 215 | } 216 | return result 217 | } 218 | } 219 | } 220 | 221 | extension Server: HandshakeHandlerDelegate { 222 | func handler(_ handler: HandshakeHandler, 223 | didReceiveRequestToCreateSession session: Session, 224 | requestId: RequestID) { 225 | guard let delegate = delegate else { return } 226 | if let delegateV2 = delegate as? ServerDelegateV2 { 227 | delegateV2.server(self, didReceiveConnectionRequest: requestId, for: session) 228 | } else { 229 | delegate.server(self, shouldStart: session) { [weak self] walletInfo in 230 | guard let `self` = self else { 231 | return 232 | } 233 | self.sendCreateSessionResponse(for: requestId, session: session, walletInfo: walletInfo) 234 | } 235 | } 236 | } 237 | } 238 | 239 | extension Server: UpdateSessionHandlerDelegate { 240 | func handler(_ handler: UpdateSessionHandler, didUpdateSessionByURL url: WCURL, sessionInfo: SessionInfo) { 241 | guard let session = communicator.session(by: url) else { return } 242 | if !sessionInfo.approved { 243 | self.communicator.addOrUpdatePendingDisconnectSession(session) 244 | self.communicator.disconnect(from: session.url) 245 | self.delegate?.server(self, didDisconnect: session) 246 | } else { 247 | // we do not add sessions without walletInfo 248 | let walletInfo = session.walletInfo! 249 | let updatedInfo = Session.WalletInfo( 250 | approved: sessionInfo.approved, 251 | accounts: sessionInfo.accounts ?? [], 252 | chainId: sessionInfo.chainId ?? ChainID.mainnet, 253 | peerId: walletInfo.peerId, 254 | peerMeta: walletInfo.peerMeta 255 | ) 256 | var updatedSesson = session 257 | updatedSesson.walletInfo = updatedInfo 258 | communicator.addOrUpdateSession(updatedSesson) 259 | delegate?.server(self, didUpdate: updatedSesson) 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Sources/Internal/WebSocketConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Network 7 | 8 | #if os(iOS) 9 | import UIKit 10 | #endif 11 | 12 | enum WebSocketError: Error { 13 | case closedUnexpectedly 14 | case peerDisconnected 15 | } 16 | 17 | 18 | class WebSocketConnection { 19 | let url: WCURL 20 | private var isConnected: Bool = false 21 | private var task: URLSessionWebSocketTask? 22 | private lazy var session: URLSession = { 23 | let delegate = WebSocketConnectionDelegate(eventHandler: { [weak self] event in 24 | self?.handleEvent(event) 25 | }) 26 | let configuration = URLSessionConfiguration.default 27 | configuration.shouldUseExtendedBackgroundIdleMode = true 28 | configuration.waitsForConnectivity = true 29 | 30 | return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) 31 | }() 32 | 33 | #if os(iOS) 34 | private var bgTaskIdentifier: UIBackgroundTaskIdentifier = .invalid 35 | private var foregroundNotificationObserver: Any? 36 | private var backgroundNotificationObserver: Any? 37 | #endif 38 | 39 | private let onConnect: (() -> Void)? 40 | private let onDisconnect: ((Error?) -> Void)? 41 | private let onTextReceive: ((String) -> Void)? 42 | 43 | // needed to keep connection alive 44 | private var pingTimer: Timer? 45 | 46 | // TODO: make injectable on server creation 47 | private let pingInterval: TimeInterval = 30 48 | private let timeoutInterval: TimeInterval = 20 49 | 50 | private var requestSerializer: RequestSerializer = JSONRPCSerializer() 51 | private var responseSerializer: ResponseSerializer = JSONRPCSerializer() 52 | 53 | var isOpen: Bool { 54 | return isConnected 55 | } 56 | 57 | init(url: WCURL, 58 | onConnect: (() -> Void)?, 59 | onDisconnect: ((Error?) -> Void)?, 60 | onTextReceive: ((String) -> Void)? 61 | ) { 62 | self.url = url 63 | self.onConnect = onConnect 64 | self.onDisconnect = onDisconnect 65 | self.onTextReceive = onTextReceive 66 | 67 | #if os(iOS) 68 | // On actual iOS devices, request some additional background execution time to the OS 69 | // each time that the app moves to background. This allows us to continue running for 70 | // around 30 secs in the background instead of having the socket killed instantly, which 71 | // solves the issue of connecting a wallet and a dApp both on the same device. 72 | // See https://github.com/WalletConnect/WalletConnectSwift/pull/81#issuecomment-1175931673 73 | 74 | if #available(iOS 14.0, *) { 75 | // We don't really need this on Apple Silicon Macs 76 | guard !ProcessInfo.processInfo.isiOSAppOnMac else { return } 77 | } 78 | 79 | self.backgroundNotificationObserver = NotificationCenter.default.addObserver( 80 | forName: UIScene.didEnterBackgroundNotification, 81 | object: nil, 82 | queue: OperationQueue.main 83 | ) { [weak self] _ in 84 | self?.requestBackgroundExecutionTime() 85 | } 86 | 87 | self.foregroundNotificationObserver = NotificationCenter.default.addObserver( 88 | forName: UIScene.didActivateNotification, 89 | object: nil, 90 | queue: OperationQueue.main 91 | ) { [weak self] _ in 92 | self?.endBackgroundExecutionTime() 93 | } 94 | #endif 95 | } 96 | 97 | deinit { 98 | session.invalidateAndCancel() 99 | pingTimer?.invalidate() 100 | 101 | #if os(iOS) 102 | if let observer = self.foregroundNotificationObserver { 103 | NotificationCenter.default.removeObserver(observer) 104 | } 105 | if let observer = self.backgroundNotificationObserver { 106 | NotificationCenter.default.removeObserver(observer) 107 | } 108 | #endif 109 | } 110 | 111 | func open() { 112 | if task != nil { 113 | close() 114 | } 115 | 116 | let request = URLRequest(url: url.bridgeURL, timeoutInterval: timeoutInterval) 117 | task = session.webSocketTask(with: request) 118 | task?.resume() 119 | receive() 120 | } 121 | 122 | func close(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure) { 123 | pingTimer?.invalidate() 124 | task?.cancel(with: closeCode, reason: nil) 125 | task = nil 126 | } 127 | 128 | func send(_ text: String) { 129 | guard isConnected else { return } 130 | task?.send(.string(text)) { [weak self] error in 131 | if let error = error { 132 | self?.handleEvent(.connnectionError(error)) 133 | } else { 134 | self?.handleEvent(.messageSent(text)) 135 | } 136 | } 137 | } 138 | } 139 | 140 | private extension WebSocketConnection { 141 | 142 | enum WebSocketEvent { 143 | case connected 144 | case disconnected(URLSessionWebSocketTask.CloseCode) 145 | case messageReceived(String) 146 | case messageSent(String) 147 | case pingSent 148 | case pongReceived 149 | case connnectionError(Error) 150 | } 151 | 152 | func receive() { 153 | guard let task = task else { return } 154 | task.receive { [weak self] result in 155 | switch result { 156 | case .success(let message): 157 | if case let .string(text) = message { 158 | self?.handleEvent(.messageReceived(text)) 159 | } 160 | self?.receive() 161 | case .failure(let error): 162 | self?.handleEvent(.connnectionError(error)) 163 | } 164 | } 165 | } 166 | 167 | func sendPing() { 168 | guard isConnected else { return } 169 | task?.sendPing(pongReceiveHandler: { [weak self] error in 170 | if let error = error { 171 | self?.handleEvent(.connnectionError(error)) 172 | } else { 173 | self?.handleEvent(.pongReceived) 174 | } 175 | }) 176 | handleEvent(.pingSent) 177 | } 178 | 179 | func handleEvent(_ event: WebSocketEvent) { 180 | switch event { 181 | case .connected: 182 | isConnected = true 183 | DispatchQueue.main.async { 184 | self.pingTimer = Timer.scheduledTimer( 185 | withTimeInterval: self.pingInterval, 186 | repeats: true 187 | ) { [weak self] _ in 188 | self?.sendPing() 189 | } 190 | } 191 | LogService.shared.info("WC: connected") 192 | onConnect?() 193 | case .disconnected(let closeCode): 194 | guard isConnected else { break } 195 | isConnected = false 196 | pingTimer?.invalidate() 197 | 198 | var error: Error? = nil 199 | switch closeCode { 200 | case .normalClosure: 201 | LogService.shared.info("WC: disconnected (normal closure)") 202 | case .abnormalClosure, .goingAway: 203 | LogService.shared.error("WC: disconnected (peer disconnected)") 204 | error = WebSocketError.peerDisconnected 205 | default: 206 | LogService.shared.error("WC: disconnected (\(closeCode)") 207 | error = WebSocketError.closedUnexpectedly 208 | } 209 | onDisconnect?(error) 210 | case .messageReceived(let text): 211 | onTextReceive?(text) 212 | case .messageSent(let text): 213 | if let request = try? requestSerializer.deserialize(text, url: url).json().string { 214 | LogService.shared.detailed("WC: ==> [request] \(request)") 215 | } else if let response = try? responseSerializer.deserialize(text, url: url).json().string { 216 | if response.contains("\"error\"") { 217 | LogService.shared.error("WC: ==> [response] \(response)") 218 | } else { 219 | LogService.shared.detailed("WC: ==> [response] \(response)") 220 | } 221 | } else { 222 | LogService.shared.detailed("WC: ==> \(text)") 223 | } 224 | case .pingSent: 225 | LogService.shared.verbose("WC: ==> ping") 226 | case .pongReceived: 227 | LogService.shared.verbose("WC: <== pong") 228 | case .connnectionError(let error): 229 | LogService.shared.error("WC: Connection error: \(error.localizedDescription)") 230 | onDisconnect?(error) 231 | } 232 | } 233 | 234 | class WebSocketConnectionDelegate: NSObject, URLSessionWebSocketDelegate, URLSessionTaskDelegate { 235 | private let eventHandler: (WebSocketEvent) -> Void 236 | private var connectivityCheckTimer: Timer? 237 | 238 | init(eventHandler: @escaping (WebSocketEvent) -> Void) { 239 | self.eventHandler = eventHandler 240 | } 241 | 242 | func urlSession( 243 | _ session: URLSession, 244 | webSocketTask: URLSessionWebSocketTask, 245 | didOpenWithProtocol protocol: String? 246 | ) { 247 | self.connectivityCheckTimer?.invalidate() 248 | eventHandler(.connected) 249 | } 250 | 251 | func urlSession( 252 | _ session: URLSession, 253 | webSocketTask: URLSessionWebSocketTask, 254 | didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, 255 | reason: Data? 256 | ) { 257 | eventHandler(.disconnected(closeCode)) 258 | } 259 | 260 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 261 | if let error = error { 262 | eventHandler(.connnectionError(error)) 263 | } else { 264 | // Possibly not really necessary since connection closure would likely have been reported 265 | // by the other delegate method, but just to be safe. We have checks in place to prevent 266 | // duplicated connection closing reporting anyway. 267 | eventHandler(.disconnected(.normalClosure)) 268 | } 269 | } 270 | 271 | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { 272 | LogService.shared.info("WC: waiting for connectivity...") 273 | 274 | // Lets not wait forever, since the user might be waiting for the connection to show in the UI. 275 | // It's better to show an error -if it's a new session- or else let the retry logic do its job 276 | DispatchQueue.main.async { 277 | self.connectivityCheckTimer?.invalidate() 278 | self.connectivityCheckTimer = Timer.scheduledTimer( 279 | withTimeInterval: task.originalRequest?.timeoutInterval ?? 30, 280 | repeats: false 281 | ) { _ in 282 | // Cancelling the task should trigger an invocation to `didCompleteWithError` 283 | task.cancel() 284 | } 285 | } 286 | } 287 | } 288 | } 289 | 290 | #if os(iOS) 291 | private extension WebSocketConnection { 292 | 293 | func requestBackgroundExecutionTime() { 294 | if bgTaskIdentifier != .invalid { 295 | endBackgroundExecutionTime() 296 | } 297 | 298 | bgTaskIdentifier = UIApplication.shared.beginBackgroundTask( 299 | withName: "WebSocketConnection-bgTime" 300 | ) { [weak self] in 301 | self?.endBackgroundExecutionTime() 302 | } 303 | } 304 | 305 | func endBackgroundExecutionTime() { 306 | guard bgTaskIdentifier != .invalid else { return } 307 | UIApplication.shared.endBackgroundTask(bgTaskIdentifier) 308 | bgTaskIdentifier = .invalid 309 | } 310 | } 311 | #endif 312 | -------------------------------------------------------------------------------- /Sources/Internal/JSONRPC_2_0.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | enum JSONRPC_2_0 { 8 | struct JSON: Equatable, ExpressibleByStringInterpolation { 9 | var string: String 10 | 11 | init(_ text: String) { 12 | string = text 13 | } 14 | 15 | init(stringLiteral value: String) { 16 | self.init(value) 17 | } 18 | } 19 | 20 | enum IDType: Hashable, Codable { 21 | case string(String) 22 | case int(Int) 23 | case double(Double) 24 | case null 25 | 26 | init(from decoder: Decoder) throws { 27 | let container = try decoder.singleValueContainer() 28 | if let string = try? container.decode(String.self) { 29 | self = .string(string) 30 | } else if let int = try? container.decode(Int.self) { 31 | self = .int(int) 32 | } else if let double = try? container.decode(Double.self) { 33 | self = .double(double) 34 | } else if container.decodeNil() { 35 | self = .null 36 | } else { 37 | let context = DecodingError.Context(codingPath: decoder.codingPath, 38 | debugDescription: "Value is not a String, Number or Null") 39 | throw DecodingError.typeMismatch(IDType.self, context) 40 | } 41 | } 42 | 43 | func encode(to encoder: Encoder) throws { 44 | var container = encoder.singleValueContainer() 45 | switch self { 46 | case .string(let value): 47 | try container.encode(value) 48 | case .int(let value): 49 | try container.encode(value) 50 | case .double(let value): 51 | try container.encode(value) 52 | case .null: 53 | try container.encodeNil() 54 | } 55 | } 56 | } 57 | 58 | enum ValueType: Hashable, Codable { 59 | case object([String: ValueType]) 60 | case array([ValueType]) 61 | case string(String) 62 | case int(Int) 63 | case double(Double) 64 | case bool(Bool) 65 | case null 66 | 67 | init(from decoder: Decoder) throws { 68 | if let keyedContainer = try? decoder.container(keyedBy: KeyType.self) { 69 | var result = [String: ValueType]() 70 | for key in keyedContainer.allKeys { 71 | result[key.stringValue] = try keyedContainer.decode(ValueType.self, forKey: key) 72 | } 73 | self = .object(result) 74 | } else if var unkeyedContainer = try? decoder.unkeyedContainer() { 75 | var result = [ValueType]() 76 | while !unkeyedContainer.isAtEnd { 77 | let value = try unkeyedContainer.decode(ValueType.self) 78 | result.append(value) 79 | } 80 | self = .array(result) 81 | } else if let singleContainer = try? decoder.singleValueContainer() { 82 | if let string = try? singleContainer.decode(String.self) { 83 | self = .string(string) 84 | } else if let int = try? singleContainer.decode(Int.self) { 85 | self = .int(int) 86 | } else if let double = try? singleContainer.decode(Double.self) { 87 | self = .double(double) 88 | } else if let bool = try? singleContainer.decode(Bool.self) { 89 | self = .bool(bool) 90 | } else if singleContainer.decodeNil() { 91 | self = .null 92 | } else { 93 | let context = DecodingError.Context(codingPath: decoder.codingPath, 94 | debugDescription: "Value is not a String, Number, Bool or Null") 95 | throw DecodingError.typeMismatch(ValueType.self, context) 96 | } 97 | } else { 98 | let context = DecodingError.Context(codingPath: decoder.codingPath, 99 | debugDescription: "Did not match any container") 100 | throw DecodingError.typeMismatch(ValueType.self, context) 101 | } 102 | } 103 | 104 | func encode(to encoder: Encoder) throws { 105 | switch self { 106 | case .object(let object): 107 | var container = encoder.container(keyedBy: KeyType.self) 108 | for (key, value) in object { 109 | try container.encode(value, forKey: KeyType(stringValue: key)!) 110 | } 111 | case .array(let array): 112 | var container = encoder.unkeyedContainer() 113 | for value in array { 114 | try container.encode(value) 115 | } 116 | case .string(let value): 117 | var container = encoder.singleValueContainer() 118 | try container.encode(value) 119 | case .int(let value): 120 | var container = encoder.singleValueContainer() 121 | try container.encode(value) 122 | case .double(let value): 123 | var container = encoder.singleValueContainer() 124 | try container.encode(value) 125 | case .bool(let value): 126 | var container = encoder.singleValueContainer() 127 | try container.encode(value) 128 | case .null: 129 | var container = encoder.singleValueContainer() 130 | try container.encodeNil() 131 | } 132 | } 133 | 134 | func jsonString() throws -> String { 135 | switch self { 136 | case .int, .double, .string, .bool, .null: 137 | // we have to wrap primitives into array because otherwise it is not a valid json 138 | let data = try JSONEncoder.encoder().encode([self]) 139 | guard let string = String(data: data, encoding: .utf8) else { 140 | throw DataConversionError.dataToStringFailed 141 | } 142 | assert(string.hasPrefix("[")) 143 | assert(string.hasSuffix("]")) 144 | // now strip the json string of the wrapping array symbols '[' ']' 145 | return String(string.dropFirst(1).dropLast(1)) 146 | case .object, .array: 147 | let data = try JSONEncoder.encoder().encode(self) 148 | guard let string = String(data: data, encoding: .utf8) else { 149 | throw DataConversionError.dataToStringFailed 150 | } 151 | return string 152 | } 153 | } 154 | } 155 | 156 | struct KeyType: CodingKey { 157 | var stringValue: String 158 | 159 | init?(stringValue: String) { 160 | self.stringValue = stringValue 161 | self.intValue = Int(stringValue) 162 | } 163 | 164 | var intValue: Int? 165 | 166 | init?(intValue: Int) { 167 | self.intValue = intValue 168 | self.stringValue = String(describing: intValue) 169 | } 170 | } 171 | 172 | /// https://www.jsonrpc.org/specification#request_object 173 | struct Request: Hashable, Codable { 174 | var jsonrpc = "2.0" 175 | var method: String 176 | var params: Params? 177 | var id: IDType? 178 | 179 | init(method: String, params: Params?, id: IDType?) { 180 | self.method = method 181 | self.params = params 182 | self.id = id 183 | } 184 | 185 | enum Params: Hashable, Codable { 186 | 187 | case positional([ValueType]) 188 | case named([String: ValueType]) 189 | 190 | init(from decoder: Decoder) throws { 191 | if let keyedContainer = try? decoder.container(keyedBy: KeyType.self) { 192 | var result = [String: ValueType]() 193 | for key in keyedContainer.allKeys { 194 | result[key.stringValue] = try keyedContainer.decode(ValueType.self, forKey: key) 195 | } 196 | self = .named(result) 197 | } else if var unkeyedContainer = try? decoder.unkeyedContainer() { 198 | var result = [ValueType]() 199 | while !unkeyedContainer.isAtEnd { 200 | let value = try unkeyedContainer.decode(ValueType.self) 201 | result.append(value) 202 | } 203 | self = .positional(result) 204 | } else { 205 | let context = DecodingError.Context(codingPath: decoder.codingPath, 206 | debugDescription: "Did not match any container") 207 | throw DecodingError.typeMismatch(Params.self, context) 208 | } 209 | } 210 | 211 | func encode(to encoder: Encoder) throws { 212 | switch self { 213 | case .named(let object): 214 | var container = encoder.container(keyedBy: KeyType.self) 215 | for (key, value) in object { 216 | try container.encode(value, forKey: KeyType(stringValue: key)!) 217 | } 218 | case .positional(let array): 219 | var container = encoder.unkeyedContainer() 220 | for value in array { 221 | try container.encode(value) 222 | } 223 | } 224 | } 225 | } 226 | 227 | static func create(from json: JSONRPC_2_0.JSON) throws -> JSONRPC_2_0.Request { 228 | guard let data = json.string.data(using: .utf8) else { 229 | throw DataConversionError.stringToDataFailed 230 | } 231 | return try JSONDecoder().decode(JSONRPC_2_0.Request.self, from: data) 232 | } 233 | 234 | func json() throws -> JSONRPC_2_0.JSON { 235 | let data = try JSONEncoder.encoder().encode(self) 236 | guard let string = String(data: data, encoding: .utf8) else { 237 | throw DataConversionError.dataToStringFailed 238 | } 239 | return JSONRPC_2_0.JSON(string) 240 | } 241 | } 242 | 243 | /// https://www.jsonrpc.org/specification#response_object 244 | struct Response: Hashable, Codable { 245 | var jsonrpc = "2.0" 246 | var result: Payload 247 | var id: IDType 248 | 249 | init(result: Payload, id: IDType) { 250 | self.result = result 251 | self.id = id 252 | } 253 | 254 | enum Payload: Hashable, Codable { 255 | case value(ValueType) 256 | case error(ErrorPayload) 257 | 258 | init(from decoder: Decoder) throws { 259 | let container = try decoder.singleValueContainer() 260 | if let error = try? container.decode(ErrorPayload.self) { 261 | self = .error(error) 262 | } else if let value = try? container.decode(ValueType.self) { 263 | self = .value(value) 264 | } else if container.decodeNil() { 265 | self = .value(.null) 266 | } else { 267 | let context = DecodingError.Context(codingPath: decoder.codingPath, 268 | debugDescription: "Payload is neither error, nor JSON value") 269 | throw DecodingError.typeMismatch(ValueType.self, context) 270 | } 271 | } 272 | 273 | func encode(to encoder: Encoder) throws { 274 | var container = encoder.singleValueContainer() 275 | switch self { 276 | case .value(let value): 277 | try container.encode(value) 278 | case .error(let value): 279 | try container.encode(value) 280 | } 281 | } 282 | 283 | /// https://www.jsonrpc.org/specification#error_object 284 | struct ErrorPayload: Hashable, Codable { 285 | 286 | var code: Code 287 | var message: String 288 | var data: ValueType? 289 | 290 | init(code: Code, message: String, data: ValueType?) { 291 | self.code = code 292 | self.message = message 293 | self.data = data 294 | } 295 | 296 | struct Code: Hashable, Codable { 297 | 298 | var code: Int 299 | 300 | enum InitializationError: String, Error { 301 | case codeAlreadyReservedForPredefinedErrors 302 | } 303 | 304 | static let invalidJSON = Code(code: -32_700) 305 | static let invalidRequest = Code(code: -32_600) 306 | static let methodNotFound = Code(code: -32_601) 307 | static let invalidParams = Code(code: -32_602) 308 | static let internalError = Code(code: -32_603) 309 | 310 | init(_ code: Int) throws { 311 | let forbiddenPredefinedRange = (-32768 ... -32000) 312 | let allowedServerErrorsRange = (-32099 ... -32000) 313 | let allowedPredefinedErrors = [-32700, -32600, -32601, -32602, -32603] 314 | 315 | if forbiddenPredefinedRange.contains(code) && 316 | !(allowedServerErrorsRange.contains(code) || allowedPredefinedErrors.contains(code)) { 317 | throw InitializationError.codeAlreadyReservedForPredefinedErrors 318 | } 319 | self.init(code: code) 320 | } 321 | 322 | init(code: Int) { 323 | self.code = code 324 | } 325 | 326 | init(from decoder: Decoder) throws { 327 | let container = try decoder.singleValueContainer() 328 | code = try container.decode(Int.self) 329 | } 330 | 331 | func encode(to encoder: Encoder) throws { 332 | var container = encoder.singleValueContainer() 333 | try container.encode(code) 334 | } 335 | } 336 | 337 | } 338 | } 339 | 340 | static func create(from json: JSONRPC_2_0.JSON) throws -> JSONRPC_2_0.Response { 341 | guard let data = json.string.data(using: .utf8) else { 342 | throw DataConversionError.stringToDataFailed 343 | } 344 | let decoder = JSONDecoder() 345 | decoder.keyDecodingStrategy = .custom { codingKeys in 346 | let lastKey = codingKeys.last! 347 | guard lastKey.intValue == nil else { return lastKey } 348 | let stringValue = lastKey.stringValue == "error" ? "result" : lastKey.stringValue 349 | return JSONRPC_2_0.KeyType(stringValue: stringValue)! 350 | } 351 | return try decoder.decode(JSONRPC_2_0.Response.self, from: data) 352 | } 353 | 354 | func json() throws -> JSONRPC_2_0.JSON { 355 | let encoder = JSONEncoder.encoder() 356 | if case Payload.error(_) = result { 357 | encoder.keyEncodingStrategy = .custom { codingKeys in 358 | let lastKey = codingKeys.last! 359 | guard lastKey.intValue == nil else { return lastKey } 360 | let stringValue = lastKey.stringValue == "result" ? "error" : lastKey.stringValue 361 | return JSONRPC_2_0.KeyType(stringValue: stringValue)! 362 | } 363 | } 364 | let data = try encoder.encode(self) 365 | guard let string = String(data: data, encoding: .utf8) else { 366 | throw DataConversionError.dataToStringFailed 367 | } 368 | return JSONRPC_2_0.JSON(string) 369 | } 370 | } 371 | } 372 | 373 | extension JSONEncoder { 374 | static func encoder() -> JSONEncoder { 375 | let encoder = JSONEncoder() 376 | encoder.outputFormatting = [.sortedKeys] 377 | return encoder 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Sources/PublicInterface/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2019 Gnosis Ltd. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol ClientDelegate: AnyObject { 8 | func client(_ client: Client, didFailToConnect url: WCURL) 9 | func client(_ client: Client, didConnect url: WCURL) 10 | func client(_ client: Client, didConnect session: Session) 11 | func client(_ client: Client, didDisconnect session: Session) 12 | func client(_ client: Client, didUpdate session: Session) 13 | } 14 | 15 | public protocol ClientDelegateV2: ClientDelegate { 16 | func client(_ client: Client, dappInfoForUrl url: WCURL) -> Session.DAppInfo? 17 | func client(_ client: Client, willReconnect session: Session) 18 | } 19 | 20 | public class Client: WalletConnect { 21 | public typealias RequestResponse = (Response) -> Void 22 | 23 | private(set) weak var delegate: ClientDelegate? 24 | private var commonDappInfo: Session.DAppInfo? 25 | private var responses: Responses 26 | 27 | public enum ClientError: Error { 28 | case missingWalletInfoInSession 29 | case sessionNotFound 30 | } 31 | 32 | public init(delegate: ClientDelegate, dAppInfo: Session.DAppInfo? = nil) { 33 | self.delegate = delegate 34 | self.commonDappInfo = dAppInfo 35 | responses = Responses(queue: DispatchQueue(label: "org.walletconnect.swift.client.pending")) 36 | super.init() 37 | } 38 | 39 | /// Send request to wallet. 40 | /// 41 | /// - Parameters: 42 | /// - request: Request object. 43 | /// - completion: RequestResponse completion. 44 | /// - Throws: Client error. 45 | public func send(_ request: Request, completion: RequestResponse?) throws { 46 | guard let session = communicator.session(by: request.url) else { 47 | throw ClientError.sessionNotFound 48 | } 49 | guard let walletInfo = session.walletInfo else { 50 | throw ClientError.missingWalletInfoInSession 51 | } 52 | if let completion = completion, let requestID = request.internalID, requestID != .null { 53 | responses.add(requestID: requestID, response: completion) 54 | } 55 | communicator.send(request, topic: walletInfo.peerId) 56 | } 57 | 58 | /// Send response to wallet. 59 | /// 60 | /// - Parameter response: Response object. 61 | /// - Throws: Client error. 62 | public func send(_ response: Response) throws { 63 | guard let session = communicator.session(by: response.url) else { 64 | throw ClientError.sessionNotFound 65 | } 66 | guard let walletInfo = session.walletInfo else { 67 | throw ClientError.missingWalletInfoInSession 68 | } 69 | communicator.send(response, topic: walletInfo.peerId) 70 | } 71 | 72 | /// Request to sign a message. 73 | /// 74 | /// https://docs.walletconnect.org/json-rpc/ethereum#personal_sign 75 | /// 76 | /// - Parameters: 77 | /// - url: WalletConnect url object. 78 | /// - message: String representing human readable message to sign. 79 | /// - account: String representing Ethereum address. 80 | /// - completion: Response with string representing signature, or error. 81 | /// - Throws: client error. 82 | public func personal_sign(url: WCURL, 83 | message: String, 84 | account: String, 85 | completion: @escaping RequestResponse) throws { 86 | let messageHex = "0x" + message.data(using: .utf8)!.map { String(format: "%02x", $0) }.joined() 87 | try sign(url: url, method: "personal_sign", param1: messageHex, param2: account, completion: completion) 88 | } 89 | 90 | /// Request to sign a message. 91 | /// 92 | /// https://docs.walletconnect.org/json-rpc/ethereum#eth_sign 93 | /// 94 | /// - Parameters: 95 | /// - url: WalletConnect url object. 96 | /// - account: String representing Ethereum address. 97 | /// - message: String representin Data to sign. 98 | /// - completion: Response with string representing signature, or error. 99 | /// - Throws: client error. 100 | public func eth_sign(url: WCURL, 101 | account: String, 102 | message: String, 103 | completion: @escaping RequestResponse) throws { 104 | try sign(url: url, method: "eth_sign", param1: account, param2: message, completion: completion) 105 | } 106 | 107 | /// Request to sign typed daya. 108 | /// 109 | /// https://docs.walletconnect.org/json-rpc/ethereum#eth_signtypeddata 110 | /// 111 | /// - Parameters: 112 | /// - url: WalletConnect url object. 113 | /// - account: String representing Ethereum address. 114 | /// - message: String representin Data to sign. 115 | /// - completion: Response with string representing signature, or error. 116 | /// - Throws: client error. 117 | public func eth_signTypedData(url: WCURL, 118 | account: String, 119 | message: String, 120 | completion: @escaping RequestResponse) throws { 121 | try sign(url: url, method: "eth_signTypedData", param1: account, param2: message, completion: completion) 122 | } 123 | 124 | private func sign(url: WCURL, 125 | method: String, 126 | param1: String, 127 | param2: String, 128 | completion: @escaping RequestResponse) throws { 129 | let request = try Request(url: url, method: method, params: [param1, param2]) 130 | try send(request, completion: completion) 131 | } 132 | 133 | /// Request to send a transaction. 134 | /// 135 | /// https://docs.walletconnect.org/json-rpc/ethereum#eth_sendtransaction 136 | /// 137 | /// - Parameters: 138 | /// - url: WalletConnect url object. 139 | /// - transaction: Transaction object. 140 | /// - completion: Response with string representing transaction hash, or error. 141 | /// - Throws: client error. 142 | public func eth_sendTransaction(url: WCURL, 143 | transaction: Transaction, 144 | completion: @escaping RequestResponse) throws { 145 | try handleTransaction(url: url, method: "eth_sendTransaction", transaction: transaction, completion: completion) 146 | } 147 | 148 | /// Request to sign a transaction. 149 | /// 150 | /// https://docs.walletconnect.org/json-rpc/ethereum#eth_signtransaction 151 | /// 152 | /// - Parameters: 153 | /// - url: WalletConnect url object. 154 | /// - transaction: Transaction object. 155 | /// - completion: Response with string representing transaction signature, or error. 156 | /// - Throws: client error. 157 | public func eth_signTransaction(url: WCURL, 158 | transaction: Transaction, 159 | completion: @escaping RequestResponse) throws { 160 | try handleTransaction(url: url, method: "eth_signTransaction", transaction: transaction, completion: completion) 161 | } 162 | 163 | private func handleTransaction(url: WCURL, 164 | method: String, 165 | transaction: Transaction, 166 | completion: @escaping RequestResponse) throws { 167 | let request = try Request(url: url, method: method, params: [transaction]) 168 | try send(request, completion: completion) 169 | } 170 | 171 | /// Request to send a raw transaction. Creates new message call transaction or 172 | /// a contract creation for signed transactions. 173 | /// 174 | /// https://docs.walletconnect.org/json-rpc/ethereum#eth_sendrawtransaction 175 | /// 176 | /// - Parameters: 177 | /// - url: WalletConnect url object. 178 | /// - data: Data as String. 179 | /// - completion: Response with the transaction hash, or the zero hash if the transaction is not 180 | /// yet available, or error. 181 | /// - Throws: client error. 182 | public func eth_sendRawTransaction(url: WCURL, data: String, completion: @escaping RequestResponse) throws { 183 | let request = try Request(url: url, method: "eth_sendRawTransaction", params: [data]) 184 | try send(request, completion: completion) 185 | } 186 | 187 | override func onConnect(to url: WCURL) { 188 | LogService.shared.info("WC: client didConnect url: \(url.bridgeURL.absoluteString)") 189 | delegate?.client(self, didConnect: url) 190 | if let existingSession = communicator.session(by: url) { 191 | communicator.subscribe(on: existingSession.dAppInfo.peerId, url: existingSession.url) 192 | delegate?.client(self, didConnect: existingSession) 193 | } else { 194 | // establishing new connection, handshake in process 195 | guard let dappInfo = commonDappInfo ?? (delegate as? ClientDelegateV2)?.client(self, dappInfoForUrl: url) else { 196 | LogService.shared.error("WC: dAppInfo not found for \(url)") 197 | delegate?.client(self, didFailToConnect: url) 198 | return 199 | } 200 | 201 | communicator.subscribe(on: dappInfo.peerId, url: url) 202 | let request = try! Request(url: url, method: "wc_sessionRequest", params: [dappInfo], id: Request.payloadId()) 203 | let requestID = request.internalID! 204 | responses.add(requestID: requestID) { [unowned self] response in 205 | self.handleHandshakeResponse(response) 206 | } 207 | communicator.send(request, topic: url.topic) 208 | } 209 | } 210 | 211 | private func handleHandshakeResponse(_ response: Response) { 212 | do { 213 | let walletInfo = try response.result(as: Session.WalletInfo.self) 214 | 215 | guard let dappInfo = commonDappInfo ?? (delegate as? ClientDelegateV2)?.client(self, dappInfoForUrl: response.url) else { 216 | LogService.shared.error("WC: dAppInfo not found for \(response.url)") 217 | return 218 | } 219 | 220 | let session = Session(url: response.url, dAppInfo: dappInfo, walletInfo: walletInfo) 221 | 222 | guard walletInfo.approved else { 223 | // TODO: handle Error 224 | delegate?.client(self, didFailToConnect: response.url) 225 | return 226 | } 227 | 228 | communicator.addOrUpdateSession(session) 229 | delegate?.client(self, didConnect: session) 230 | } catch { 231 | // TODO: handle error 232 | delegate?.client(self, didFailToConnect: response.url) 233 | } 234 | } 235 | 236 | override func onTextReceive(_ text: String, from url: WCURL) { 237 | if let response = try? communicator.response(from: text, url: url) { 238 | log(response) 239 | if let completion = responses.find(requestID: response.internalID) { 240 | completion(response) 241 | responses.remove(requestID: response.internalID) 242 | } 243 | } else if let request = try? communicator.request(from: text, url: url) { 244 | log(request) 245 | expectUpdateSessionRequest(request) 246 | } 247 | } 248 | 249 | private func expectUpdateSessionRequest(_ request: Request) { 250 | if request.method == "wc_sessionUpdate" { 251 | guard let info = sessionInfo(from: request) else { 252 | // TODO: error handling 253 | try! send(Response(request: request, error: .invalidJSON)) 254 | return 255 | } 256 | 257 | guard let session = communicator.session(by: request.url) else { return } 258 | 259 | if !info.approved { 260 | do { 261 | try disconnect(from: session) 262 | } catch { // session already disconnected 263 | delegate?.client(self, didDisconnect: session) 264 | } 265 | } else { 266 | // we do not add sessions without walletInfo 267 | let walletInfo = session.walletInfo! 268 | let updatedInfo = Session.WalletInfo( 269 | approved: info.approved, 270 | accounts: info.accounts ?? [], 271 | chainId: info.chainId ?? ChainID.mainnet, 272 | peerId: walletInfo.peerId, 273 | peerMeta: walletInfo.peerMeta 274 | ) 275 | var updatedSesson = session 276 | updatedSesson.walletInfo = updatedInfo 277 | communicator.addOrUpdateSession(updatedSesson) 278 | delegate?.client(self, didUpdate: updatedSesson) 279 | } 280 | } else { 281 | // TODO: error handling 282 | let response = try! Response(request: request, error: .methodNotFound) 283 | try! send(response) 284 | } 285 | } 286 | 287 | private func sessionInfo(from request: Request) -> SessionInfo? { 288 | do { 289 | let info = try request.parameter(of: SessionInfo.self, at: 0) 290 | return info 291 | } catch { 292 | LogService.shared.error("WC: incoming approval cannot be parsed: \(error)") 293 | return nil 294 | } 295 | } 296 | 297 | override func sendDisconnectSessionRequest(for session: Session) throws { 298 | let dappInfo = session.dAppInfo.with(approved: false) 299 | let request = try Request(url: session.url, method: "wc_sessionUpdate", params: [dappInfo], id: nil) 300 | try send(request, completion: nil) 301 | } 302 | 303 | override func failedToConnect(_ url: WCURL) { 304 | delegate?.client(self, didFailToConnect: url) 305 | } 306 | 307 | override func didDisconnect(_ session: Session) { 308 | delegate?.client(self, didDisconnect: session) 309 | } 310 | 311 | override func willReconnect(_ session: Session) { 312 | if let delegate = delegate as? ClientDelegateV2 { 313 | delegate.client(self, willReconnect: session) 314 | } 315 | } 316 | 317 | /// Thread-safe collection of client reponses 318 | private class Responses { 319 | 320 | private var responses = [JSONRPC_2_0.IDType: RequestResponse]() 321 | private let queue: DispatchQueue 322 | 323 | init(queue: DispatchQueue) { 324 | self.queue = queue 325 | } 326 | 327 | func add(requestID: JSONRPC_2_0.IDType, response: @escaping RequestResponse) { 328 | dispatchPrecondition(condition: .notOnQueue(queue)) 329 | queue.sync { [unowned self] in 330 | self.responses[requestID] = response 331 | } 332 | } 333 | 334 | func find(requestID: JSONRPC_2_0.IDType) -> RequestResponse? { 335 | var result: RequestResponse? 336 | dispatchPrecondition(condition: .notOnQueue(queue)) 337 | queue.sync { [unowned self] in 338 | result = self.responses[requestID] 339 | } 340 | return result 341 | } 342 | 343 | func remove(requestID: JSONRPC_2_0.IDType) { 344 | dispatchPrecondition(condition: .notOnQueue(queue)) 345 | queue.sync { [unowned self] in 346 | _ = self.responses.removeValue(forKey: requestID) 347 | } 348 | } 349 | } 350 | 351 | /// https://docs.walletconnect.org/json-rpc-api-methods/ethereum#parameters-4 352 | public struct Transaction: Codable { 353 | public var from: String 354 | public var to: String? 355 | public var data: String 356 | public var gas: String? 357 | public var gasPrice: String? 358 | public var value: String? 359 | public var nonce: String? 360 | public var type: String? 361 | public var accessList: [AccessListItem]? 362 | public var chainId: String? 363 | public var maxPriorityFeePerGas: String? 364 | public var maxFeePerGas: String? 365 | 366 | /// https://eips.ethereum.org/EIPS/eip-2930 367 | public struct AccessListItem: Codable { 368 | public var address: String 369 | public var storageKeys: [String] 370 | 371 | public init(address: String, storageKeys: [String]) { 372 | self.address = address 373 | self.storageKeys = storageKeys 374 | } 375 | } 376 | 377 | public init(from: String, 378 | to: String?, 379 | data: String, 380 | gas: String?, 381 | gasPrice: String?, 382 | value: String?, 383 | nonce: String?, 384 | type: String?, 385 | accessList: [AccessListItem]?, 386 | chainId: String?, 387 | maxPriorityFeePerGas: String?, 388 | maxFeePerGas: String?) { 389 | self.from = from 390 | self.to = to 391 | self.data = data 392 | self.gas = gas 393 | self.gasPrice = gasPrice 394 | self.value = value 395 | self.nonce = nonce 396 | self.type = type 397 | self.accessList = accessList 398 | self.chainId = chainId 399 | self.maxPriorityFeePerGas = maxPriorityFeePerGas 400 | self.maxFeePerGas = maxFeePerGas 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /WalletConnectSwift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0A927859230BFB2600FDCC0D /* WalletConnectSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A927850230BFB2600FDCC0D /* WalletConnectSwift.framework */; }; 11 | 0A927862230BFB2600FDCC0D /* WalletConnectSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A927852230BFB2600FDCC0D /* WalletConnectSwift.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 0A92786E230BFB3F00FDCC0D /* AES_256_CBC_HMAC_SHA256_Codec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073822FC22F800BC18BB /* AES_256_CBC_HMAC_SHA256_Codec.swift */; }; 13 | 0A92786F230BFB3F00FDCC0D /* BridgeTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073622FC22F800BC18BB /* BridgeTransport.swift */; }; 14 | 0A927870230BFB3F00FDCC0D /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B65F822FD78B700573DBD /* Client.swift */; }; 15 | 0A927871230BFB3F00FDCC0D /* Communicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B65ED22FC7E4100573DBD /* Communicator.swift */; }; 16 | 0A927873230BFB3F00FDCC0D /* HandshakeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073922FC22F800BC18BB /* HandshakeHandler.swift */; }; 17 | 0A927874230BFB3F00FDCC0D /* PubSubMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073222FC22F800BC18BB /* PubSubMessage.swift */; }; 18 | 0A927875230BFB3F00FDCC0D /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073A22FC22F800BC18BB /* Serializer.swift */; }; 19 | 0A927876230BFB3F00FDCC0D /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073522FC22F800BC18BB /* Server.swift */; }; 20 | 0A927877230BFB3F00FDCC0D /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073122FC22F800BC18BB /* Session.swift */; }; 21 | 0A927878230BFB3F00FDCC0D /* UpdateSessionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073422FC22F800BC18BB /* UpdateSessionHandler.swift */; }; 22 | 0A92787A230BFB3F00FDCC0D /* WebSocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073722FC22F800BC18BB /* WebSocketConnection.swift */; }; 23 | 0A92787B230BFB3F00FDCC0D /* JSONRPC_2_0.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB073022FC22F800BC18BB /* JSONRPC_2_0.swift */; }; 24 | 0A92787C230BFB3F00FDCC0D /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CE9CCB2302A8E600CB414D /* WalletConnect.swift */; }; 25 | 0A92787D230BFB4500FDCC0D /* JSONRPC_2_0_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB075622FC231400BC18BB /* JSONRPC_2_0_Tests.swift */; }; 26 | 0A92787E230BFB4500FDCC0D /* SessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABB075722FC231400BC18BB /* SessionTests.swift */; }; 27 | 0AB4B7F02317D3FD00FDEB49 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4B7EF2317D3FD00FDEB49 /* RequestTests.swift */; }; 28 | 0AB4B7FB2317FCFC00FDEB49 /* ResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4B7FA2317FCFC00FDEB49 /* ResponseTests.swift */; }; 29 | 0AB4B7FD2318050B00FDEB49 /* WCURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4B7FC2318050B00FDEB49 /* WCURLTests.swift */; }; 30 | 0AD7D426231680BC002BF84B /* WCURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD7D41623168008002BF84B /* WCURL.swift */; }; 31 | 0AD7D427231680CE002BF84B /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD7D4212316802C002BF84B /* Request.swift */; }; 32 | 0AD7D428231680CE002BF84B /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD7D42323168033002BF84B /* Response.swift */; }; 33 | 5520F7B82317C0E600D58B74 /* ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5520F7B72317C0E600D58B74 /* ClientTests.swift */; }; 34 | 5520F7C32317C11F00D58B74 /* WalletConnectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5520F7C22317C11F00D58B74 /* WalletConnectTests.swift */; }; 35 | 5520F7C52317C1A600D58B74 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5520F7C42317C1A600D58B74 /* Helpers.swift */; }; 36 | 557CA9B424C1AD05009836F0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557CA9B324C1AD05009836F0 /* Logger.swift */; }; 37 | 55B7819924A614470036F8E5 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 55B7819824A614470036F8E5 /* CryptoSwift */; }; 38 | /* End PBXBuildFile section */ 39 | 40 | /* Begin PBXContainerItemProxy section */ 41 | 0A92785A230BFB2600FDCC0D /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = 0ABB071C22FC22BA00BC18BB /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = 0A92784F230BFB2600FDCC0D; 46 | remoteInfo = WalletConnectSwift; 47 | }; 48 | /* End PBXContainerItemProxy section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | 0A927850230BFB2600FDCC0D /* WalletConnectSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WalletConnectSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | 0A927852230BFB2600FDCC0D /* WalletConnectSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WalletConnectSwift.h; sourceTree = ""; }; 53 | 0A927853230BFB2600FDCC0D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | 0A927858230BFB2600FDCC0D /* WalletConnectSwiftTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WalletConnectSwiftTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 0A927861230BFB2600FDCC0D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 0AB4B7EF2317D3FD00FDEB49 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = ""; }; 57 | 0AB4B7FA2317FCFC00FDEB49 /* ResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseTests.swift; sourceTree = ""; }; 58 | 0AB4B7FC2318050B00FDEB49 /* WCURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCURLTests.swift; sourceTree = ""; }; 59 | 0ABB073022FC22F800BC18BB /* JSONRPC_2_0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONRPC_2_0.swift; sourceTree = ""; }; 60 | 0ABB073122FC22F800BC18BB /* Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 61 | 0ABB073222FC22F800BC18BB /* PubSubMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PubSubMessage.swift; sourceTree = ""; }; 62 | 0ABB073422FC22F800BC18BB /* UpdateSessionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateSessionHandler.swift; sourceTree = ""; }; 63 | 0ABB073522FC22F800BC18BB /* Server.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; 64 | 0ABB073622FC22F800BC18BB /* BridgeTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BridgeTransport.swift; sourceTree = ""; }; 65 | 0ABB073722FC22F800BC18BB /* WebSocketConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketConnection.swift; sourceTree = ""; }; 66 | 0ABB073822FC22F800BC18BB /* AES_256_CBC_HMAC_SHA256_Codec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AES_256_CBC_HMAC_SHA256_Codec.swift; sourceTree = ""; }; 67 | 0ABB073922FC22F800BC18BB /* HandshakeHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HandshakeHandler.swift; sourceTree = ""; }; 68 | 0ABB073A22FC22F800BC18BB /* Serializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializer.swift; sourceTree = ""; }; 69 | 0ABB074E22FC230400BC18BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 70 | 0ABB075622FC231400BC18BB /* JSONRPC_2_0_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONRPC_2_0_Tests.swift; sourceTree = ""; }; 71 | 0ABB075722FC231400BC18BB /* SessionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionTests.swift; sourceTree = ""; }; 72 | 0AD7D41623168008002BF84B /* WCURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCURL.swift; sourceTree = ""; }; 73 | 0AD7D4212316802C002BF84B /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 74 | 0AD7D42323168033002BF84B /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 75 | 551B65ED22FC7E4100573DBD /* Communicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Communicator.swift; sourceTree = ""; }; 76 | 551B65F822FD78B700573DBD /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 77 | 5520F7B72317C0E600D58B74 /* ClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientTests.swift; sourceTree = ""; }; 78 | 5520F7C22317C11F00D58B74 /* WalletConnectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectTests.swift; sourceTree = ""; }; 79 | 5520F7C42317C1A600D58B74 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 80 | 557CA9B324C1AD05009836F0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 81 | 55CE9CCB2302A8E600CB414D /* WalletConnect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnect.swift; sourceTree = ""; }; 82 | /* End PBXFileReference section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 0A92784D230BFB2600FDCC0D /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 55B7819924A614470036F8E5 /* CryptoSwift in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 0A927855230BFB2600FDCC0D /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | 0A927859230BFB2600FDCC0D /* WalletConnectSwift.framework in Frameworks */, 98 | ); 99 | runOnlyForDeploymentPostprocessing = 0; 100 | }; 101 | /* End PBXFrameworksBuildPhase section */ 102 | 103 | /* Begin PBXGroup section */ 104 | 0A927851230BFB2600FDCC0D /* WalletConnectSwift */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | 0A927852230BFB2600FDCC0D /* WalletConnectSwift.h */, 108 | 0A927853230BFB2600FDCC0D /* Info.plist */, 109 | ); 110 | path = WalletConnectSwift; 111 | sourceTree = ""; 112 | }; 113 | 0A92785E230BFB2600FDCC0D /* WalletConnectSwiftTests */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 0A927861230BFB2600FDCC0D /* Info.plist */, 117 | ); 118 | path = WalletConnectSwiftTests; 119 | sourceTree = ""; 120 | }; 121 | 0ABB071B22FC22BA00BC18BB = { 122 | isa = PBXGroup; 123 | children = ( 124 | 0ABB072E22FC22F800BC18BB /* Sources */, 125 | 0ABB075522FC231400BC18BB /* Tests */, 126 | 0A927851230BFB2600FDCC0D /* WalletConnectSwift */, 127 | 0A92785E230BFB2600FDCC0D /* WalletConnectSwiftTests */, 128 | 0ABB072522FC22BA00BC18BB /* Products */, 129 | 0ABB077722FC23F200BC18BB /* Frameworks */, 130 | ); 131 | sourceTree = ""; 132 | }; 133 | 0ABB072522FC22BA00BC18BB /* Products */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 0A927850230BFB2600FDCC0D /* WalletConnectSwift.framework */, 137 | 0A927858230BFB2600FDCC0D /* WalletConnectSwiftTests.xctest */, 138 | ); 139 | name = Products; 140 | sourceTree = ""; 141 | }; 142 | 0ABB072E22FC22F800BC18BB /* Sources */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 0AD7D4362316CF1B002BF84B /* PublicInterface */, 146 | 0AD7D4372316CF34002BF84B /* Internal */, 147 | ); 148 | path = Sources; 149 | sourceTree = ""; 150 | }; 151 | 0ABB075522FC231400BC18BB /* Tests */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 0ABB074E22FC230400BC18BB /* Info.plist */, 155 | 0ABB075622FC231400BC18BB /* JSONRPC_2_0_Tests.swift */, 156 | 0ABB075722FC231400BC18BB /* SessionTests.swift */, 157 | 5520F7B72317C0E600D58B74 /* ClientTests.swift */, 158 | 5520F7C22317C11F00D58B74 /* WalletConnectTests.swift */, 159 | 5520F7C42317C1A600D58B74 /* Helpers.swift */, 160 | 0AB4B7EF2317D3FD00FDEB49 /* RequestTests.swift */, 161 | 0AB4B7FA2317FCFC00FDEB49 /* ResponseTests.swift */, 162 | 0AB4B7FC2318050B00FDEB49 /* WCURLTests.swift */, 163 | ); 164 | path = Tests; 165 | sourceTree = ""; 166 | }; 167 | 0ABB077722FC23F200BC18BB /* Frameworks */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | ); 171 | name = Frameworks; 172 | sourceTree = ""; 173 | }; 174 | 0AD7D4362316CF1B002BF84B /* PublicInterface */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 55CE9CCB2302A8E600CB414D /* WalletConnect.swift */, 178 | 551B65F822FD78B700573DBD /* Client.swift */, 179 | 0ABB073522FC22F800BC18BB /* Server.swift */, 180 | 0ABB073122FC22F800BC18BB /* Session.swift */, 181 | 0AD7D41623168008002BF84B /* WCURL.swift */, 182 | 0AD7D4212316802C002BF84B /* Request.swift */, 183 | 0AD7D42323168033002BF84B /* Response.swift */, 184 | 557CA9B324C1AD05009836F0 /* Logger.swift */, 185 | ); 186 | path = PublicInterface; 187 | sourceTree = ""; 188 | }; 189 | 0AD7D4372316CF34002BF84B /* Internal */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 0ABB073822FC22F800BC18BB /* AES_256_CBC_HMAC_SHA256_Codec.swift */, 193 | 0ABB073622FC22F800BC18BB /* BridgeTransport.swift */, 194 | 551B65ED22FC7E4100573DBD /* Communicator.swift */, 195 | 0ABB073922FC22F800BC18BB /* HandshakeHandler.swift */, 196 | 0ABB073222FC22F800BC18BB /* PubSubMessage.swift */, 197 | 0ABB073A22FC22F800BC18BB /* Serializer.swift */, 198 | 0ABB073422FC22F800BC18BB /* UpdateSessionHandler.swift */, 199 | 0ABB073722FC22F800BC18BB /* WebSocketConnection.swift */, 200 | 0ABB073022FC22F800BC18BB /* JSONRPC_2_0.swift */, 201 | ); 202 | path = Internal; 203 | sourceTree = ""; 204 | }; 205 | /* End PBXGroup section */ 206 | 207 | /* Begin PBXHeadersBuildPhase section */ 208 | 0A92784B230BFB2600FDCC0D /* Headers */ = { 209 | isa = PBXHeadersBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | 0A927862230BFB2600FDCC0D /* WalletConnectSwift.h in Headers */, 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXHeadersBuildPhase section */ 217 | 218 | /* Begin PBXNativeTarget section */ 219 | 0A92784F230BFB2600FDCC0D /* WalletConnectSwift */ = { 220 | isa = PBXNativeTarget; 221 | buildConfigurationList = 0A927867230BFB2600FDCC0D /* Build configuration list for PBXNativeTarget "WalletConnectSwift" */; 222 | buildPhases = ( 223 | 0A92784B230BFB2600FDCC0D /* Headers */, 224 | 0A92784C230BFB2600FDCC0D /* Sources */, 225 | 0A92784D230BFB2600FDCC0D /* Frameworks */, 226 | 0A92784E230BFB2600FDCC0D /* Resources */, 227 | ); 228 | buildRules = ( 229 | ); 230 | dependencies = ( 231 | ); 232 | name = WalletConnectSwift; 233 | packageProductDependencies = ( 234 | 55B7819824A614470036F8E5 /* CryptoSwift */, 235 | ); 236 | productName = WalletConnectSwift; 237 | productReference = 0A927850230BFB2600FDCC0D /* WalletConnectSwift.framework */; 238 | productType = "com.apple.product-type.framework"; 239 | }; 240 | 0A927857230BFB2600FDCC0D /* WalletConnectSwiftTests */ = { 241 | isa = PBXNativeTarget; 242 | buildConfigurationList = 0A92786B230BFB2600FDCC0D /* Build configuration list for PBXNativeTarget "WalletConnectSwiftTests" */; 243 | buildPhases = ( 244 | 0A927854230BFB2600FDCC0D /* Sources */, 245 | 0A927855230BFB2600FDCC0D /* Frameworks */, 246 | 0A927856230BFB2600FDCC0D /* Resources */, 247 | ); 248 | buildRules = ( 249 | ); 250 | dependencies = ( 251 | 0A92785B230BFB2600FDCC0D /* PBXTargetDependency */, 252 | ); 253 | name = WalletConnectSwiftTests; 254 | productName = WalletConnectSwiftTests; 255 | productReference = 0A927858230BFB2600FDCC0D /* WalletConnectSwiftTests.xctest */; 256 | productType = "com.apple.product-type.bundle.unit-test"; 257 | }; 258 | /* End PBXNativeTarget section */ 259 | 260 | /* Begin PBXProject section */ 261 | 0ABB071C22FC22BA00BC18BB /* Project object */ = { 262 | isa = PBXProject; 263 | attributes = { 264 | LastSwiftUpdateCheck = 1030; 265 | LastUpgradeCheck = 1310; 266 | ORGANIZATIONNAME = "Gnosis Ltd."; 267 | TargetAttributes = { 268 | 0A92784F230BFB2600FDCC0D = { 269 | CreatedOnToolsVersion = 10.3; 270 | }; 271 | 0A927857230BFB2600FDCC0D = { 272 | CreatedOnToolsVersion = 10.3; 273 | }; 274 | }; 275 | }; 276 | buildConfigurationList = 0ABB071F22FC22BA00BC18BB /* Build configuration list for PBXProject "WalletConnectSwift" */; 277 | compatibilityVersion = "Xcode 9.3"; 278 | developmentRegion = en; 279 | hasScannedForEncodings = 0; 280 | knownRegions = ( 281 | en, 282 | Base, 283 | ); 284 | mainGroup = 0ABB071B22FC22BA00BC18BB; 285 | packageReferences = ( 286 | 55B7819224A613F40036F8E5 /* XCRemoteSwiftPackageReference "CryptoSwift" */, 287 | ); 288 | productRefGroup = 0ABB072522FC22BA00BC18BB /* Products */; 289 | projectDirPath = ""; 290 | projectRoot = ""; 291 | targets = ( 292 | 0A92784F230BFB2600FDCC0D /* WalletConnectSwift */, 293 | 0A927857230BFB2600FDCC0D /* WalletConnectSwiftTests */, 294 | ); 295 | }; 296 | /* End PBXProject section */ 297 | 298 | /* Begin PBXResourcesBuildPhase section */ 299 | 0A92784E230BFB2600FDCC0D /* Resources */ = { 300 | isa = PBXResourcesBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | ); 304 | runOnlyForDeploymentPostprocessing = 0; 305 | }; 306 | 0A927856230BFB2600FDCC0D /* Resources */ = { 307 | isa = PBXResourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | ); 311 | runOnlyForDeploymentPostprocessing = 0; 312 | }; 313 | /* End PBXResourcesBuildPhase section */ 314 | 315 | /* Begin PBXSourcesBuildPhase section */ 316 | 0A92784C230BFB2600FDCC0D /* Sources */ = { 317 | isa = PBXSourcesBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | 0A927877230BFB3F00FDCC0D /* Session.swift in Sources */, 321 | 0A927878230BFB3F00FDCC0D /* UpdateSessionHandler.swift in Sources */, 322 | 0A92786F230BFB3F00FDCC0D /* BridgeTransport.swift in Sources */, 323 | 0AD7D426231680BC002BF84B /* WCURL.swift in Sources */, 324 | 0AD7D428231680CE002BF84B /* Response.swift in Sources */, 325 | 0A92786E230BFB3F00FDCC0D /* AES_256_CBC_HMAC_SHA256_Codec.swift in Sources */, 326 | 0A92787B230BFB3F00FDCC0D /* JSONRPC_2_0.swift in Sources */, 327 | 0AD7D427231680CE002BF84B /* Request.swift in Sources */, 328 | 557CA9B424C1AD05009836F0 /* Logger.swift in Sources */, 329 | 0A927871230BFB3F00FDCC0D /* Communicator.swift in Sources */, 330 | 0A927873230BFB3F00FDCC0D /* HandshakeHandler.swift in Sources */, 331 | 0A927875230BFB3F00FDCC0D /* Serializer.swift in Sources */, 332 | 0A927874230BFB3F00FDCC0D /* PubSubMessage.swift in Sources */, 333 | 0A927876230BFB3F00FDCC0D /* Server.swift in Sources */, 334 | 0A92787A230BFB3F00FDCC0D /* WebSocketConnection.swift in Sources */, 335 | 0A927870230BFB3F00FDCC0D /* Client.swift in Sources */, 336 | 0A92787C230BFB3F00FDCC0D /* WalletConnect.swift in Sources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | 0A927854230BFB2600FDCC0D /* Sources */ = { 341 | isa = PBXSourcesBuildPhase; 342 | buildActionMask = 2147483647; 343 | files = ( 344 | 0A92787E230BFB4500FDCC0D /* SessionTests.swift in Sources */, 345 | 5520F7C32317C11F00D58B74 /* WalletConnectTests.swift in Sources */, 346 | 5520F7B82317C0E600D58B74 /* ClientTests.swift in Sources */, 347 | 0AB4B7FD2318050B00FDEB49 /* WCURLTests.swift in Sources */, 348 | 0AB4B7FB2317FCFC00FDEB49 /* ResponseTests.swift in Sources */, 349 | 0AB4B7F02317D3FD00FDEB49 /* RequestTests.swift in Sources */, 350 | 5520F7C52317C1A600D58B74 /* Helpers.swift in Sources */, 351 | 0A92787D230BFB4500FDCC0D /* JSONRPC_2_0_Tests.swift in Sources */, 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | /* End PBXSourcesBuildPhase section */ 356 | 357 | /* Begin PBXTargetDependency section */ 358 | 0A92785B230BFB2600FDCC0D /* PBXTargetDependency */ = { 359 | isa = PBXTargetDependency; 360 | target = 0A92784F230BFB2600FDCC0D /* WalletConnectSwift */; 361 | targetProxy = 0A92785A230BFB2600FDCC0D /* PBXContainerItemProxy */; 362 | }; 363 | /* End PBXTargetDependency section */ 364 | 365 | /* Begin XCBuildConfiguration section */ 366 | 0A927868230BFB2600FDCC0D /* Debug */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | CODE_SIGN_IDENTITY = ""; 370 | CODE_SIGN_STYLE = Automatic; 371 | CURRENT_PROJECT_VERSION = 1; 372 | DEFINES_MODULE = YES; 373 | DEVELOPMENT_TEAM = ZKG876RKJ8; 374 | DYLIB_COMPATIBILITY_VERSION = 1; 375 | DYLIB_CURRENT_VERSION = 1; 376 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 377 | INFOPLIST_FILE = WalletConnectSwift/Info.plist; 378 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 379 | LD_RUNPATH_SEARCH_PATHS = ( 380 | "$(inherited)", 381 | "@executable_path/Frameworks", 382 | "@loader_path/Frameworks", 383 | ); 384 | PRODUCT_BUNDLE_IDENTIFIER = io.gnosis.WalletConnectSwift; 385 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 386 | SKIP_INSTALL = YES; 387 | SWIFT_VERSION = 5.0; 388 | TARGETED_DEVICE_FAMILY = "1,2"; 389 | VERSIONING_SYSTEM = "apple-generic"; 390 | VERSION_INFO_PREFIX = ""; 391 | }; 392 | name = Debug; 393 | }; 394 | 0A927869230BFB2600FDCC0D /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | buildSettings = { 397 | CODE_SIGN_IDENTITY = ""; 398 | CODE_SIGN_STYLE = Automatic; 399 | CURRENT_PROJECT_VERSION = 1; 400 | DEFINES_MODULE = YES; 401 | DEVELOPMENT_TEAM = ZKG876RKJ8; 402 | DYLIB_COMPATIBILITY_VERSION = 1; 403 | DYLIB_CURRENT_VERSION = 1; 404 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 405 | INFOPLIST_FILE = WalletConnectSwift/Info.plist; 406 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 407 | LD_RUNPATH_SEARCH_PATHS = ( 408 | "$(inherited)", 409 | "@executable_path/Frameworks", 410 | "@loader_path/Frameworks", 411 | ); 412 | PRODUCT_BUNDLE_IDENTIFIER = io.gnosis.WalletConnectSwift; 413 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 414 | SKIP_INSTALL = YES; 415 | SWIFT_VERSION = 5.0; 416 | TARGETED_DEVICE_FAMILY = "1,2"; 417 | VERSIONING_SYSTEM = "apple-generic"; 418 | VERSION_INFO_PREFIX = ""; 419 | }; 420 | name = Release; 421 | }; 422 | 0A92786C230BFB2600FDCC0D /* Debug */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 426 | CODE_SIGN_STYLE = Automatic; 427 | DEVELOPMENT_TEAM = ZKG876RKJ8; 428 | INFOPLIST_FILE = WalletConnectSwiftTests/Info.plist; 429 | LD_RUNPATH_SEARCH_PATHS = ( 430 | "$(inherited)", 431 | "@executable_path/Frameworks", 432 | "@loader_path/Frameworks", 433 | ); 434 | PRODUCT_BUNDLE_IDENTIFIER = io.gnosis.WalletConnectSwiftTests; 435 | PRODUCT_NAME = "$(TARGET_NAME)"; 436 | SWIFT_VERSION = 5.0; 437 | TARGETED_DEVICE_FAMILY = "1,2"; 438 | }; 439 | name = Debug; 440 | }; 441 | 0A92786D230BFB2600FDCC0D /* Release */ = { 442 | isa = XCBuildConfiguration; 443 | buildSettings = { 444 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 445 | CODE_SIGN_STYLE = Automatic; 446 | DEVELOPMENT_TEAM = ZKG876RKJ8; 447 | INFOPLIST_FILE = WalletConnectSwiftTests/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/Frameworks", 451 | "@loader_path/Frameworks", 452 | ); 453 | PRODUCT_BUNDLE_IDENTIFIER = io.gnosis.WalletConnectSwiftTests; 454 | PRODUCT_NAME = "$(TARGET_NAME)"; 455 | SWIFT_VERSION = 5.0; 456 | TARGETED_DEVICE_FAMILY = "1,2"; 457 | }; 458 | name = Release; 459 | }; 460 | 0ABB072922FC22BA00BC18BB /* Debug */ = { 461 | isa = XCBuildConfiguration; 462 | buildSettings = { 463 | ALWAYS_SEARCH_USER_PATHS = NO; 464 | CLANG_ANALYZER_NONNULL = YES; 465 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 466 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 467 | CLANG_CXX_LIBRARY = "libc++"; 468 | CLANG_ENABLE_MODULES = YES; 469 | CLANG_ENABLE_OBJC_ARC = YES; 470 | CLANG_ENABLE_OBJC_WEAK = YES; 471 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 472 | CLANG_WARN_BOOL_CONVERSION = YES; 473 | CLANG_WARN_COMMA = YES; 474 | CLANG_WARN_CONSTANT_CONVERSION = YES; 475 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 476 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 477 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 478 | CLANG_WARN_EMPTY_BODY = YES; 479 | CLANG_WARN_ENUM_CONVERSION = YES; 480 | CLANG_WARN_INFINITE_RECURSION = YES; 481 | CLANG_WARN_INT_CONVERSION = YES; 482 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 483 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 484 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 485 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 486 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 487 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 488 | CLANG_WARN_STRICT_PROTOTYPES = YES; 489 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 490 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 491 | CLANG_WARN_UNREACHABLE_CODE = YES; 492 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 493 | CODE_SIGN_IDENTITY = "iPhone Developer"; 494 | COPY_PHASE_STRIP = NO; 495 | DEBUG_INFORMATION_FORMAT = dwarf; 496 | ENABLE_STRICT_OBJC_MSGSEND = YES; 497 | ENABLE_TESTABILITY = YES; 498 | GCC_C_LANGUAGE_STANDARD = gnu11; 499 | GCC_DYNAMIC_NO_PIC = NO; 500 | GCC_NO_COMMON_BLOCKS = YES; 501 | GCC_OPTIMIZATION_LEVEL = 0; 502 | GCC_PREPROCESSOR_DEFINITIONS = ( 503 | "DEBUG=1", 504 | "$(inherited)", 505 | ); 506 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 507 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 508 | GCC_WARN_UNDECLARED_SELECTOR = YES; 509 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 510 | GCC_WARN_UNUSED_FUNCTION = YES; 511 | GCC_WARN_UNUSED_VARIABLE = YES; 512 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 513 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 514 | MTL_FAST_MATH = YES; 515 | ONLY_ACTIVE_ARCH = YES; 516 | SDKROOT = iphoneos; 517 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 518 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 519 | }; 520 | name = Debug; 521 | }; 522 | 0ABB072A22FC22BA00BC18BB /* Release */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | ALWAYS_SEARCH_USER_PATHS = NO; 526 | CLANG_ANALYZER_NONNULL = YES; 527 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 528 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 529 | CLANG_CXX_LIBRARY = "libc++"; 530 | CLANG_ENABLE_MODULES = YES; 531 | CLANG_ENABLE_OBJC_ARC = YES; 532 | CLANG_ENABLE_OBJC_WEAK = YES; 533 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 534 | CLANG_WARN_BOOL_CONVERSION = YES; 535 | CLANG_WARN_COMMA = YES; 536 | CLANG_WARN_CONSTANT_CONVERSION = YES; 537 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 538 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 539 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 540 | CLANG_WARN_EMPTY_BODY = YES; 541 | CLANG_WARN_ENUM_CONVERSION = YES; 542 | CLANG_WARN_INFINITE_RECURSION = YES; 543 | CLANG_WARN_INT_CONVERSION = YES; 544 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 545 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 546 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 547 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 548 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 549 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 550 | CLANG_WARN_STRICT_PROTOTYPES = YES; 551 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 552 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 553 | CLANG_WARN_UNREACHABLE_CODE = YES; 554 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 555 | CODE_SIGN_IDENTITY = "iPhone Developer"; 556 | COPY_PHASE_STRIP = NO; 557 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 558 | ENABLE_NS_ASSERTIONS = NO; 559 | ENABLE_STRICT_OBJC_MSGSEND = YES; 560 | GCC_C_LANGUAGE_STANDARD = gnu11; 561 | GCC_NO_COMMON_BLOCKS = YES; 562 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 563 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 564 | GCC_WARN_UNDECLARED_SELECTOR = YES; 565 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 566 | GCC_WARN_UNUSED_FUNCTION = YES; 567 | GCC_WARN_UNUSED_VARIABLE = YES; 568 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 569 | MTL_ENABLE_DEBUG_INFO = NO; 570 | MTL_FAST_MATH = YES; 571 | SDKROOT = iphoneos; 572 | SWIFT_COMPILATION_MODE = wholemodule; 573 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 574 | VALIDATE_PRODUCT = YES; 575 | }; 576 | name = Release; 577 | }; 578 | /* End XCBuildConfiguration section */ 579 | 580 | /* Begin XCConfigurationList section */ 581 | 0A927867230BFB2600FDCC0D /* Build configuration list for PBXNativeTarget "WalletConnectSwift" */ = { 582 | isa = XCConfigurationList; 583 | buildConfigurations = ( 584 | 0A927868230BFB2600FDCC0D /* Debug */, 585 | 0A927869230BFB2600FDCC0D /* Release */, 586 | ); 587 | defaultConfigurationIsVisible = 0; 588 | defaultConfigurationName = Release; 589 | }; 590 | 0A92786B230BFB2600FDCC0D /* Build configuration list for PBXNativeTarget "WalletConnectSwiftTests" */ = { 591 | isa = XCConfigurationList; 592 | buildConfigurations = ( 593 | 0A92786C230BFB2600FDCC0D /* Debug */, 594 | 0A92786D230BFB2600FDCC0D /* Release */, 595 | ); 596 | defaultConfigurationIsVisible = 0; 597 | defaultConfigurationName = Release; 598 | }; 599 | 0ABB071F22FC22BA00BC18BB /* Build configuration list for PBXProject "WalletConnectSwift" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | 0ABB072922FC22BA00BC18BB /* Debug */, 603 | 0ABB072A22FC22BA00BC18BB /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | /* End XCConfigurationList section */ 609 | 610 | /* Begin XCRemoteSwiftPackageReference section */ 611 | 55B7819224A613F40036F8E5 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { 612 | isa = XCRemoteSwiftPackageReference; 613 | repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; 614 | requirement = { 615 | kind = upToNextMinorVersion; 616 | minimumVersion = 1.4.0; 617 | }; 618 | }; 619 | /* End XCRemoteSwiftPackageReference section */ 620 | 621 | /* Begin XCSwiftPackageProductDependency section */ 622 | 55B7819824A614470036F8E5 /* CryptoSwift */ = { 623 | isa = XCSwiftPackageProductDependency; 624 | package = 55B7819224A613F40036F8E5 /* XCRemoteSwiftPackageReference "CryptoSwift" */; 625 | productName = CryptoSwift; 626 | }; 627 | /* End XCSwiftPackageProductDependency section */ 628 | }; 629 | rootObject = 0ABB071C22FC22BA00BC18BB /* Project object */; 630 | } 631 | --------------------------------------------------------------------------------