├── 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 |
--------------------------------------------------------------------------------