diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index d47aa4129..ca91b0389 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -19,7 +19,11 @@ jobs: run: swift build -v - name: Run tests run: swift test -v - - name: Test Example App + - name: Test Wallet working-directory: ./Example - run: fastlane test_app + run: fastlane test_wallet + - name: Test Dapp + working-directory: ./Example + run: fastlane test_dapp + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/IntegrationTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/IntegrationTests.xcscheme new file mode 100644 index 000000000..0236cb7e7 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/IntegrationTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme index ce6b1b1ca..138656d6e 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -62,6 +62,20 @@ ReferencedContainer = "container:"> + + + + ())? + + private let accountsView: AccountsView = { + AccountsView() + }() + + init(session: Session) { + self.session = session + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = accountsView + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "Accounts" + + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "Disconnect", + style: .plain, + target: self, + action: #selector(disconnect) + ) + accountsView.tableView.dataSource = self + accountsView.tableView.delegate = self + client.logger.setLogging(level: .debug) + session.accounts.forEach { account in + let splits = account.split(separator: ":", omittingEmptySubsequences: false) + guard splits.count == 3 else { return } + let chain = String(splits[0] + ":" + splits[1]) + accountsDetails.append(AccountDetails(chain: chain, methods: Array(session.permissions.methods), account: account)) + } + } + + @objc + private func disconnect() { + client.disconnect(topic: session.topic, reason: Reason(code: 0, message: "disconnect")) + onDisconnect?() + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + accountsDetails.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "accountCell", for: indexPath) + let details = accountsDetails[indexPath.row] + cell.textLabel?.text = details.account + cell.imageView?.image = UIImage(named: details.chain) + cell.textLabel?.numberOfLines = 0 + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + showAccountRequestScreen(accountsDetails[indexPath.row]) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 70 + } + + func showAccountRequestScreen(_ details: AccountDetails) { + let vc = AccountRequestViewController(session: session, accountDetails: details) + navigationController?.pushViewController(vc, animated: true) + } + +} diff --git a/Example/DApp/AccountRequest/AccountRequestView.swift b/Example/DApp/AccountRequest/AccountRequestView.swift new file mode 100644 index 000000000..3f446422f --- /dev/null +++ b/Example/DApp/AccountRequest/AccountRequestView.swift @@ -0,0 +1,69 @@ + +import UIKit +import Foundation + +class AccountRequestView: UIView { + let iconView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.backgroundColor = .systemFill + imageView.layer.cornerRadius = 32 + return imageView + }() + + let chainLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 17.0, weight: .heavy) + return label + }() + let accountLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.textColor = .secondaryLabel + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + let headerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16 + stackView.alignment = .center + return stackView + }() + + let tableView = UITableView() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .systemBackground + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "method_cell") + addSubview(iconView) + addSubview(headerStackView) + addSubview(tableView) + + headerStackView.addArrangedSubview(chainLabel) + headerStackView.addArrangedSubview(accountLabel) + + subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + iconView.topAnchor.constraint(equalTo: topAnchor, constant: 64), + iconView.centerXAnchor.constraint(equalTo: centerXAnchor), + + headerStackView.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 32), + headerStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32), + headerStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32), + + tableView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 0), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0), + tableView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0), + tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Example/DApp/AccountRequest/AccountRequestViewController.swift b/Example/DApp/AccountRequest/AccountRequestViewController.swift new file mode 100644 index 000000000..5fdab0d97 --- /dev/null +++ b/Example/DApp/AccountRequest/AccountRequestViewController.swift @@ -0,0 +1,166 @@ + + +import Foundation +import UIKit +import WalletConnect +import WalletConnectUtils + +class AccountRequestViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + private let session: Session + private let client: WalletConnectClient = ClientDelegate.shared.client + private let chainId: String + private let account: String + private let methods = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] + private let accountRequestView = { + AccountRequestView() + }() + + init(session: Session, accountDetails: AccountDetails) { + self.session = session + self.chainId = accountDetails.chain + self.account = accountDetails.account + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = accountRequestView + } + + override func viewDidLoad() { + super.viewDidLoad() + accountRequestView.tableView.delegate = self + accountRequestView.tableView.dataSource = self + accountRequestView.iconView.image = UIImage(named: chainId) + accountRequestView.chainLabel.text = chainId + accountRequestView.accountLabel.text = account + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + methods.count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return "Methods" + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "method_cell", for: indexPath) + cell.textLabel?.text = methods[indexPath.row] + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let method = methods[indexPath.row] + let requestParams = getRequest(for: method) + + + let request = Request(topic: session.topic, method: method, params: requestParams, chainId: chainId) + client.request(params: request) + presentConfirmationAlert() + } + + private func presentConfirmationAlert() { + let alert = UIAlertController(title: "Request Sent", message: nil, preferredStyle: .alert) + let action = UIAlertAction(title: "OK", style: .cancel) + alert.addAction(action) + present(alert, animated: true) + } + + private func getRequest(for method: String) -> AnyCodable { + let account = "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83" + if method == "eth_sendTransaction" { + let tx = Stub.tx + return AnyCodable(tx) + } else if method == "personal_sign" { + return AnyCodable(["0xdeadbeaf", account]) + } else if method == "eth_signTypedData" { + return AnyCodable([account, Stub.eth_signTypedData]) + } + fatalError("not implemented") + } +} + +struct Transaction: Codable { + let from, to, data, gas: String + let gasPrice, value, nonce: String +} + +fileprivate enum Stub { + static let tx = [Transaction(from: "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83", + to: "0x9b2055d370f73ec7d8a03e965129118dc8f5bf83", + data: "0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675", + gas: "0x76c0", + gasPrice: "0x9184e72a000", + value: "0x9184e72a", + nonce: "0x117")] + static let eth_signTypedData = """ +{ +"types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "verifyingContract", + "type": "address" + } + ], + "Person": [ + { + "name": "name", + "type": "string" + }, + { + "name": "wallet", + "type": "address" + } + ], + "Mail": [ + { + "name": "from", + "type": "Person" + }, + { + "name": "to", + "type": "Person" + }, + { + "name": "contents", + "type": "string" + } + ] +}, +"primaryType": "Mail", +"domain": { + "name": "Ether Mail", + "version": "1", + "chainId": 1, + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" +}, +"message": { + "from": { + "name": "Cow", + "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to": { + "name": "Bob", + "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents": "Hello, Bob!" +} +} +""" +} diff --git a/Example/DApp/AppDelegate.swift b/Example/DApp/AppDelegate.swift new file mode 100644 index 000000000..7256ad23d --- /dev/null +++ b/Example/DApp/AppDelegate.swift @@ -0,0 +1,27 @@ + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Example/DApp/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/DApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Example/DApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..78d34c2c3 --- /dev/null +++ b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ItunesArtwork@2x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..e8b3b928c Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png new file mode 100644 index 000000000..7f3a702c3 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..7f3a702c3 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..d38db763f Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..941819fce Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png new file mode 100644 index 000000000..d71e54b8d Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..d71e54b8d Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..dd60b860b Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..7f3a702c3 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png new file mode 100644 index 000000000..5c0665582 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..5c0665582 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0fdab5f95 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0fdab5f95 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..3b88e179c Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..d0c9a1a98 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..03bb3d021 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..fd505b567 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/Example/DApp/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/Example/DApp/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 000000000..f527fe624 Binary files /dev/null and b/Example/DApp/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png differ diff --git a/Example/DApp/Assets.xcassets/Contents.json b/Example/DApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Example/DApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/eip155:1.imageset/Contents.json b/Example/DApp/Assets.xcassets/eip155:1.imageset/Contents.json new file mode 100644 index 000000000..4664aca0a --- /dev/null +++ b/Example/DApp/Assets.xcassets/eip155:1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ethereum (1).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/eip155:1.imageset/ethereum (1).png b/Example/DApp/Assets.xcassets/eip155:1.imageset/ethereum (1).png new file mode 100644 index 000000000..35e1cb09f Binary files /dev/null and b/Example/DApp/Assets.xcassets/eip155:1.imageset/ethereum (1).png differ diff --git a/Example/DApp/Assets.xcassets/eip155:137.imageset/Contents.json b/Example/DApp/Assets.xcassets/eip155:137.imageset/Contents.json new file mode 100644 index 000000000..afd53b2a5 --- /dev/null +++ b/Example/DApp/Assets.xcassets/eip155:137.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "coin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/eip155:137.imageset/coin.png b/Example/DApp/Assets.xcassets/eip155:137.imageset/coin.png new file mode 100644 index 000000000..318e76f50 Binary files /dev/null and b/Example/DApp/Assets.xcassets/eip155:137.imageset/coin.png differ diff --git a/Example/DApp/Base.lproj/LaunchScreen.storyboard b/Example/DApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/Example/DApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/DApp/ClientDelegate.swift b/Example/DApp/ClientDelegate.swift new file mode 100644 index 000000000..2fecc1d15 --- /dev/null +++ b/Example/DApp/ClientDelegate.swift @@ -0,0 +1,42 @@ +import WalletConnect + +class ClientDelegate: WalletConnectClientDelegate { + var client: WalletConnectClient + var onSessionSettled: ((Session)->())? + var onSessionResponse: ((Response)->())? + var onSessionDelete: (()->())? + + static var shared: ClientDelegate = ClientDelegate() + private init() { + let metadata = AppMetadata( + name: "Swift Dapp", + description: "a description", + url: "wallet.connect", + icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) + self.client = WalletConnectClient( + metadata: metadata, + projectId: "52af113ee0c1e1a20f4995730196c13e", + isController: false, + relayHost: "relay.dev.walletconnect.com" + ) + client.delegate = self + } + + func didSettle(session: Session) { + onSessionSettled?(session) + } + + func didDelete(sessionTopic: String, reason: Reason) { + onSessionDelete?() + } + + func didReceive(sessionResponse: Response) { + onSessionResponse?(sessionResponse) + } + + func didUpdate(sessionTopic: String, accounts: Set) { + } + + func didUpgrade(sessionTopic: String, permissions: Session.Permissions) { + } +} diff --git a/Example/ExampleApp/Proposer/ProposerView.swift b/Example/DApp/Connect/ConnectView.swift similarity index 58% rename from Example/ExampleApp/Proposer/ProposerView.swift rename to Example/DApp/Connect/ConnectView.swift index 687b4d2de..64165ff3d 100644 --- a/Example/ExampleApp/Proposer/ProposerView.swift +++ b/Example/DApp/Connect/ConnectView.swift @@ -1,6 +1,9 @@ + +import Foundation import UIKit -final class ProposerView: UIView { +final class ConnectView: UIView { + let tableView = UITableView() let qrCodeView: UIImageView = { let imageView = UIImageView() @@ -19,25 +22,34 @@ final class ProposerView: UIView { return button }() - let tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .insetGrouped) - tableView.backgroundColor = .tertiarySystemBackground - tableView.register(ActiveSessionCell.self, forCellReuseIdentifier: "sessionCell") - return tableView + let connectWalletButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Connect Wallet", for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 17.0, weight: .semibold) + button.backgroundColor = .systemBlue + button.tintColor = .white + button.layer.cornerRadius = 8 + return button }() - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .systemBackground - addSubview(qrCodeView) addSubview(copyButton) + addSubview(connectWalletButton) addSubview(tableView) - + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "pairing_cell") subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ - qrCodeView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 64), + tableView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + + + qrCodeView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 50), qrCodeView.centerXAnchor.constraint(equalTo: centerXAnchor), qrCodeView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.6), qrCodeView.widthAnchor.constraint(equalTo: qrCodeView.heightAnchor), @@ -47,10 +59,10 @@ final class ProposerView: UIView { copyButton.widthAnchor.constraint(equalTo: qrCodeView.widthAnchor), copyButton.heightAnchor.constraint(equalToConstant: 44), - tableView.topAnchor.constraint(equalTo: copyButton.bottomAnchor, constant: 16), - tableView.leadingAnchor.constraint(equalTo: leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) + connectWalletButton.topAnchor.constraint(equalTo: copyButton.bottomAnchor, constant: 16), + connectWalletButton.centerXAnchor.constraint(equalTo: centerXAnchor), + connectWalletButton.widthAnchor.constraint(equalTo: copyButton.widthAnchor), + connectWalletButton.heightAnchor.constraint(equalToConstant: 44), ]) } diff --git a/Example/DApp/Connect/ConnectViewController.swift b/Example/DApp/Connect/ConnectViewController.swift new file mode 100644 index 000000000..09e000a7d --- /dev/null +++ b/Example/DApp/Connect/ConnectViewController.swift @@ -0,0 +1,103 @@ + +import Foundation +import UIKit +import WalletConnect + +class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + let uriString: String + let activePairings: [Pairing] = ClientDelegate.shared.client.getSettledPairings() + let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) + + init(uri: String) { + self.uriString = uri + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private let connectView: ConnectView = { + ConnectView() + }() + + override func loadView() { + view = connectView + } + + override func viewDidLoad() { + super.viewDidLoad() + DispatchQueue.global().async { [unowned self] in + if let qrImage = generateQRCode(from: uriString) { + DispatchQueue.main.async { + connectView.qrCodeView.image = qrImage + connectView.copyButton.isHidden = false + } + } + } + connectView.copyButton.addTarget(self, action: #selector(copyURI), for: .touchUpInside) + connectView.connectWalletButton.addTarget(self, action: #selector(connectWithExampleWallet), for: .touchUpInside) + connectView.tableView.dataSource = self + connectView.tableView.delegate = self + connectView.copyButton.isHidden = true + setUpSegmentedControl() + } + + func setUpSegmentedControl() { + segmentedControl.selectedSegmentIndex = 0 + self.navigationItem.titleView = segmentedControl + segmentedControl.addTarget(self, action: #selector(segmentAction), for: .valueChanged) + } + + @objc func segmentAction() { + if segmentedControl.selectedSegmentIndex == 0 { + connectView.tableView.isHidden = false + } else { + connectView.tableView.isHidden = true + } + } + + @objc func copyURI() { + UIPasteboard.general.string = uriString + } + + + private func generateQRCode(from string: String) -> UIImage? { + let data = string.data(using: .ascii) + if let filter = CIFilter(name: "CIQRCodeGenerator") { + filter.setValue(data, forKey: "inputMessage") + if let output = filter.outputImage { + return UIImage(ciImage: output) + } + } + return nil + } + + @objc func connectWithExampleWallet() { + let url = URL(string: "https://walletconnect.com/wc?uri=\(uriString)")! + UIApplication.shared.open(url, options: [:]) { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + activePairings.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "pairing_cell", for: indexPath) + cell.textLabel?.text = activePairings[indexPath.row].peer!.name + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let pairingTopic = activePairings[indexPath.row].topic + let permissions = Session.Permissions( + blockchains: ["eip155:1", "eip155:137"], + methods: ["eth_sendTransaction", "personal_sign", "eth_signTypedData"], + notifications: [] + ) + _ = try! ClientDelegate.shared.client.connect(sessionPermissions: permissions, topic: pairingTopic) + connectWithExampleWallet() + } +} diff --git a/Example/DApp/Info.plist b/Example/DApp/Info.plist new file mode 100644 index 000000000..7c5f0be1e --- /dev/null +++ b/Example/DApp/Info.plist @@ -0,0 +1,25 @@ + + + + + ITSAppUsesNonExemptEncryption + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Example/DApp/ResponseViewController.swift b/Example/DApp/ResponseViewController.swift new file mode 100644 index 000000000..e85baaafe --- /dev/null +++ b/Example/DApp/ResponseViewController.swift @@ -0,0 +1,109 @@ + +import Foundation +import WalletConnect +import UIKit + +class ResponseViewController: UIViewController { + let response: Response + private let responseView = { + ResponseView() + }() + + init(response: Response) { + self.response = response + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = responseView + } + + override func viewDidLoad() { + super.viewDidLoad() + let record = ClientDelegate.shared.client.getSessionRequestRecord(id: response.result.id)! + switch response.result { + case .response(let response): + responseView.nameLabel.text = "Received Response\n\(record.request.method)" + responseView.descriptionLabel.text = try! response.result.get(String.self).description + case .error(let error): + responseView.nameLabel.text = "Received Error\n\(record.request.method)" + responseView.descriptionLabel.text = error.error.message + } + responseView.dismissButton.addTarget(self, action: #selector(dismissSelf), for: .touchUpInside) + } + + @objc func dismissSelf() { + dismiss(animated: true, completion: nil) + } +} + + +final class ResponseView: UIView { + + let nameLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = UIFont.systemFont(ofSize: 17.0, weight: .heavy) + return label + }() + + let descriptionLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.textColor = .secondaryLabel + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + let dismissButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Dismiss", for: .normal) + button.backgroundColor = .systemBlue + button.tintColor = .white + button.layer.cornerRadius = 8 + button.titleLabel?.font = UIFont.systemFont(ofSize: 17.0, weight: .semibold) + return button + }() + + let headerStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 16 + stackView.alignment = .center + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .systemBackground + + addSubview(headerStackView) + addSubview(dismissButton) + headerStackView.addArrangedSubview(nameLabel) + headerStackView.addArrangedSubview(descriptionLabel) + + subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + + headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 32), + headerStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32), + headerStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32), + + dismissButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16), + dismissButton.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor, constant: 16), + dismissButton.heightAnchor.constraint(equalToConstant: 44), + dismissButton.widthAnchor.constraint(equalToConstant: 120) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift new file mode 100644 index 000000000..ba447fb33 --- /dev/null +++ b/Example/DApp/SceneDelegate.swift @@ -0,0 +1,58 @@ + +import UIKit +import WalletConnect + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + ClientDelegate.shared.onSessionDelete = { [unowned self] in + showSelectChainScreen() + } + ClientDelegate.shared.onSessionResponse = { [unowned self] response in + presentResponse(for: response) + } + if let session = ClientDelegate.shared.client.getSettledSessions().first { + showAccountsScreen(session) + } else { + showSelectChainScreen() + } + } + + func showSelectChainScreen() { + DispatchQueue.main.async { [unowned self] in + let vc = SelectChainViewController() + vc.onSessionSettled = { [unowned self] session in + showAccountsScreen(session) + } + window?.rootViewController = UINavigationController(rootViewController: vc) + window?.makeKeyAndVisible() + } + } + + func showAccountsScreen(_ session: Session) { + DispatchQueue.main.async { [unowned self] in + let vc = AccountsViewController(session: session) + vc.onDisconnect = { [unowned self] in + showSelectChainScreen() + } + window?.rootViewController = UINavigationController(rootViewController: vc) + window?.makeKeyAndVisible() + } + } + + func presentResponse(for response: Response) { + DispatchQueue.main.async { [unowned self] in + let vc = UINavigationController(rootViewController: ResponseViewController(response: response)) + window?.rootViewController?.present(vc, animated: true, completion: nil) + } + } +} + diff --git a/Example/DApp/SelectChain/SelectChainView.swift b/Example/DApp/SelectChain/SelectChainView.swift new file mode 100644 index 000000000..81891e15a --- /dev/null +++ b/Example/DApp/SelectChain/SelectChainView.swift @@ -0,0 +1,48 @@ + +import Foundation +import UIKit + + +class SelectChainView: UIView { + let tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .insetGrouped) + tableView.backgroundColor = .tertiarySystemBackground + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "chain") + return tableView + }() + let connectButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Connect", for: .normal) + button.backgroundColor = .systemBlue + button.tintColor = .white + button.layer.cornerRadius = 8 + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "chain_cell") + backgroundColor = .systemBackground + addSubview(tableView) + addSubview(connectButton) + + subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 16), + tableView.leadingAnchor.constraint(equalTo: leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + + connectButton.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -16), + connectButton.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + connectButton.heightAnchor.constraint(equalToConstant: 44), + connectButton.widthAnchor.constraint(equalToConstant: 120), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Example/DApp/SelectChain/SelectChainViewController.swift b/Example/DApp/SelectChain/SelectChainViewController.swift new file mode 100644 index 000000000..88ea048ed --- /dev/null +++ b/Example/DApp/SelectChain/SelectChainViewController.swift @@ -0,0 +1,71 @@ + + +import Foundation +import WalletConnect +import UIKit + +struct Chain { + let name: String + let id: String +} + +class SelectChainViewController: UIViewController, UITableViewDataSource { + private let selectChainView: SelectChainView = { + SelectChainView() + }() + let chains = [Chain(name: "Ethereum", id: "eip155:1"), Chain(name: "Polygon", id: "eip155:137")] + let client = ClientDelegate.shared.client + var onSessionSettled: ((Session)->())? + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = "Available Chains" + selectChainView.tableView.dataSource = self + selectChainView.connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside) + ClientDelegate.shared.onSessionSettled = { [unowned self] session in + onSessionSettled?(session) + } + } + + override func loadView() { + view = selectChainView + + + } + + @objc + private func connect() { + print("[PROPOSER] Connecting to a pairing...") + let permissions = Session.Permissions( + blockchains: ["eip155:1", "eip155:137"], + methods: ["eth_sendTransaction", "personal_sign", "eth_signTypedData"], + notifications: [] + ) + do { + if let uri = try client.connect(sessionPermissions: permissions) { + showConnectScreen(uriString: uri) + } + } catch { + print("[PROPOSER] Pairing connect error: \(error)") + } + } + + private func showConnectScreen(uriString: String) { + DispatchQueue.main.async { [unowned self] in + let vc = UINavigationController(rootViewController: ConnectViewController(uri: uriString)) + present(vc, animated: true, completion: nil) + } + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + chains.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "chain_cell", for: indexPath) + let chain = chains[indexPath.row] + cell.textLabel?.text = chain.name + cell.imageView?.image = UIImage(named: chain.id) + cell.selectionStyle = .none + return cell + } +} diff --git a/Example/DappTests/DappTests.swift b/Example/DappTests/DappTests.swift new file mode 100644 index 000000000..2792b20c0 --- /dev/null +++ b/Example/DappTests/DappTests.swift @@ -0,0 +1,35 @@ +// +// DappTests.swift +// DappTests +// +// Created by Admin on 27/01/2022. +// + +import XCTest + +class DappTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index dc0b3c745..5bd25f858 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -7,8 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 7603D74D2703429A00DD27A2 /* ProposerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7603D74C2703429A00DD27A2 /* ProposerView.swift */; }; - 761C649A26FB7ABB004239D1 /* ProposerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761C649926FB7ABB004239D1 /* ProposerViewController.swift */; }; 761C649C26FB7B7F004239D1 /* ResponderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761C649B26FB7B7F004239D1 /* ResponderViewController.swift */; }; 761C649E26FB7FD7004239D1 /* SessionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761C649D26FB7FD7004239D1 /* SessionViewController.swift */; }; 761C64A326FB83CE004239D1 /* SessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761C64A226FB83CE004239D1 /* SessionView.swift */; }; @@ -24,32 +22,63 @@ 76744CF926FE4D7400B77ED9 /* ActiveSessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */; }; 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84494387278D9C1B00CC26BB /* UIAlertController.swift */; }; 844943A1278EC49700CC26BB /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = 844943A0278EC49700CC26BB /* Web3 */; }; - 845B30EF27859686002E4094 /* ExampleAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B30EE27859686002E4094 /* ExampleAppTests.swift */; }; 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFB274F98A10081F94C /* RequestViewController.swift */; }; 8460DD002750D6F50081F94C /* SessionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DCFF2750D6F50081F94C /* SessionDetailsViewController.swift */; }; 8460DD022750D7020081F94C /* SessionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8460DD012750D7020081F94C /* SessionDetailsView.swift */; }; + 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE641E27981DED00142511 /* AppDelegate.swift */; }; + 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE642027981DED00142511 /* SceneDelegate.swift */; }; + 84CE642827981DF000142511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642727981DF000142511 /* Assets.xcassets */; }; + 84CE642B27981DF000142511 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642927981DF000142511 /* LaunchScreen.storyboard */; }; + 84CE6430279820F600142511 /* AccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761C649926FB7ABB004239D1 /* AccountsViewController.swift */; }; + 84CE6431279820F600142511 /* AccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7603D74C2703429A00DD27A2 /* AccountsView.swift */; }; + 84CE64392798228D00142511 /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = 84CE64382798228D00142511 /* Web3 */; }; + 84CE643B2798229100142511 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 84CE643A2798229100142511 /* WalletConnect */; }; + 84CE643D2798322600142511 /* ConnectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE643C2798322600142511 /* ConnectViewController.swift */; }; + 84CE6444279AB5AD00142511 /* SelectChainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6443279AB5AD00142511 /* SelectChainViewController.swift */; }; + 84CE6446279ABBF300142511 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6445279ABBF300142511 /* ClientDelegate.swift */; }; + 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6447279AE68600142511 /* AccountRequestViewController.swift */; }; + 84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644A279EA1FA00142511 /* AccountRequestView.swift */; }; + 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644D279ED2FF00142511 /* SelectChainView.swift */; }; + 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6451279ED42B00142511 /* ConnectView.swift */; }; + 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE645427A29D4C00142511 /* ResponseViewController.swift */; }; + 84CE646127A2C85B00142511 /* DappTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE646027A2C85B00142511 /* DappTests.swift */; }; + 84CE647027A2CD6B00142511 /* WalletTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE646F27A2CD6B00142511 /* WalletTests.swift */; }; 84F568C2279582D200D0A289 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C1279582D200D0A289 /* Signer.swift */; }; 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 845B30F027859686002E4094 /* PBXContainerItemProxy */ = { + 84CE646227A2C85B00142511 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; proxyType = 1; remoteGlobalIDString = 764E1D3B26F8D3FC00A1FB15; - remoteInfo = ExampleApp; + remoteInfo = Wallet; + }; + 84CE646727A2C86100142511 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84CE641B27981DED00142511; + remoteInfo = DApp; + }; + 84CE647127A2CD6B00142511 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 764E1D3426F8D3FC00A1FB15 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 764E1D3B26F8D3FC00A1FB15; + remoteInfo = Wallet; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 7603D74C2703429A00DD27A2 /* ProposerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposerView.swift; sourceTree = ""; }; - 761C649926FB7ABB004239D1 /* ProposerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProposerViewController.swift; sourceTree = ""; }; + 7603D74C2703429A00DD27A2 /* AccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsView.swift; sourceTree = ""; }; + 761C649926FB7ABB004239D1 /* AccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsViewController.swift; sourceTree = ""; }; 761C649B26FB7B7F004239D1 /* ResponderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponderViewController.swift; sourceTree = ""; wrapsLines = 0; }; 761C649D26FB7FD7004239D1 /* SessionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionViewController.swift; sourceTree = ""; }; 761C64A226FB83CE004239D1 /* SessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionView.swift; sourceTree = ""; }; 761C64A526FCB0AA004239D1 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; - 764E1D3C26F8D3FC00A1FB15 /* ExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 764E1D3C26F8D3FC00A1FB15 /* WalletConnect Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WalletConnect Wallet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 764E1D3F26F8D3FC00A1FB15 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 764E1D4126F8D3FC00A1FB15 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 764E1D4826F8D3FE00A1FB15 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -63,11 +92,28 @@ 76744CF626FE4D5400B77ED9 /* ActiveSessionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionItem.swift; sourceTree = ""; }; 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionCell.swift; sourceTree = ""; }; 84494387278D9C1B00CC26BB /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; - 845B30EC27859686002E4094 /* ExampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 845B30EE27859686002E4094 /* ExampleAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppTests.swift; sourceTree = ""; }; 8460DCFB274F98A10081F94C /* RequestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestViewController.swift; sourceTree = ""; }; 8460DCFF2750D6F50081F94C /* SessionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailsViewController.swift; sourceTree = ""; }; 8460DD012750D7020081F94C /* SessionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailsView.swift; sourceTree = ""; }; + 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 84CE641E27981DED00142511 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 84CE642027981DED00142511 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 84CE642727981DF000142511 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 84CE642A27981DF000142511 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 84CE642C27981DF000142511 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84CE643C2798322600142511 /* ConnectViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViewController.swift; sourceTree = ""; }; + 84CE6443279AB5AD00142511 /* SelectChainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectChainViewController.swift; sourceTree = ""; }; + 84CE6445279ABBF300142511 /* ClientDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; }; + 84CE6447279AE68600142511 /* AccountRequestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRequestViewController.swift; sourceTree = ""; }; + 84CE644A279EA1FA00142511 /* AccountRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRequestView.swift; sourceTree = ""; }; + 84CE644D279ED2FF00142511 /* SelectChainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectChainView.swift; sourceTree = ""; }; + 84CE6451279ED42B00142511 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = ""; }; + 84CE6453279FFE1100142511 /* Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wallet.entitlements; sourceTree = ""; }; + 84CE645427A29D4C00142511 /* ResponseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseViewController.swift; sourceTree = ""; }; + 84CE645E27A2C85B00142511 /* DappTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DappTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 84CE646027A2C85B00142511 /* DappTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DappTests.swift; sourceTree = ""; }; + 84CE646D27A2CD6B00142511 /* WalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 84CE646F27A2CD6B00142511 /* WalletTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTests.swift; sourceTree = ""; }; 84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; 84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -82,7 +128,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 845B30E927859686002E4094 /* Frameworks */ = { + 84CE641927981DED00142511 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 84CE643B2798229100142511 /* WalletConnect in Frameworks */, + 84CE64392798228D00142511 /* Web3 in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE645B27A2C85B00142511 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE646A27A2CD6B00142511 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -100,8 +162,6 @@ 84F568C1279582D200D0A289 /* Signer.swift */, 84494387278D9C1B00CC26BB /* UIAlertController.swift */, 76744CF426FDFB6B00B77ED9 /* ResponderView.swift */, - 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */, - 76744CF626FE4D5400B77ED9 /* ActiveSessionItem.swift */, 764E1D5926F8DF1B00A1FB15 /* ScannerViewController.swift */, 761C64A426FCB08B004239D1 /* SessionProposal */, 8460DCFD274F98A90081F94C /* Request */, @@ -109,15 +169,6 @@ path = Responder; sourceTree = ""; }; - 761C64A126FB838D004239D1 /* Proposer */ = { - isa = PBXGroup; - children = ( - 761C649926FB7ABB004239D1 /* ProposerViewController.swift */, - 7603D74C2703429A00DD27A2 /* ProposerView.swift */, - ); - path = Proposer; - sourceTree = ""; - }; 761C64A426FCB08B004239D1 /* SessionProposal */ = { isa = PBXGroup; children = ( @@ -131,8 +182,11 @@ 764E1D3326F8D3FC00A1FB15 = { isa = PBXGroup; children = ( + 84CE6453279FFE1100142511 /* Wallet.entitlements */, 764E1D3E26F8D3FC00A1FB15 /* ExampleApp */, - 845B30ED27859686002E4094 /* ExampleAppTests */, + 84CE641D27981DED00142511 /* DApp */, + 84CE645F27A2C85B00142511 /* DappTests */, + 84CE646E27A2CD6B00142511 /* WalletTests */, 764E1D3D26F8D3FC00A1FB15 /* Products */, 764E1D5326F8DAC800A1FB15 /* Frameworks */, 764E1D5626F8DB6000A1FB15 /* WalletConnectSwiftV2 */, @@ -142,8 +196,10 @@ 764E1D3D26F8D3FC00A1FB15 /* Products */ = { isa = PBXGroup; children = ( - 764E1D3C26F8D3FC00A1FB15 /* ExampleApp.app */, - 845B30EC27859686002E4094 /* ExampleAppTests.xctest */, + 764E1D3C26F8D3FC00A1FB15 /* WalletConnect Wallet.app */, + 84CE641C27981DED00142511 /* DApp.app */, + 84CE645E27A2C85B00142511 /* DappTests.xctest */, + 84CE646D27A2CD6B00142511 /* WalletTests.xctest */, ); name = Products; sourceTree = ""; @@ -153,9 +209,10 @@ children = ( 764E1D3F26F8D3FC00A1FB15 /* AppDelegate.swift */, 764E1D4126F8D3FC00A1FB15 /* SceneDelegate.swift */, + 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */, + 76744CF626FE4D5400B77ED9 /* ActiveSessionItem.swift */, 8460DCFE2750D6DF0081F94C /* SessionDetails */, 761C64A026FB8387004239D1 /* Responder */, - 761C64A126FB838D004239D1 /* Proposer */, 764E1D4826F8D3FE00A1FB15 /* Assets.xcassets */, 764E1D4A26F8D3FE00A1FB15 /* LaunchScreen.storyboard */, 764E1D4D26F8D3FE00A1FB15 /* Info.plist */, @@ -172,14 +229,6 @@ name = Frameworks; sourceTree = ""; }; - 845B30ED27859686002E4094 /* ExampleAppTests */ = { - isa = PBXGroup; - children = ( - 845B30EE27859686002E4094 /* ExampleAppTests.swift */, - ); - path = ExampleAppTests; - sourceTree = ""; - }; 8460DCFD274F98A90081F94C /* Request */ = { isa = PBXGroup; children = ( @@ -197,12 +246,82 @@ path = SessionDetails; sourceTree = ""; }; + 84CE641D27981DED00142511 /* DApp */ = { + isa = PBXGroup; + children = ( + 84CE641E27981DED00142511 /* AppDelegate.swift */, + 84CE642027981DED00142511 /* SceneDelegate.swift */, + 84CE645427A29D4C00142511 /* ResponseViewController.swift */, + 84CE6449279EA1E600142511 /* AccountRequest */, + 84CE6445279ABBF300142511 /* ClientDelegate.swift */, + 84CE644C279ED2EC00142511 /* SelectChain */, + 84CE6450279ED41D00142511 /* Connect */, + 84CE644F279ED3FB00142511 /* Accounts */, + 84CE642727981DF000142511 /* Assets.xcassets */, + 84CE642927981DF000142511 /* LaunchScreen.storyboard */, + 84CE642C27981DF000142511 /* Info.plist */, + ); + path = DApp; + sourceTree = ""; + }; + 84CE6449279EA1E600142511 /* AccountRequest */ = { + isa = PBXGroup; + children = ( + 84CE6447279AE68600142511 /* AccountRequestViewController.swift */, + 84CE644A279EA1FA00142511 /* AccountRequestView.swift */, + ); + path = AccountRequest; + sourceTree = ""; + }; + 84CE644C279ED2EC00142511 /* SelectChain */ = { + isa = PBXGroup; + children = ( + 84CE6443279AB5AD00142511 /* SelectChainViewController.swift */, + 84CE644D279ED2FF00142511 /* SelectChainView.swift */, + ); + path = SelectChain; + sourceTree = ""; + }; + 84CE644F279ED3FB00142511 /* Accounts */ = { + isa = PBXGroup; + children = ( + 761C649926FB7ABB004239D1 /* AccountsViewController.swift */, + 7603D74C2703429A00DD27A2 /* AccountsView.swift */, + ); + path = " Accounts"; + sourceTree = ""; + }; + 84CE6450279ED41D00142511 /* Connect */ = { + isa = PBXGroup; + children = ( + 84CE643C2798322600142511 /* ConnectViewController.swift */, + 84CE6451279ED42B00142511 /* ConnectView.swift */, + ); + path = Connect; + sourceTree = ""; + }; + 84CE645F27A2C85B00142511 /* DappTests */ = { + isa = PBXGroup; + children = ( + 84CE646027A2C85B00142511 /* DappTests.swift */, + ); + path = DappTests; + sourceTree = ""; + }; + 84CE646E27A2CD6B00142511 /* WalletTests */ = { + isa = PBXGroup; + children = ( + 84CE646F27A2CD6B00142511 /* WalletTests.swift */, + ); + path = WalletTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 764E1D3B26F8D3FC00A1FB15 /* ExampleApp */ = { + 764E1D3B26F8D3FC00A1FB15 /* Wallet */ = { isa = PBXNativeTarget; - buildConfigurationList = 764E1D5026F8D3FE00A1FB15 /* Build configuration list for PBXNativeTarget "ExampleApp" */; + buildConfigurationList = 764E1D5026F8D3FE00A1FB15 /* Build configuration list for PBXNativeTarget "Wallet" */; buildPhases = ( 764E1D3826F8D3FC00A1FB15 /* Sources */, 764E1D3926F8D3FC00A1FB15 /* Frameworks */, @@ -212,31 +331,71 @@ ); dependencies = ( ); - name = ExampleApp; + name = Wallet; packageProductDependencies = ( 764E1D5726F8DBAB00A1FB15 /* WalletConnect */, 844943A0278EC49700CC26BB /* Web3 */, ); productName = ExampleApp; - productReference = 764E1D3C26F8D3FC00A1FB15 /* ExampleApp.app */; + productReference = 764E1D3C26F8D3FC00A1FB15 /* WalletConnect Wallet.app */; productType = "com.apple.product-type.application"; }; - 845B30EB27859686002E4094 /* ExampleAppTests */ = { + 84CE641B27981DED00142511 /* DApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84CE642F27981DF000142511 /* Build configuration list for PBXNativeTarget "DApp" */; + buildPhases = ( + 84CE641827981DED00142511 /* Sources */, + 84CE641927981DED00142511 /* Frameworks */, + 84CE641A27981DED00142511 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DApp; + packageProductDependencies = ( + 84CE64382798228D00142511 /* Web3 */, + 84CE643A2798229100142511 /* WalletConnect */, + ); + productName = DApp; + productReference = 84CE641C27981DED00142511 /* DApp.app */; + productType = "com.apple.product-type.application"; + }; + 84CE645D27A2C85B00142511 /* DappTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84CE646427A2C85B00142511 /* Build configuration list for PBXNativeTarget "DappTests" */; + buildPhases = ( + 84CE645A27A2C85B00142511 /* Sources */, + 84CE645B27A2C85B00142511 /* Frameworks */, + 84CE645C27A2C85B00142511 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 84CE646327A2C85B00142511 /* PBXTargetDependency */, + 84CE646827A2C86100142511 /* PBXTargetDependency */, + ); + name = DappTests; + productName = DappTests; + productReference = 84CE645E27A2C85B00142511 /* DappTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 84CE646C27A2CD6B00142511 /* WalletTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 845B30F227859686002E4094 /* Build configuration list for PBXNativeTarget "ExampleAppTests" */; + buildConfigurationList = 84CE647327A2CD6C00142511 /* Build configuration list for PBXNativeTarget "WalletTests" */; buildPhases = ( - 845B30E827859686002E4094 /* Sources */, - 845B30E927859686002E4094 /* Frameworks */, - 845B30EA27859686002E4094 /* Resources */, + 84CE646927A2CD6B00142511 /* Sources */, + 84CE646A27A2CD6B00142511 /* Frameworks */, + 84CE646B27A2CD6B00142511 /* Resources */, ); buildRules = ( ); dependencies = ( - 845B30F127859686002E4094 /* PBXTargetDependency */, + 84CE647227A2CD6B00142511 /* PBXTargetDependency */, ); - name = ExampleAppTests; - productName = ExampleAppTests; - productReference = 845B30EC27859686002E4094 /* ExampleAppTests.xctest */; + name = WalletTests; + productName = WalletTests; + productReference = 84CE646D27A2CD6B00142511 /* WalletTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -251,7 +410,14 @@ 764E1D3B26F8D3FC00A1FB15 = { CreatedOnToolsVersion = 12.5.1; }; - 845B30EB27859686002E4094 = { + 84CE641B27981DED00142511 = { + CreatedOnToolsVersion = 13.2; + }; + 84CE645D27A2C85B00142511 = { + CreatedOnToolsVersion = 13.2; + TestTargetID = 84CE641B27981DED00142511; + }; + 84CE646C27A2CD6B00142511 = { CreatedOnToolsVersion = 13.2; TestTargetID = 764E1D3B26F8D3FC00A1FB15; }; @@ -273,8 +439,10 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 764E1D3B26F8D3FC00A1FB15 /* ExampleApp */, - 845B30EB27859686002E4094 /* ExampleAppTests */, + 764E1D3B26F8D3FC00A1FB15 /* Wallet */, + 84CE641B27981DED00142511 /* DApp */, + 84CE645D27A2C85B00142511 /* DappTests */, + 84CE646C27A2CD6B00142511 /* WalletTests */, ); }; /* End PBXProject section */ @@ -289,7 +457,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 845B30EA27859686002E4094 /* Resources */ = { + 84CE641A27981DED00142511 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84CE642B27981DF000142511 /* LaunchScreen.storyboard in Resources */, + 84CE642827981DF000142511 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE645C27A2C85B00142511 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE646B27A2CD6B00142511 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -306,7 +490,6 @@ 761C64A326FB83CE004239D1 /* SessionView.swift in Sources */, 8460DCFC274F98A10081F94C /* RequestViewController.swift in Sources */, 76744CF526FDFB6B00B77ED9 /* ResponderView.swift in Sources */, - 761C649A26FB7ABB004239D1 /* ProposerViewController.swift in Sources */, 8460DD002750D6F50081F94C /* SessionDetailsViewController.swift in Sources */, 76744CF926FE4D7400B77ED9 /* ActiveSessionCell.swift in Sources */, 764E1D4026F8D3FC00A1FB15 /* AppDelegate.swift in Sources */, @@ -317,28 +500,64 @@ 764E1D4226F8D3FC00A1FB15 /* SceneDelegate.swift in Sources */, 84F568C2279582D200D0A289 /* Signer.swift in Sources */, 8460DD022750D7020081F94C /* SessionDetailsView.swift in Sources */, - 7603D74D2703429A00DD27A2 /* ProposerView.swift in Sources */, 764E1D5A26F8DF1B00A1FB15 /* ScannerViewController.swift in Sources */, 84494388278D9C1B00CC26BB /* UIAlertController.swift in Sources */, 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 845B30E827859686002E4094 /* Sources */ = { + 84CE641827981DED00142511 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84CE6430279820F600142511 /* AccountsViewController.swift in Sources */, + 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */, + 84CE6446279ABBF300142511 /* ClientDelegate.swift in Sources */, + 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */, + 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, + 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, + 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */, + 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */, + 84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */, + 84CE6431279820F600142511 /* AccountsView.swift in Sources */, + 84CE643D2798322600142511 /* ConnectViewController.swift in Sources */, + 84CE6444279AB5AD00142511 /* SelectChainViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE645A27A2C85B00142511 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 845B30EF27859686002E4094 /* ExampleAppTests.swift in Sources */, + 84CE646127A2C85B00142511 /* DappTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84CE646927A2CD6B00142511 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84CE647027A2CD6B00142511 /* WalletTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 845B30F127859686002E4094 /* PBXTargetDependency */ = { + 84CE646327A2C85B00142511 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 764E1D3B26F8D3FC00A1FB15 /* Wallet */; + targetProxy = 84CE646227A2C85B00142511 /* PBXContainerItemProxy */; + }; + 84CE646827A2C86100142511 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84CE641B27981DED00142511 /* DApp */; + targetProxy = 84CE646727A2C86100142511 /* PBXContainerItemProxy */; + }; + 84CE647227A2CD6B00142511 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 764E1D3B26F8D3FC00A1FB15 /* ExampleApp */; - targetProxy = 845B30F027859686002E4094 /* PBXContainerItemProxy */; + target = 764E1D3B26F8D3FC00A1FB15 /* Wallet */; + targetProxy = 84CE647127A2CD6B00142511 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -351,6 +570,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 84CE642927981DF000142511 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 84CE642A27981DF000142511 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -475,9 +702,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Wallet.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = W5R8AG9K22; INFOPLIST_FILE = ExampleApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -487,7 +715,7 @@ ); MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.example; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "WalletConnect Wallet"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -499,9 +727,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Wallet.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = W5R8AG9K22; INFOPLIST_FILE = ExampleApp/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -511,14 +740,78 @@ ); MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.example; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "WalletConnect Wallet"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 845B30F327859686002E4094 /* Debug */ = { + 84CE642D27981DF000142511 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = dApp; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 84CE642E27981DF000142511 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = dApp; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.0.1; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.dapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 84CE646527A2C85B00142511 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; @@ -529,16 +822,16 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = walletconnect.ExampleAppTests; + PRODUCT_BUNDLE_IDENTIFIER = walletconnect.DappTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExampleApp.app/ExampleApp"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DApp.app/DApp"; }; name = Debug; }; - 845B30F427859686002E4094 /* Release */ = { + 84CE646627A2C85B00142511 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; @@ -549,12 +842,52 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = walletconnect.ExampleAppTests; + PRODUCT_BUNDLE_IDENTIFIER = walletconnect.DappTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ExampleApp.app/ExampleApp"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DApp.app/DApp"; + }; + name = Release; + }; + 84CE647427A2CD6C00142511 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = walletconnect.WalletTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WalletConnect Wallet.app/WalletConnect Wallet"; + }; + name = Debug; + }; + 84CE647527A2CD6C00142511 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = W5R8AG9K22; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = walletconnect.WalletTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WalletConnect Wallet.app/WalletConnect Wallet"; }; name = Release; }; @@ -570,7 +903,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 764E1D5026F8D3FE00A1FB15 /* Build configuration list for PBXNativeTarget "ExampleApp" */ = { + 764E1D5026F8D3FE00A1FB15 /* Build configuration list for PBXNativeTarget "Wallet" */ = { isa = XCConfigurationList; buildConfigurations = ( 764E1D5126F8D3FE00A1FB15 /* Debug */, @@ -579,11 +912,29 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 845B30F227859686002E4094 /* Build configuration list for PBXNativeTarget "ExampleAppTests" */ = { + 84CE642F27981DF000142511 /* Build configuration list for PBXNativeTarget "DApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84CE642D27981DF000142511 /* Debug */, + 84CE642E27981DF000142511 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84CE646427A2C85B00142511 /* Build configuration list for PBXNativeTarget "DappTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 845B30F327859686002E4094 /* Debug */, - 845B30F427859686002E4094 /* Release */, + 84CE646527A2C85B00142511 /* Debug */, + 84CE646627A2C85B00142511 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84CE647327A2CD6C00142511 /* Build configuration list for PBXNativeTarget "WalletTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84CE647427A2CD6C00142511 /* Debug */, + 84CE647527A2CD6C00142511 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -611,6 +962,15 @@ package = 8449439F278EC49700CC26BB /* XCRemoteSwiftPackageReference "Web3" */; productName = Web3; }; + 84CE64382798228D00142511 /* Web3 */ = { + isa = XCSwiftPackageProductDependency; + package = 8449439F278EC49700CC26BB /* XCRemoteSwiftPackageReference "Web3" */; + productName = Web3; + }; + 84CE643A2798229100142511 /* WalletConnect */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnect; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 764E1D3426F8D3FC00A1FB15 /* Project object */; diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/DApp.xcscheme similarity index 81% rename from Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme rename to Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/DApp.xcscheme index dce1bdf8a..bb02c9037 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/ExampleApp.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/DApp.xcscheme @@ -14,9 +14,9 @@ buildForAnalyzing = "YES"> @@ -32,9 +32,9 @@ skipped = "NO"> @@ -54,9 +54,9 @@ runnableDebuggingMode = "0"> @@ -71,9 +71,9 @@ runnableDebuggingMode = "0"> diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme new file mode 100644 index 000000000..5c9f393ee --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Wallet.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp/Responder/ActiveSessionCell.swift b/Example/ExampleApp/ActiveSessionCell.swift similarity index 100% rename from Example/ExampleApp/Responder/ActiveSessionCell.swift rename to Example/ExampleApp/ActiveSessionCell.swift diff --git a/Example/ExampleApp/Responder/ActiveSessionItem.swift b/Example/ExampleApp/ActiveSessionItem.swift similarity index 100% rename from Example/ExampleApp/Responder/ActiveSessionItem.swift rename to Example/ExampleApp/ActiveSessionItem.swift diff --git a/Example/ExampleApp/AppDelegate.swift b/Example/ExampleApp/AppDelegate.swift index 8476d254c..a02457777 100644 --- a/Example/ExampleApp/AppDelegate.swift +++ b/Example/ExampleApp/AppDelegate.swift @@ -15,4 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } + + + } diff --git a/Example/ExampleApp/Info.plist b/Example/ExampleApp/Info.plist index 1fa4cd501..fd00d23da 100644 --- a/Example/ExampleApp/Info.plist +++ b/Example/ExampleApp/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Demo App + WalletConnect Wallet CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -18,6 +18,19 @@ $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.walletconnect.example + CFBundleURLSchemes + + walletconnectwallet + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption diff --git a/Example/ExampleApp/Proposer/ProposerViewController.swift b/Example/ExampleApp/Proposer/ProposerViewController.swift deleted file mode 100644 index 511c1447e..000000000 --- a/Example/ExampleApp/Proposer/ProposerViewController.swift +++ /dev/null @@ -1,182 +0,0 @@ -import UIKit -import WalletConnect - -final class ProposerViewController: UIViewController { - - let client: WalletConnectClient = { - let metadata = AppMetadata( - name: "Example Proposer", - description: "a description", - url: "wallet.connect", - icons: ["https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media"]) - return WalletConnectClient( - metadata: metadata, - projectId: "52af113ee0c1e1a20f4995730196c13e", - isController: false, - relayHost: "relay.dev.walletconnect.com", - clientName: "proposer" - ) - }() - - var activeItems: [ActiveSessionItem] = [] - private var currentURI: String? - - private let proposerView: ProposerView = { - ProposerView() - }() - - override func loadView() { - view = proposerView - } - - override func viewDidLoad() { - super.viewDidLoad() - navigationItem.title = "Dapp" - - navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "Connect", - style: .plain, - target: self, - action: #selector(connect) - ) - - proposerView.copyButton.addTarget(self, action: #selector(copyURI), for: .touchUpInside) - proposerView.copyButton.isHidden = true - - proposerView.tableView.dataSource = self - proposerView.tableView.delegate = self - - client.delegate = self - client.logger.setLogging(level: .debug) - } - - @objc func copyURI() { - UIPasteboard.general.string = currentURI - } - - @objc - private func connect() { - print("[PROPOSER] Connecting to a pairing...") - let permissions = Session.Permissions( - blockchains: ["a chain"], - methods: ["a method"], - notifications: [] - ) - do { - if let uri = try client.connect(sessionPermissions: permissions) { - showQRCode(uriString: uri) - } - } catch { - print("[PROPOSER] Pairing connect error: \(error)") - } - } - - private func showQRCode(uriString: String) { - currentURI = uriString - DispatchQueue.global().async { [weak self] in - if let qrImage = self?.generateQRCode(from: uriString) { - DispatchQueue.main.async { - self?.proposerView.qrCodeView.image = qrImage - self?.proposerView.copyButton.isHidden = false - } - } - } - } - - private func generateQRCode(from string: String) -> UIImage? { - let data = string.data(using: .ascii) - if let filter = CIFilter(name: "CIQRCodeGenerator") { - filter.setValue(data, forKey: "inputMessage") - if let output = filter.outputImage { - return UIImage(ciImage: output) - } - } - return nil - } -} - -extension ProposerViewController: UITableViewDataSource, UITableViewDelegate { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - activeItems.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "sessionCell", for: indexPath) as! ActiveSessionCell - cell.item = activeItems[indexPath.row] - return cell - } - - func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - activeItems.remove(at: indexPath.row) - tableView.deleteRows(at: [indexPath], with: .automatic) - } - } - - func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { - "Disconnect" - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - print("did select row \(indexPath)") - } -} - -extension ProposerViewController: WalletConnectClientDelegate { - - func didReceive(sessionProposal: Session.Proposal) { - print("[PROPOSER] WC: Did receive session proposal") - } - - func didReceive(sessionRequest: Request) { - print("[PROPOSER] WC: Did receive session request") - } - - func didReceive(notification: Session.Notification, sessionTopic: String) { - - } - - func didUpgrade(sessionTopic: String, permissions: Session.Permissions) { - - } - - func didUpdate(sessionTopic: String, accounts: Set) { - - } - - func didUpdate(pairingTopic: String, appMetadata: AppMetadata) { - - } - - func didDelete(sessionTopic: String, reason: Reason) { - - } - - func didSettle(session: Session) { - print("[PROPOSER] WC: Did settle session") - } - - func didSettle(pairing: Pairing) { - print("[PROPOSER] WC: Did settle pairing") - let settledPairings = client.getSettledPairings() - let activePairings = settledPairings.map { pairing -> ActiveSessionItem in - let app = pairing.peer - return ActiveSessionItem( - dappName: app?.name ?? "", - dappURL: app?.url ?? "", - iconURL: app?.icons?.first ?? "", - topic: pairing.topic) - } - DispatchQueue.main.async { - self.activeItems = activePairings - self.proposerView.tableView.reloadData() - self.proposerView.qrCodeView.image = nil - self.proposerView.copyButton.isHidden = true - } - } - - func didReject(pendingSessionTopic: String, reason: Reason) { - print("[PROPOSER] WC: Did reject session") - } -} diff --git a/Example/ExampleApp/Responder/Request/RequestViewController.swift b/Example/ExampleApp/Responder/Request/RequestViewController.swift index 0bac90b73..a6e392661 100644 --- a/Example/ExampleApp/Responder/Request/RequestViewController.swift +++ b/Example/ExampleApp/Responder/Request/RequestViewController.swift @@ -8,9 +8,8 @@ class RequestViewController: UIViewController { var onSign: (()->())? var onReject: (()->())? let sessionRequest: Request - private let requestView = { - RequestView() - }() + private let requestView = RequestView() + init(_ sessionRequest: Request) { self.sessionRequest = sessionRequest super.init(nibName: nil, bundle: nil) @@ -59,15 +58,6 @@ class RequestViewController: UIViewController { } final class RequestView: UIView { - - let iconView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = .scaleAspectFit - imageView.backgroundColor = .systemFill - imageView.layer.cornerRadius = 32 - return imageView - }() - let nameLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 17.0, weight: .heavy) @@ -115,7 +105,6 @@ final class RequestView: UIView { super.init(frame: frame) backgroundColor = .systemBackground - addSubview(iconView) addSubview(headerStackView) addSubview(approveButton) addSubview(rejectButton) @@ -125,12 +114,8 @@ final class RequestView: UIView { subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ - iconView.topAnchor.constraint(equalTo: topAnchor, constant: 64), - iconView.centerXAnchor.constraint(equalTo: centerXAnchor), - iconView.widthAnchor.constraint(equalToConstant: 64), - iconView.heightAnchor.constraint(equalToConstant: 64), - headerStackView.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 32), + headerStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 32), headerStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 32), headerStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -32), @@ -148,17 +133,6 @@ final class RequestView: UIView { ]) } - func loadImage(at url: String) { - guard let iconURL = URL(string: url) else { return } - DispatchQueue.global().async { - if let imageData = try? Data(contentsOf: iconURL) { - DispatchQueue.main.async { [weak self] in - self?.iconView.image = UIImage(data: imageData) - } - } - } - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Example/ExampleApp/Responder/ResponderViewController.swift b/Example/ExampleApp/Responder/ResponderViewController.swift index c5049becd..c5e662ca6 100644 --- a/Example/ExampleApp/Responder/ResponderViewController.swift +++ b/Example/ExampleApp/Responder/ResponderViewController.swift @@ -16,8 +16,7 @@ final class ResponderViewController: UIViewController { metadata: metadata, projectId: "52af113ee0c1e1a20f4995730196c13e", isController: true, - relayHost: "relay.dev.walletconnect.com", //use with dapp at https://canary.react-app.walletconnect.com/ - clientName: "responder" + relayHost: "relay.dev.walletconnect.com" ) }() lazy var account = Signer.privateKey.address.hex(eip55: true) @@ -43,6 +42,7 @@ final class ResponderViewController: UIViewController { let settledSessions = client.getSettledSessions() sessionItems = getActiveSessionItem(for: settledSessions) client.delegate = self + client.logger.setLogging(level: .debug) } @objc @@ -119,7 +119,6 @@ extension ResponderViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { let item = sessionItems[indexPath.row] -// let deleteParams = SessionType.DeleteParams(topic: item.topic, reason: SessionType.Reason(code: 0, message: "disconnect")) client.disconnect(topic: item.topic, reason: Reason(code: 0, message: "disconnect")) sessionItems.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .automatic) @@ -194,10 +193,6 @@ extension ResponderViewController: WalletConnectClientDelegate { } - func didReceive(notification: Session.Notification, sessionTopic: String) { - - } - func didUpgrade(sessionTopic: String, permissions: Session.Permissions) { } @@ -208,6 +203,9 @@ extension ResponderViewController: WalletConnectClientDelegate { func didDelete(sessionTopic: String, reason: Reason) { reloadActiveSessions() + DispatchQueue.main.async { [unowned self] in + navigationController?.popToRootViewController(animated: true) + } } private func getActiveSessionItem(for settledSessions: [Session]) -> [ActiveSessionItem] { diff --git a/Example/ExampleApp/Responder/Signer.swift b/Example/ExampleApp/Responder/Signer.swift index 89482ac59..683bde6d3 100644 --- a/Example/ExampleApp/Responder/Signer.swift +++ b/Example/ExampleApp/Responder/Signer.swift @@ -16,12 +16,8 @@ class Signer { let result = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) return AnyCodable(result) } else if method == "eth_signTypedData" { - let params = try! request.params.get([String].self) - print(params) - let messageToSign = params[1] - let dataToHash = dataToHash(messageToSign) - let (v, r, s) = try! self.privateKey.sign(message: .init(hex: dataToHash.toHexString())) - let result = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) + //TODO + let result = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" return AnyCodable(result) } else if method == "eth_sendTransaction" { let params = try! request.params.get([EthereumTransaction].self) diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index 3cdd41306..9109d0f93 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -10,17 +10,29 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = UITabBarController.createExampleApp() window?.makeKeyAndVisible() } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL else { + return + } + let wcUri = incomingURL.absoluteString.deletingPrefix("https://walletconnect.com/wc?uri=") + let client = ((window!.rootViewController as! UINavigationController).viewControllers[0] as! ResponderViewController).client + try? client.pair(uri: wcUri) + } } extension UITabBarController { - static func createExampleApp() -> UITabBarController { + static func createExampleApp() -> UINavigationController { let responderController = UINavigationController(rootViewController: ResponderViewController()) - responderController.tabBarItem = UITabBarItem(title: "Wallet", image: UIImage(systemName: "dollarsign.circle"), selectedImage: nil) - let proposerController = UINavigationController(rootViewController: ProposerViewController()) - proposerController.tabBarItem = UITabBarItem(title: "Dapp", image: UIImage(systemName: "appclip"), selectedImage: nil) - let tabBarController = UITabBarController() - tabBarController.viewControllers = [responderController] - return tabBarController + return responderController + } +} + +extension String { + func deletingPrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) } } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift index 688cc3c87..69fe928ad 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailsViewController.swift @@ -10,7 +10,7 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, private let client: WalletConnectClient private let session: Session init(_ session: Session, _ client: WalletConnectClient) { - let pendingRequests = client.getPendingRequests().map{$0.method} + let pendingRequests = client.getPendingRequests(topic: session.topic).map{$0.method} self.sessionInfo = SessionInfo(name: session.peer.name ?? "", descriptionText: session.peer.description ?? "", dappURL: session.peer.description ?? "", @@ -99,7 +99,7 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.section == 2 { - let pendingRequests = client.getPendingRequests() + let pendingRequests = client.getPendingRequests(topic: session.topic) showSessionRequest(pendingRequests[indexPath.row]) } } @@ -120,7 +120,7 @@ final class SessionDetailsViewController: UIViewController, UITableViewDelegate, } func reloadTable() { - let pendingRequests = client.getPendingRequests().map{$0.method} + let pendingRequests = client.getPendingRequests(topic: session.topic).map{$0.method} self.sessionInfo = SessionInfo(name: session.peer.name ?? "", descriptionText: session.peer.description ?? "", dappURL: session.peer.description ?? "", diff --git a/Example/ExampleAppTests/ExampleAppTests.swift b/Example/ExampleAppTests/ExampleAppTests.swift deleted file mode 100644 index e16431ce7..000000000 --- a/Example/ExampleAppTests/ExampleAppTests.swift +++ /dev/null @@ -1,5 +0,0 @@ - -import XCTest - -class ExampleAppTests: XCTestCase { -} diff --git a/Example/Wallet.entitlements b/Example/Wallet.entitlements new file mode 100644 index 000000000..44da28df4 --- /dev/null +++ b/Example/Wallet.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:walletconnect.com + + + diff --git a/Example/WalletTests/WalletTests.swift b/Example/WalletTests/WalletTests.swift new file mode 100644 index 000000000..a248726c3 --- /dev/null +++ b/Example/WalletTests/WalletTests.swift @@ -0,0 +1,35 @@ +// +// WalletTests.swift +// WalletTests +// +// Created by Admin on 27/01/2022. +// + +import XCTest + +class WalletTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Example/fastlane/Fastfile b/Example/fastlane/Fastfile index b63a04a84..45180026e 100644 --- a/Example/fastlane/Fastfile +++ b/Example/fastlane/Fastfile @@ -2,8 +2,13 @@ default_platform(:ios) platform :ios do - desc "Test Example App" - lane :test_app do - scan(xcargs: "-allowProvisioningUpdates") + desc "Test Example Wallet" + lane :test_wallet do + scan(xcargs: "-allowProvisioningUpdates", scheme: "Wallet") + end + + desc "Test Example dapp" + lane :test_dapp do + scan(xcargs: "-allowProvisioningUpdates", scheme: "DApp") end end diff --git a/README.md b/README.md index 3d23662fe..0da9866f2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ Swift implementation of WalletConnect v.2 protocol for native iOS applications. - Swift 5 ## Documentation -In order to build documentation in XCode go to Product -> Build Documentation +- In order to build API documentation in XCode go to Product -> Build Documentation +- [Getting started with wallet integration](https://docs.walletconnect.com/2.0/quick-start/wallets/swift) +- [Beginner guide to WalletConnect v2.0 for iOS Developers](https://medium.com/walletconnect/beginner-guide-to-walletconnect-v2-0-for-swift-developers-4534b0975218) +- [Protocol Documentation](https://docs.walletconnect.com/2.0/protocol/client-communication) +- [Glossary](https://docs.walletconnect.com/2.0/protocol/glossary) ## Usage ### Responder diff --git a/Sources/Relayer/WakuNetworkRelay.swift b/Sources/Relayer/WakuNetworkRelay.swift index e126b30b9..7477a759c 100644 --- a/Sources/Relayer/WakuNetworkRelay.swift +++ b/Sources/Relayer/WakuNetworkRelay.swift @@ -184,7 +184,7 @@ public final class WakuNetworkRelay { private func acknowledgeSubscription(requestId: Int64) { let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) let responseJson = try! response.json() - _ = try? jsonRpcSubscriptionsHistory.resolve(response: JsonRpcResponseTypes.response(response)) + _ = try? jsonRpcSubscriptionsHistory.resolve(response: JsonRpcResult.response(response)) dispatcher.send(responseJson) { [weak self] error in if let error = error { self?.logger.debug("Failed to Respond for request id: \(requestId), error: \(error)") diff --git a/Sources/WalletConnect/Engine/PairingEngine.swift b/Sources/WalletConnect/Engine/PairingEngine.swift index bff5522e0..29736ef5a 100644 --- a/Sources/WalletConnect/Engine/PairingEngine.swift +++ b/Sources/WalletConnect/Engine/PairingEngine.swift @@ -196,7 +196,7 @@ final class PairingEngine { return } let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [unowned self] error in + relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [unowned self] error in if let error = error { logger.error(error) } else { @@ -209,7 +209,7 @@ final class PairingEngine { private func handlePairingPing(topic: String, requestId: Int64) { let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { error in + relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { error in //todo } } @@ -229,7 +229,7 @@ final class PairingEngine { try? crypto.setAgreementSecret(pairingAgreementSecret, topic: sessionProposal.topic) } let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [weak self] error in + relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [weak self] error in self?.onSessionProposal?(sessionProposal) } } @@ -260,7 +260,7 @@ final class PairingEngine { // TODO: Move JSON-RPC responding to networking layer let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: proposal.topic, response: JsonRpcResponseTypes.response(response)) { [weak self] error in + relayer.respond(topic: proposal.topic, response: JsonRpcResult.response(response)) { [weak self] error in if let error = error { self?.logger.error("Could not respond with error: \(error)") } diff --git a/Sources/WalletConnect/Engine/SessionEngine.swift b/Sources/WalletConnect/Engine/SessionEngine.swift index cea274cd0..529fc5810 100644 --- a/Sources/WalletConnect/Engine/SessionEngine.swift +++ b/Sources/WalletConnect/Engine/SessionEngine.swift @@ -3,8 +3,8 @@ import Combine import WalletConnectUtils final class SessionEngine { - var onSessionPayloadRequest: ((Request)->())? + var onSessionPayloadResponse: ((Response)->())? var onSessionApproved: ((Session)->())? var onApprovalAcknowledgement: ((Session) -> Void)? var onSessionRejected: ((String, SessionType.Reason)->())? @@ -44,7 +44,7 @@ final class SessionEngine { restoreSubscriptions() relayer.onResponse = { [weak self] in - self?.handleReponse($0) + self?.handleResponse($0) } } @@ -56,7 +56,7 @@ final class SessionEngine { sequencesStore.getAll().compactMap { guard let settled = $0.settled else { return nil } let permissions = Session.Permissions(blockchains: settled.permissions.blockchain.chains, methods: settled.permissions.jsonrpc.methods) - return Session(topic: $0.topic, peer: settled.peer.metadata!, permissions: permissions) + return Session(topic: $0.topic, peer: settled.peer.metadata!, permissions: permissions, accounts: settled.state.accounts) } } @@ -164,7 +164,7 @@ final class SessionEngine { } } - func request(params: Request, completion: @escaping ((Result, JSONRPCErrorResponse>)->())) { + func request(params: Request) { guard sequencesStore.hasSequence(forTopic: params.topic) else { logger.debug("Could not find session for topic \(params.topic)") return @@ -174,17 +174,15 @@ final class SessionEngine { let sessionPayloadRequest = WCRequest(id: params.id, method: .sessionPayload, params: .sessionPayload(sessionPayloadParams)) relayer.request(topic: params.topic, payload: sessionPayloadRequest) { [weak self] result in switch result { - case .success(let response): - completion(.success(response)) + case .success(_): self?.logger.debug("Did receive session payload response") case .failure(let error): self?.logger.debug("error: \(error)") - completion(.failure(error)) } } } - func respondSessionPayload(topic: String, response: JsonRpcResponseTypes) { + func respondSessionPayload(topic: String, response: JsonRpcResult) { guard sequencesStore.hasSequence(forTopic: topic) else { logger.debug("Could not find session for topic \(topic)") return @@ -198,46 +196,45 @@ final class SessionEngine { } } - func update(topic: String, accounts: Set) { - guard var session = try? sequencesStore.getSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") - return + func update(topic: String, accounts: Set) throws { + guard var session = sequencesStore.getSequence(forTopic: topic) else { + throw WalletConnectError.internal(.noSequenceForTopic) } - session.update(accounts) - relayer.request(.wcSessionUpdate(SessionType.UpdateParams(state: SessionState(accounts: accounts))), onTopic: topic) { [unowned self] result in - switch result { - case .success(_): - sequencesStore.setSequence(session) - onSessionUpdate?(topic, accounts) - case .failure(_): - break + for account in accounts { + if !String.conformsToCAIP10(account) { + throw WalletConnectError.internal(.notApproved) // TODO: Use a suitable error cases } } + if !isController || session.settled?.status != .acknowledged { + throw WalletConnectError.unauthrorized(.unauthorizedUpdateRequest) + } + session.update(accounts) + sequencesStore.setSequence(session) + relayer.request(.wcSessionUpdate(SessionType.UpdateParams(accounts: accounts)), onTopic: topic) } - func upgrade(topic: String, permissions: Session.Permissions) { - guard var session = try? sequencesStore.getSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") - return + func upgrade(topic: String, permissions: Session.Permissions) throws { + let permissions = SessionPermissions(permissions: permissions) + guard var session = sequencesStore.getSequence(forTopic: topic) else { + throw WalletConnectError.noSessionMatchingTopic(topic) } - session.upgrade(permissions) - guard let newPermissions = session.settled?.permissions else { - return + guard session.isSettled else { + throw WalletConnectError.sessionNotSettled(topic) } - relayer.request(.wcSessionUpgrade(SessionType.UpgradeParams(permissions: newPermissions)), onTopic: topic) { [unowned self] result in - switch result { - case .success(_): - sequencesStore.setSequence(session) - onSessionUpgrade?(session.topic, newPermissions) - case .failure(_): - return - //TODO - } + guard isController else { + throw WalletConnectError.unauthorizedNonControllerCall + } + guard validatePermissions(permissions) else { + throw WalletConnectError.invalidPermissions } + session.upgrade(permissions) + let newPermissions = session.settled!.permissions // We know session is settled + sequencesStore.setSequence(session) + relayer.request(.wcSessionUpgrade(SessionType.UpgradeParams(permissions: newPermissions)), onTopic: topic) } func notify(topic: String, params: Session.Notification, completion: ((Error?)->())?) { - guard let session = try? sequencesStore.getSequence(forTopic: topic), session.isSettled else { + guard let session = sequencesStore.getSequence(forTopic: topic), session.isSettled else { logger.debug("Could not find session for topic \(topic)") return } @@ -270,9 +267,9 @@ final class SessionEngine { case .sessionReject(let rejectParams): handleSessionReject(rejectParams, topic: topic) case .sessionUpdate(let updateParams): - handleSessionUpdate(topic: topic, updateParams: updateParams, requestId: requestId) + handleSessionUpdate(payload: subscriptionPayload, updateParams: updateParams) case .sessionUpgrade(let upgradeParams): - handleSessionUpgrade(topic: topic, upgradeParams: upgradeParams, requestId: requestId) + handleSessionUpgrade(payload: subscriptionPayload, upgradeParams: upgradeParams) case .sessionDelete(let deleteParams): handleSessionDelete(deleteParams, topic: topic) case .sessionPayload(let sessionPayloadParams): @@ -288,13 +285,13 @@ final class SessionEngine { } private func handleSessionNotification(topic: String, notificationParams: SessionType.NotificationParams, requestId: Int64) { - guard let session = try? sequencesStore.getSequence(forTopic: topic), session.isSettled else { + guard let session = sequencesStore.getSequence(forTopic: topic), session.isSettled else { return } do { try validateNotification(session: session, params: notificationParams) let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [unowned self] error in + relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [unowned self] error in if let error = error { logger.error(error) } else { @@ -320,56 +317,54 @@ final class SessionEngine { } } - private func handleSessionUpdate(topic: String, updateParams: SessionType.UpdateParams, requestId: Int64) { - guard var session = try? sequencesStore.getSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") + private func handleSessionUpdate(payload: WCRequestSubscriptionPayload, updateParams: SessionType.UpdateParams) { + for account in updateParams.state.accounts { + if !String.conformsToCAIP10(account) { + relayer.respondError(for: payload, reason: .invalidUpdateRequest(context: .session)) + return + } + } + let topic = payload.topic + guard var session = sequencesStore.getSequence(forTopic: topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) return } guard session.peerIsController else { - let error = WalletConnectError.unauthrorized(.unauthorizedUpdateRequest) - logger.error(error) - respond(error: error, requestId: requestId, topic: topic) + relayer.respondError(for: payload, reason: .unauthorizedUpdateRequest(context: .session)) return } - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [unowned self] error in - if let error = error { - logger.error(error) - } else { - session.settled?.state = updateParams.state - sequencesStore.setSequence(session) - onSessionUpdate?(topic, updateParams.state.accounts) - } + guard !isController else { + relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController)) + return } + session.settled?.state = updateParams.state + sequencesStore.setSequence(session) + relayer.respondSuccess(for: payload) + onSessionUpdate?(topic, updateParams.state.accounts) } - private func handleSessionUpgrade(topic: String, upgradeParams: SessionType.UpgradeParams, requestId: Int64) { - guard var session = try? sequencesStore.getSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") + private func handleSessionUpgrade(payload: WCRequestSubscriptionPayload, upgradeParams: SessionType.UpgradeParams) { + guard validatePermissions(upgradeParams.permissions) else { + relayer.respondError(for: payload, reason: .invalidUpgradeRequest(context: .session)) return } - guard session.peerIsController else { - let error = WalletConnectError.unauthrorized(.unauthorizedUpgradeRequest) - logger.error(error) - respond(error: error, requestId: requestId, topic: topic) + guard var session = sequencesStore.getSequence(forTopic: payload.topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: payload.topic)) return } - let permissions = Session.Permissions( - blockchains: upgradeParams.permissions.blockchain.chains, - methods: upgradeParams.permissions.jsonrpc.methods) - session.upgrade(permissions) - guard let newPermissions = session.settled?.permissions else { + guard session.peerIsController else { + relayer.respondError(for: payload, reason: .unauthorizedUpgradeRequest(context: .session)) return } - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [unowned self] error in - if let error = error { - logger.error(error) - } else { - sequencesStore.setSequence(session) - onSessionUpgrade?(session.topic, newPermissions) - } + guard !isController else { + relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController)) + return } + session.upgrade(upgradeParams.permissions) + sequencesStore.setSequence(session) + let newPermissions = session.settled!.permissions // We know session is settled + relayer.respondSuccess(for: payload) + onSessionUpgrade?(session.topic, newPermissions) } private func handleSessionPing(topic: String, requestId: Int64) { @@ -419,7 +414,7 @@ final class SessionEngine { private func respond(error: WalletConnectError, requestId: Int64, topic: String) { let jsonrpcError = JSONRPCErrorResponse.Error(code: error.code, message: error.description) let response = JSONRPCErrorResponse(id: requestId, error: jsonrpcError) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.error(response)) { [weak self] responseError in + relayer.respond(topic: topic, response: JsonRpcResult.error(response)) { [weak self] responseError in if let responseError = responseError { self?.logger.error("Could not respond with error: \(responseError)") } else { @@ -429,7 +424,7 @@ final class SessionEngine { } private func validatePayload(_ sessionRequest: Request) throws { - guard let session = try? sequencesStore.getSequence(forTopic: sessionRequest.topic) else { + guard let session = sequencesStore.getSequence(forTopic: sessionRequest.topic) else { throw WalletConnectError.internal(.noSequenceForTopic) } if let chainId = sessionRequest.chainId { @@ -449,7 +444,7 @@ final class SessionEngine { logger.warn("Warning: Session Engine - Unexpected handleSessionApprove method call by non Controller client") return } - guard let session = try? sequencesStore.getSequence(forTopic: topic), + guard let session = sequencesStore.getSequence(forTopic: topic), let pendingSession = session.pending else { logger.error("Could not find pending session for topic: \(topic)") return @@ -476,10 +471,10 @@ final class SessionEngine { peer: approveParams.responder.metadata, permissions: Session.Permissions( blockchains: pendingSession.proposal.permissions.blockchain.chains, - methods: pendingSession.proposal.permissions.jsonrpc.methods)) + methods: pendingSession.proposal.permissions.jsonrpc.methods), accounts: settledSession.settled!.state.accounts) let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResponseTypes.response(response)) { [unowned self] error in + relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [unowned self] error in if let error = error { logger.error(error) } @@ -502,23 +497,30 @@ final class SessionEngine { }.store(in: &publishers) } - private func handleReponse(_ response: WCResponse) { + private func handleResponse(_ response: WCResponse) { switch response.requestParams { case .pairingPayload(let payloadParams): let proposeParams = payloadParams.request.params handleProposeResponse(topic: response.topic, proposeParams: proposeParams, result: response.result) - case .sessionApprove(let approveParams): + case .sessionApprove(_): handleApproveResponse(topic: response.topic, result: response.result) + case .sessionUpdate: + handleUpdateResponse(topic: response.topic, result: response.result) + case .sessionUpgrade: + handleUpgradeResponse(topic: response.topic, result: response.result) + case .sessionPayload(_): + let response = Response(topic: response.topic, chainId: response.chainId, result: response.result) + onSessionPayloadResponse?(response) default: break } } - private func handleProposeResponse(topic: String, proposeParams: SessionProposal, result: Result, Error>) { + private func handleProposeResponse(topic: String, proposeParams: SessionProposal, result: JsonRpcResult) { switch result { - case .success: + case .response: break - case .failure: + case .error: wcSubscriber.removeSubscription(topic: proposeParams.topic) crypto.deletePrivateKey(for: proposeParams.proposer.publicKey) crypto.deleteAgreementSecret(for: topic) @@ -526,16 +528,17 @@ final class SessionEngine { } } - private func handleApproveResponse(topic: String, result: Result, Error>) { + private func handleApproveResponse(topic: String, result: JsonRpcResult) { guard - let pendingSession = try? sequencesStore.getSequence(forTopic: topic), + let pendingSession = sequencesStore.getSequence(forTopic: topic), let settledTopic = pendingSession.pending?.outcomeTopic, let proposal = pendingSession.pending?.proposal else { return } switch result { - case .success: + case .response: + guard let settledSession = try? sequencesStore.getSequence(forTopic: settledTopic) else {return} crypto.deleteAgreementSecret(for: topic) wcSubscriber.removeSubscription(topic: topic) sequencesStore.delete(topic: topic) @@ -544,9 +547,9 @@ final class SessionEngine { peer: proposal.proposer.metadata, permissions: Session.Permissions( blockchains: proposal.permissions.blockchain.chains, - methods: proposal.permissions.jsonrpc.methods)) + methods: proposal.permissions.jsonrpc.methods), accounts: settledSession.settled!.state.accounts) onApprovalAcknowledgement?(sessionSuccess) - case .failure: + case .error: wcSubscriber.removeSubscription(topic: topic) wcSubscriber.removeSubscription(topic: settledTopic) sequencesStore.delete(topic: topic) @@ -556,4 +559,49 @@ final class SessionEngine { crypto.deletePrivateKey(for: pendingSession.publicKey) } } + + private func handleUpdateResponse(topic: String, result: JsonRpcResult) { + guard let session = sequencesStore.getSequence(forTopic: topic), let accounts = session.settled?.state.accounts else { + return + } + switch result { + case .response: + onSessionUpdate?(topic, accounts) + case .error: + logger.error("Peer failed to update state.") + } + } + + private func handleUpgradeResponse(topic: String, result: JsonRpcResult) { + guard let session = sequencesStore.getSequence(forTopic: topic), let permissions = session.settled?.permissions else { + return + } + switch result { + case .response: + onSessionUpgrade?(session.topic, permissions) + case .error: + logger.error("Peer failed to upgrade permissions.") + } + } + + private func validatePermissions(_ permissions: SessionPermissions) -> Bool { + for chainId in permissions.blockchain.chains { + if !String.conformsToCAIP2(chainId) { + return false + } + } + for method in permissions.jsonrpc.methods { + if method.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return false + } + } + if let notificationTypes = permissions.notifications?.types { + for notification in notificationTypes { + if notification.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return false + } + } + } + return true + } } diff --git a/Sources/WalletConnect/JsonRpcHistory/JsonRpcHistory.swift b/Sources/WalletConnect/JsonRpcHistory/JsonRpcHistory.swift index 9909dccd2..2d0724c0f 100644 --- a/Sources/WalletConnect/JsonRpcHistory/JsonRpcHistory.swift +++ b/Sources/WalletConnect/JsonRpcHistory/JsonRpcHistory.swift @@ -6,7 +6,7 @@ protocol JsonRpcHistoryRecording { func get(id: Int64) -> JsonRpcRecord? func set(topic: String, request: WCRequest, chainId: String?) throws func delete(topic: String) - func resolve(response: JsonRpcResponseTypes) throws -> JsonRpcRecord + func resolve(response: JsonRpcResult) throws -> JsonRpcRecord func exist(id: Int64) -> Bool } //TODO -remove and use jsonrpc history only from utils @@ -40,7 +40,7 @@ class JsonRpcHistory: JsonRpcHistoryRecording { } } - func resolve(response: JsonRpcResponseTypes) throws -> JsonRpcRecord { + func resolve(response: JsonRpcResult) throws -> JsonRpcRecord { logger.debug("Resolving JSON-RPC response - ID: \(response.id)") guard var record = try? storage.get(key: "\(response.id)") else { throw WalletConnectError.internal(.noJsonRpcRequestMatchingResponse) diff --git a/Sources/WalletConnect/JsonRpcHistory/JsonRpcRecord.swift b/Sources/WalletConnect/JsonRpcHistory/JsonRpcRecord.swift index f1461c5be..da8a1e0b1 100644 --- a/Sources/WalletConnect/JsonRpcHistory/JsonRpcRecord.swift +++ b/Sources/WalletConnect/JsonRpcHistory/JsonRpcRecord.swift @@ -7,7 +7,7 @@ struct JsonRpcRecord: Codable { let id: Int64 let topic: String let request: Request - var response: JsonRpcResponseTypes? + var response: JsonRpcResult? let chainId: String? struct Request: Codable { diff --git a/Sources/WalletConnect/Relay/WalletConnectRelay.swift b/Sources/WalletConnect/Relay/WalletConnectRelay.swift index c0f39e4e0..64d25655b 100644 --- a/Sources/WalletConnect/Relay/WalletConnectRelay.swift +++ b/Sources/WalletConnect/Relay/WalletConnectRelay.swift @@ -5,9 +5,10 @@ import WalletConnectUtils struct WCResponse { let topic: String + let chainId: String? let requestMethod: WCRequest.Method let requestParams: WCRequest.Params - let result: Result, Error> + let result: JsonRpcResult } protocol WalletConnectRelaying: AnyObject { @@ -17,7 +18,9 @@ protocol WalletConnectRelaying: AnyObject { var wcRequestPublisher: AnyPublisher {get} func request(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>)->())?) func request(topic: String, payload: WCRequest, completion: ((Result, JSONRPCErrorResponse>)->())?) - func respond(topic: String, response: JsonRpcResponseTypes, completion: @escaping ((Error?)->())) + func respond(topic: String, response: JsonRpcResult, completion: @escaping ((Error?)->())) + func respondSuccess(for payload: WCRequestSubscriptionPayload) + func respondError(for payload: WCRequestSubscriptionPayload, reason: ReasonCode) func subscribe(topic: String) func unsubscribe(topic: String) } @@ -34,7 +37,7 @@ class WalletConnectRelay: WalletConnectRelaying { var onResponse: ((WCResponse) -> Void)? private var networkRelayer: NetworkRelaying - private let jsonRpcSerialiser: JSONRPCSerialising + private let jsonRpcSerializer: JSONRPCSerializing private let jsonRpcHistory: JsonRpcHistoryRecording var transportConnectionPublisher: AnyPublisher { @@ -48,18 +51,18 @@ class WalletConnectRelay: WalletConnectRelaying { } private let wcRequestPublisherSubject = PassthroughSubject() - private var wcResponsePublisher: AnyPublisher { + private var wcResponsePublisher: AnyPublisher { wcResponsePublisherSubject.eraseToAnyPublisher() } - private let wcResponsePublisherSubject = PassthroughSubject() + private let wcResponsePublisherSubject = PassthroughSubject() let logger: ConsoleLogging init(networkRelayer: NetworkRelaying, - jsonRpcSerialiser: JSONRPCSerialising, + jsonRpcSerializer: JSONRPCSerializing, logger: ConsoleLogging, jsonRpcHistory: JsonRpcHistoryRecording) { self.networkRelayer = networkRelayer - self.jsonRpcSerialiser = jsonRpcSerialiser + self.jsonRpcSerializer = jsonRpcSerializer self.logger = logger self.jsonRpcHistory = jsonRpcHistory setUpPublishers() @@ -72,7 +75,7 @@ class WalletConnectRelay: WalletConnectRelaying { func request(topic: String, payload: WCRequest, completion: ((Result, JSONRPCErrorResponse>)->())?) { do { try jsonRpcHistory.set(topic: topic, request: payload, chainId: getChainId(payload)) - let message = try jsonRpcSerialiser.serialise(topic: topic, encodable: payload) + let message = try jsonRpcSerializer.serialize(topic: topic, encodable: payload) networkRelayer.publish(topic: topic, payload: message) { [weak self] error in guard let self = self else {return} if let error = error { @@ -101,10 +104,11 @@ class WalletConnectRelay: WalletConnectRelaying { } } - func respond(topic: String, response: JsonRpcResponseTypes, completion: @escaping ((Error?)->())) { + func respond(topic: String, response: JsonRpcResult, completion: @escaping ((Error?)->())) { do { _ = try jsonRpcHistory.resolve(response: response) - let message = try jsonRpcSerialiser.serialise(topic: topic, encodable: response.value) + + let message = try jsonRpcSerializer.serialize(topic: topic, encodable: response.value) logger.debug("Responding....topic: \(topic)") networkRelayer.publish(topic: topic, payload: message) { error in completion(error) @@ -116,6 +120,16 @@ class WalletConnectRelay: WalletConnectRelaying { } } + func respondSuccess(for payload: WCRequestSubscriptionPayload) { + let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(true)) + respond(topic: payload.topic, response: JsonRpcResult.response(response)) { _ in } // TODO: Move error handling to relayer package + } + + func respondError(for payload: WCRequestSubscriptionPayload, reason: ReasonCode) { + let response = JSONRPCErrorResponse(id: payload.wcRequest.id, error: JSONRPCErrorResponse.Error(code: reason.code, message: reason.message)) + respond(topic: payload.topic, response: JsonRpcResult.error(response)) { _ in } // TODO: Move error handling to relayer package + } + func subscribe(topic: String) { networkRelayer.subscribe(topic: topic) { [weak self] error in if let error = error { @@ -145,12 +159,12 @@ class WalletConnectRelay: WalletConnectRelaying { } private func manageSubscription(_ topic: String, _ message: String) { - if let deserialisedJsonRpcRequest: WCRequest = jsonRpcSerialiser.tryDeserialise(topic: topic, message: message) { - handleWCRequest(topic: topic, request: deserialisedJsonRpcRequest) - } else if let deserialisedJsonRpcResponse: JSONRPCResponse = jsonRpcSerialiser.tryDeserialise(topic: topic, message: message) { - handleJsonRpcResponse(response: deserialisedJsonRpcResponse) - } else if let deserialisedJsonRpcError: JSONRPCErrorResponse = jsonRpcSerialiser.tryDeserialise(topic: topic, message: message) { - handleJsonRpcErrorResponse(response: deserialisedJsonRpcError) + if let deserializedJsonRpcRequest: WCRequest = jsonRpcSerializer.tryDeserialize(topic: topic, message: message) { + handleWCRequest(topic: topic, request: deserializedJsonRpcRequest) + } else if let deserializedJsonRpcResponse: JSONRPCResponse = jsonRpcSerializer.tryDeserialize(topic: topic, message: message) { + handleJsonRpcResponse(response: deserializedJsonRpcResponse) + } else if let deserializedJsonRpcError: JSONRPCErrorResponse = jsonRpcSerializer.tryDeserialize(topic: topic, message: message) { + handleJsonRpcErrorResponse(response: deserializedJsonRpcError) } else { logger.warn("Warning: WalletConnect Relay - Received unknown object type from networking relay") } @@ -170,12 +184,13 @@ class WalletConnectRelay: WalletConnectRelaying { private func handleJsonRpcResponse(response: JSONRPCResponse) { do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResponseTypes.response(response)) + let record = try jsonRpcHistory.resolve(response: JsonRpcResult.response(response)) let wcResponse = WCResponse( topic: record.topic, + chainId: record.chainId, requestMethod: record.request.method, requestParams: record.request.params, - result: .success(response)) + result: JsonRpcResult.response(response)) wcResponsePublisherSubject.send(.response(response)) onPairingResponse?(wcResponse) onResponse?(wcResponse) @@ -186,12 +201,13 @@ class WalletConnectRelay: WalletConnectRelaying { private func handleJsonRpcErrorResponse(response: JSONRPCErrorResponse) { do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResponseTypes.error(response)) + let record = try jsonRpcHistory.resolve(response: JsonRpcResult.error(response)) let wcResponse = WCResponse( topic: record.topic, + chainId: record.chainId, requestMethod: record.request.method, requestParams: record.request.params, - result: .failure(response)) + result: JsonRpcResult.error(response)) wcResponsePublisherSubject.send(.error(response)) onPairingResponse?(wcResponse) onResponse?(wcResponse) diff --git a/Sources/WalletConnect/Response.swift b/Sources/WalletConnect/Response.swift new file mode 100644 index 000000000..736893173 --- /dev/null +++ b/Sources/WalletConnect/Response.swift @@ -0,0 +1,9 @@ + +import Foundation +import WalletConnectUtils + +public struct Response: Codable { + public let topic: String + public let chainId: String? + public let result: JsonRpcResult +} diff --git a/Sources/WalletConnect/Serialiser/JSONRPCSerialiserError.swift b/Sources/WalletConnect/Serialiser/JSONRPCSerialiserError.swift index eadaeec5d..87b10edf9 100644 --- a/Sources/WalletConnect/Serialiser/JSONRPCSerialiserError.swift +++ b/Sources/WalletConnect/Serialiser/JSONRPCSerialiserError.swift @@ -2,6 +2,6 @@ import Foundation -enum JSONRPCSerialiserError: String, Error { +enum JSONRPCSerializerError: String, Error { case messageToShort = "Error: message is too short" } diff --git a/Sources/WalletConnect/Serialiser/JSONRPCSerializing.swift b/Sources/WalletConnect/Serialiser/JSONRPCSerializing.swift index 14e804bc2..6be4a9a2f 100644 --- a/Sources/WalletConnect/Serialiser/JSONRPCSerializing.swift +++ b/Sources/WalletConnect/Serialiser/JSONRPCSerializing.swift @@ -1,12 +1,12 @@ import Foundation -protocol JSONRPCSerialising { - func serialise(topic: String, encodable: Encodable) throws -> String - func tryDeserialise(topic: String, message: String) -> T? +protocol JSONRPCSerializing { + func serialize(topic: String, encodable: Encodable) throws -> String + func tryDeserialize(topic: String, message: String) -> T? var codec: Codec {get} } -class JSONRPCSerialiser: JSONRPCSerialising { +class JSONRPCSerializer: JSONRPCSerializing { private let crypto: Crypto let codec: Codec @@ -16,7 +16,7 @@ class JSONRPCSerialiser: JSONRPCSerialising { self.codec = codec } - func serialise(topic: String, encodable: Encodable) throws -> String { + func serialize(topic: String, encodable: Encodable) throws -> String { let messageJson = try encodable.json() var message: String if let agreementKeys = try? crypto.getAgreementSecret(for: topic) { @@ -27,23 +27,23 @@ class JSONRPCSerialiser: JSONRPCSerialising { return message } - func tryDeserialise(topic: String, message: String) -> T? { + func tryDeserialize(topic: String, message: String) -> T? { do { - let deserialisedJsonRpcRequest: T + let deserializedJsonRpcRequest: T if let agreementKeys = try? crypto.getAgreementSecret(for: topic) { - deserialisedJsonRpcRequest = try deserialise(message: message, symmetricKey: agreementKeys.sharedSecret) + deserializedJsonRpcRequest = try deserialize(message: message, symmetricKey: agreementKeys.sharedSecret) } else { let jsonData = Data(hex: message) - deserialisedJsonRpcRequest = try JSONDecoder().decode(T.self, from: jsonData) + deserializedJsonRpcRequest = try JSONDecoder().decode(T.self, from: jsonData) } - return deserialisedJsonRpcRequest + return deserializedJsonRpcRequest } catch { // logger.debug("Type \(T.self) does not match the payload") return nil } } - func deserialise(message: String, symmetricKey: Data) throws -> T { + func deserialize(message: String, symmetricKey: Data) throws -> T { let JSONRPCData = try decrypt(message: message, symmetricKey: symmetricKey) return try JSONDecoder().decode(T.self, from: JSONRPCData) } @@ -58,7 +58,7 @@ class JSONRPCSerialiser: JSONRPCSerialising { } private func decrypt(message: String, symmetricKey: Data) throws -> Data { - let encryptionPayload = try deserialiseIntoPayload(message: message) + let encryptionPayload = try deserializeIntoPayload(message: message) let decryptedJSONRPC = try codec.decode(payload: encryptionPayload, sharedSecret: symmetricKey) guard let JSONRPCData = decryptedJSONRPC.data(using: .utf8) else { throw DataConversionError.stringToDataFailed @@ -66,10 +66,10 @@ class JSONRPCSerialiser: JSONRPCSerialising { return JSONRPCData } - private func deserialiseIntoPayload(message: String) throws -> EncryptionPayload { + private func deserializeIntoPayload(message: String) throws -> EncryptionPayload { let data = Data(hex: message) guard data.count > EncryptionPayload.ivLength + EncryptionPayload.publicKeyLength + EncryptionPayload.macLength else { - throw JSONRPCSerialiserError.messageToShort + throw JSONRPCSerializerError.messageToShort } let pubKeyRangeStartIndex = EncryptionPayload.ivLength let macStartIndex = pubKeyRangeStartIndex + EncryptionPayload.publicKeyLength diff --git a/Sources/WalletConnect/Session.swift b/Sources/WalletConnect/Session.swift index cd790bf29..6b371767b 100644 --- a/Sources/WalletConnect/Session.swift +++ b/Sources/WalletConnect/Session.swift @@ -7,6 +7,7 @@ public struct Session { public let topic: String public let peer: AppMetadata public let permissions: Permissions + public let accounts: Set } extension Session { diff --git a/Sources/WalletConnect/Storage/SessionStorage.swift b/Sources/WalletConnect/Storage/SessionStorage.swift index c34095eb7..64e5aefb7 100644 --- a/Sources/WalletConnect/Storage/SessionStorage.swift +++ b/Sources/WalletConnect/Storage/SessionStorage.swift @@ -2,7 +2,7 @@ protocol SessionSequenceStorage: AnyObject { var onSequenceExpiration: ((_ topic: String, _ pubKey: String) -> Void)? { get set } func hasSequence(forTopic topic: String) -> Bool func setSequence(_ sequence: SessionSequence) - func getSequence(forTopic topic: String) throws -> SessionSequence? + func getSequence(forTopic topic: String) -> SessionSequence? func getAll() -> [SessionSequence] func delete(topic: String) } @@ -28,8 +28,8 @@ final class SessionStorage: SessionSequenceStorage { storage.setSequence(sequence) } - func getSequence(forTopic topic: String) throws -> SessionSequence? { - try storage.getSequence(forTopic: topic) + func getSequence(forTopic topic: String) -> SessionSequence? { + return try? storage.getSequence(forTopic: topic) } func getAll() -> [SessionSequence] { diff --git a/Sources/WalletConnect/Types/ReasonCode.swift b/Sources/WalletConnect/Types/ReasonCode.swift new file mode 100644 index 000000000..187095d47 --- /dev/null +++ b/Sources/WalletConnect/Types/ReasonCode.swift @@ -0,0 +1,51 @@ +enum ReasonCode { + + enum Context: String { + case pairing = "pairing" + case session = "session" + } + + // 0 (Generic) + case generic(message: String) + + // 1000 (Internal) + case invalidUpdateRequest(context: Context) + case invalidUpgradeRequest(context: Context) + case noContextWithTopic(context: Context, topic: String) + + // 3000 (Unauthorized) + case unauthorizedUpdateRequest(context: Context) + case unauthorizedUpgradeRequest(context: Context) + case unauthorizedMatchingController(isController: Bool) + + var code: Int { + switch self { + case .generic: return 0 + case .invalidUpdateRequest: return 1003 + case .invalidUpgradeRequest: return 1004 + case .noContextWithTopic: return 1301 + case .unauthorizedUpdateRequest: return 3003 + case .unauthorizedUpgradeRequest: return 3004 + case .unauthorizedMatchingController: return 3005 + } + } + + var message: String { + switch self { + case .generic(let message): + return message + case .invalidUpdateRequest(let context): + return "Invalid \(context) update request" + case .invalidUpgradeRequest(let context): + return "Invalid \(context) upgrade request" + case .noContextWithTopic(let context, let topic): + return "No matching \(context) with topic: \(topic)" + case .unauthorizedUpdateRequest(let context): + return "Unauthorized \(context) update request" + case .unauthorizedUpgradeRequest(let context): + return "Unauthorized \(context) upgrade request" + case .unauthorizedMatchingController(let isController): + return "Unauthorized: peer is also \(isController ? "" : "non-")controller" + } + } +} diff --git a/Sources/WalletConnect/Types/Session/SessionPermissions.swift b/Sources/WalletConnect/Types/Session/SessionPermissions.swift index dc65d9ef1..6f19280b1 100644 --- a/Sources/WalletConnect/Types/Session/SessionPermissions.swift +++ b/Sources/WalletConnect/Types/Session/SessionPermissions.swift @@ -38,8 +38,8 @@ struct SessionPermissions: Codable, Equatable { self.controller = nil } - mutating func upgrade(with sessionPermissions: Session.Permissions) { - blockchain.chains.formUnion(sessionPermissions.blockchains) - jsonrpc.methods.formUnion(sessionPermissions.methods) + mutating func upgrade(with permissions: SessionPermissions) { + blockchain.chains.formUnion(permissions.blockchain.chains) + jsonrpc.methods.formUnion(permissions.jsonrpc.methods) } } diff --git a/Sources/WalletConnect/Types/Session/SessionSequence.swift b/Sources/WalletConnect/Types/Session/SessionSequence.swift index 3a8604ce5..b54cf5158 100644 --- a/Sources/WalletConnect/Types/Session/SessionSequence.swift +++ b/Sources/WalletConnect/Types/Session/SessionSequence.swift @@ -39,7 +39,7 @@ struct SessionSequence: ExpirableSequence { } var isSettled: Bool { - settled != nil + settled?.status == .acknowledged } var isController: Bool { @@ -73,7 +73,7 @@ struct SessionSequence: ExpirableSequence { return settled.permissions.jsonrpc.methods.contains(method) } - mutating func upgrade(_ permissions: Session.Permissions) { + mutating func upgrade(_ permissions: SessionPermissions) { settled?.permissions.upgrade(with: permissions) } diff --git a/Sources/WalletConnect/Types/Session/SessionType.swift b/Sources/WalletConnect/Types/Session/SessionType.swift index 2fe408f27..889dfe82d 100644 --- a/Sources/WalletConnect/Types/Session/SessionType.swift +++ b/Sources/WalletConnect/Types/Session/SessionType.swift @@ -19,6 +19,10 @@ internal enum SessionType { struct UpdateParams: Codable, Equatable { let state: SessionState + + init(accounts: Set) { + self.state = SessionState(accounts: accounts) + } } struct UpgradeParams: Codable, Equatable { diff --git a/Sources/WalletConnect/Types/WalletConnectURI.swift b/Sources/WalletConnect/Types/WalletConnectURI.swift index 246d67637..078d97785 100644 --- a/Sources/WalletConnect/Types/WalletConnectURI.swift +++ b/Sources/WalletConnect/Types/WalletConnectURI.swift @@ -1,11 +1,11 @@ import Foundation -struct WalletConnectURI: Equatable { +public struct WalletConnectURI: Equatable { - let topic: String - let version: String - let publicKey: String - let isController: Bool + public let topic: String + public let version: String + public let publicKey: String + public let isController: Bool let relay: RelayProtocolOptions init(topic: String, publicKey: String, isController: Bool, relay: RelayProtocolOptions) { @@ -16,7 +16,7 @@ struct WalletConnectURI: Equatable { self.relay = relay } - init?(string: String) { + public init?(string: String) { guard string.hasPrefix("wc:") else { return nil } @@ -41,7 +41,7 @@ struct WalletConnectURI: Equatable { self.relay = relay } - var absoluteString: String { + public var absoluteString: String { return "wc:\(topic)@\(version)?controller=\(isController)&publicKey=\(publicKey)&relay=\(relay.asPercentEncodedString())" } } diff --git a/Sources/WalletConnect/WalletConnectClient.swift b/Sources/WalletConnect/WalletConnectClient.swift index 5f5cd0157..ddb239ff8 100644 --- a/Sources/WalletConnect/WalletConnectClient.swift +++ b/Sources/WalletConnect/WalletConnectClient.swift @@ -8,7 +8,7 @@ import UIKit /// An Object that expose public API to provide interactions with WalletConnect SDK /// -/// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialise it. Usually only one instance of a client is required in the application. +/// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. /// /// ```swift /// let metadata = AppMetadata(name: String?, description: String?, url: String?, icons: [String]?) @@ -47,7 +47,7 @@ public final class WalletConnectClient { /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults but if for some reasons you want to provide your own storage you can inject it here. /// - clientName: if your app requires more than one client you are required to call them with different names to distinguish logs source and prefix storage keys. /// - /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialise it. Usually only one instance of a client is required in the application. + /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. public convenience init(metadata: AppMetadata, projectId: String, isController: Bool, relayHost: String, keyValueStorage: KeyValueStorage = UserDefaults.standard, clientName: String? = nil) { self.init(metadata: metadata, projectId: projectId, isController: isController, relayHost: relayHost, logger: ConsoleLogger(loggingLevel: .off), keychain: KeychainStorage(uniqueIdentifier: clientName), keyValueStorage: keyValueStorage, clientName: clientName) } @@ -61,9 +61,9 @@ public final class WalletConnectClient { self.secureStorage = SecureStorage(keychain: keychain) let relayUrl = WakuNetworkRelay.makeRelayUrl(host: relayHost, projectId: projectId) self.wakuRelay = WakuNetworkRelay(logger: logger, url: relayUrl, keyValueStorage: keyValueStorage, uniqueIdentifier: clientName ?? "") - let serialiser = JSONRPCSerialiser(crypto: crypto) + let serializer = JSONRPCSerializer(crypto: crypto) self.history = JsonRpcHistory(logger: logger, keyValueStore: KeyValueStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory(clientName: clientName ?? "_"))) - self.relay = WalletConnectRelay(networkRelayer: wakuRelay, jsonRpcSerialiser: serialiser, logger: logger, jsonRpcHistory: history) + self.relay = WalletConnectRelay(networkRelayer: wakuRelay, jsonRpcSerializer: serializer, logger: logger, jsonRpcHistory: history) let pairingSequencesStore = PairingStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.pairings(clientName: clientName ?? "_"))) let sessionSequencesStore = SessionStorage(storage: SequenceStore(storage: keyValueStorage, identifier: StorageDomainIdentifiers.sessions(clientName: clientName ?? "_"))) self.pairingEngine = PairingEngine(relay: relay, crypto: crypto, subscriber: WCSubscriber(relay: relay, logger: logger), sequencesStore: pairingSequencesStore, isController: isController, metadata: metadata, logger: logger) @@ -76,7 +76,7 @@ public final class WalletConnectClient { func registerBackgroundTask() { #if os(iOS) - self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "Finish Network Tasks") { [weak self] in + backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "Finish Network Tasks") { [weak self] in self?.endBackgroundTask() } #endif @@ -157,30 +157,33 @@ public final class WalletConnectClient { /// - topic: Topic of the session that is intended to be updated. /// - accounts: Set of accounts that will be allowed to be used by the session after the update. public func update(topic: String, accounts: Set) { - sessionEngine.update(topic: topic, accounts: accounts) + do { + try sessionEngine.update(topic: topic, accounts: accounts) + } catch { + print("Error on session update call: \(error)") + } } /// For the responder to upgrade session permissions /// - Parameters: /// - topic: Topic of the session that is intended to be upgraded. /// - permissions: Sets of permissions that will be combined with existing ones. - public func upgrade(topic: String, permissions: Session.Permissions) { - sessionEngine.upgrade(topic: topic, permissions: permissions) + public func upgrade(topic: String, permissions: Session.Permissions) throws { + try sessionEngine.upgrade(topic: topic, permissions: permissions) } /// For the proposer to send JSON-RPC requests to responding peer. /// - Parameters: /// - params: Parameters defining request and related session - /// - completion: completion block will provide response from responding client - public func request(params: Request, completion: @escaping (Result, JSONRPCErrorResponse>) -> ()) { - sessionEngine.request(params: params, completion: completion) + public func request(params: Request) { + sessionEngine.request(params: params) } /// For the responder to respond on pending peer's session JSON-RPC Request /// - Parameters: /// - topic: Topic of the session for which the request was received. /// - response: Your JSON RPC response or an error. - public func respond(topic: String, response: JsonRpcResponseTypes) { + public func respond(topic: String, response: JsonRpcResult) { sessionEngine.respondSessionPayload(topic: topic, response: response) } @@ -244,15 +247,31 @@ public final class WalletConnectClient { pairingEngine.getSettledPairings() } - public func getPendingRequests() -> [Request] { - history.getPending() + /// - Returns: Pending requests received with wc_sessionPayload + /// - Parameter topic: topic representing session for which you want to get pending requests. If nil, you will receive pending requests for all active sessions. + public func getPendingRequests(topic: String? = nil) -> [Request] { + let pendingRequests: [Request] = history.getPending() .filter{$0.request.method == .sessionPayload} .compactMap { guard case let .sessionPayload(payloadRequest) = $0.request.params else {return nil} return Request(id: $0.id, topic: $0.topic, method: payloadRequest.request.method, params: payloadRequest.request.params, chainId: payloadRequest.chainId) } + if let topic = topic { + return pendingRequests.filter{$0.topic == topic} + } else { + return pendingRequests + } } + /// - Parameter id: id of a wc_sessionPayload jsonrpc request + /// - Returns: json rpc record object for given id or nil if record for give id does not exits + public func getSessionRequestRecord(id: Int64) -> WalletConnectUtils.JsonRpcRecord? { + guard let record = history.get(id: id), + case .sessionPayload(let payload) = record.request.params else {return nil} + let request = WalletConnectUtils.JsonRpcRecord.Request(method: payload.request.method, params: payload.request.params) + return WalletConnectUtils.JsonRpcRecord(id: record.id, topic: record.topic, request: request, response: record.response, chainId: record.chainId) + } + // MARK: - Private private func setUpEnginesCallbacks() { @@ -266,10 +285,11 @@ public final class WalletConnectClient { pairingEngine.onApprovalAcknowledgement = { [weak self] settledPairing in self?.delegate?.didSettle(pairing: settledPairing) } + pairingEngine.onPairingUpdate = { [unowned self] topic, appMetadata in + delegate?.didUpdate(pairingTopic: topic, appMetadata: appMetadata) + } sessionEngine.onSessionApproved = { [unowned self] settledSession in - let permissions = Session.Permissions.init(blockchains: settledSession.permissions.blockchains, methods: settledSession.permissions.methods) - let session = Session(topic: settledSession.topic, peer: settledSession.peer, permissions: permissions) - delegate?.didSettle(session: session) + delegate?.didSettle(session: settledSession) } sessionEngine.onApprovalAcknowledgement = { [weak self] session in self?.delegate?.didSettle(session: session) @@ -293,8 +313,8 @@ public final class WalletConnectClient { sessionEngine.onNotificationReceived = { [unowned self] topic, notification in delegate?.didReceive(notification: notification, sessionTopic: topic) } - pairingEngine.onPairingUpdate = { [unowned self] topic, appMetadata in - delegate?.didUpdate(pairingTopic: topic, appMetadata: appMetadata) + sessionEngine.onSessionPayloadResponse = { [unowned self] response in + delegate?.didReceive(sessionResponse: response) } } diff --git a/Sources/WalletConnect/WalletConnectClientDelegate.swift b/Sources/WalletConnect/WalletConnectClientDelegate.swift index 372b3067a..a9df37c0a 100644 --- a/Sources/WalletConnect/WalletConnectClientDelegate.swift +++ b/Sources/WalletConnect/WalletConnectClientDelegate.swift @@ -9,13 +9,20 @@ public protocol WalletConnectClientDelegate: AnyObject { /// Function is executed on responder client only func didReceive(sessionProposal: Session.Proposal) - /// Tells the delegate that session request has been received + /// Tells the delegate that session payload request has been received /// /// In most cases that function is supposed to be called on wallet client. /// - Parameters: /// - sessionRequest: Object containing request received from peer client. func didReceive(sessionRequest: Request) + /// Tells the delegate that session payload response has been received + /// + /// In most cases that function is supposed to be called on dApp client. + /// - Parameters: + /// - sessionResponse: Object containing response received from peer client. + func didReceive(sessionResponse: Response) + /// Tells the delegate that the peer client has terminated the session. /// /// Function can be executed on any type of the client. @@ -26,7 +33,7 @@ public protocol WalletConnectClientDelegate: AnyObject { /// Function is executed on controller and non-controller client when both communicating peers have successfully upgraded permissions. func didUpgrade(sessionTopic: String, permissions: Session.Permissions) - /// Tells the delegate that extra eccounts has been included in session sequence + /// Tells the delegate that extra accounts has been included in session sequence /// /// Function is executed on controller and non-controller client when both communicating peers have successfully included new accounts requested by the controller client. func didUpdate(sessionTopic: String, accounts: Set) @@ -41,7 +48,7 @@ public protocol WalletConnectClientDelegate: AnyObject { /// Function is executed on proposer and responder client when both communicating peers have successfully established a pairing. func didSettle(pairing: Pairing) - /// Tells the delegate that sotification has been received. + /// Tells the delegate that notification has been received. func didReceive(notification: Session.Notification, sessionTopic : String) /// Tells the delegate that peer client has rejected a session proposal. @@ -60,4 +67,7 @@ public extension WalletConnectClientDelegate { func didReceive(notification: Session.Notification, sessionTopic: String) {} func didReject(pendingSessionTopic: String, reason: Reason) {} func didUpdate(pairingTopic: String, appMetadata: AppMetadata) {} + func didReceive(sessionRequest: Request) {} + func didReceive(sessionProposal: Session.Proposal) {} + func didReceive(sessionResponse: Response) {} } diff --git a/Sources/WalletConnect/WalletConnectError.swift b/Sources/WalletConnect/WalletConnectError.swift index a05eb5a06..67ce2bd7a 100644 --- a/Sources/WalletConnect/WalletConnectError.swift +++ b/Sources/WalletConnect/WalletConnectError.swift @@ -2,19 +2,18 @@ import Foundation +// TODO: Migrate protocol errors to ReasonCode enum over time. Use WalletConnectError for client errors only. enum WalletConnectError: Error { - // 1000 (Internal) + case noSessionMatchingTopic(String) + case sessionNotSettled(String) + case invalidPermissions + case unauthorizedNonControllerCall + case `internal`(_ reason: InternalReason) - // 2000 (Timeout) - // 3000 (Unauthorized) case unauthrorized(_ reason: UnauthorizedReason) - // 4000 (EIP-1193) - // 5000 (CAIP-25) - // 9000 (Unknown) - enum InternalReason: Error { case notApproved case malformedPairingURI @@ -50,11 +49,21 @@ extension WalletConnectError: CustomStringConvertible { return reason.code case .unauthrorized(let reason): return reason.code + default: + return 0 } } var localizedDescription: String { switch self { + case .noSessionMatchingTopic(let topic): + return "No session found matching topic \(topic)." + case .sessionNotSettled(let topic): + return "Session is not settled on topic \(topic)." + case .invalidPermissions: + return "Permission set is invalid." + case .unauthorizedNonControllerCall: + return "Method must be called by a controller client." case .internal(let reason): return reason.description case .unauthrorized(let reason): diff --git a/Sources/WalletConnectUtils/AnyCodable.swift b/Sources/WalletConnectUtils/AnyCodable.swift index 8d0436d6a..98ec034cf 100644 --- a/Sources/WalletConnectUtils/AnyCodable.swift +++ b/Sources/WalletConnectUtils/AnyCodable.swift @@ -75,10 +75,10 @@ extension AnyCodable: Decodable, Encodable { } else if let stringVal = try? container.decode(String.self) { value = stringVal } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable") + throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serializable") } } else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise")) + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialize")) } } diff --git a/Sources/WalletConnectUtils/JSONRPC/JsonRpcResponseTypes.swift b/Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift similarity index 91% rename from Sources/WalletConnectUtils/JSONRPC/JsonRpcResponseTypes.swift rename to Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift index 1ef0fb00d..f7fee74f8 100644 --- a/Sources/WalletConnectUtils/JSONRPC/JsonRpcResponseTypes.swift +++ b/Sources/WalletConnectUtils/JSONRPC/JsonRpcResult.swift @@ -2,7 +2,7 @@ import Foundation -public enum JsonRpcResponseTypes: Codable { +public enum JsonRpcResult: Codable { case error(JSONRPCErrorResponse) case response(JSONRPCResponse) public var id: Int64 { diff --git a/Sources/WalletConnectUtils/JsonRpcHistory.swift b/Sources/WalletConnectUtils/JsonRpcHistory.swift index bca7270a0..8d2f27b58 100644 --- a/Sources/WalletConnectUtils/JsonRpcHistory.swift +++ b/Sources/WalletConnectUtils/JsonRpcHistory.swift @@ -35,7 +35,7 @@ public class JsonRpcHistory where T: Codable&Equatable { } } - public func resolve(response: JsonRpcResponseTypes) throws -> JsonRpcRecord { + public func resolve(response: JsonRpcResult) throws -> JsonRpcRecord { logger.debug("Resolving JSON-RPC response - ID: \(response.id)") guard var record = try? storage.get(key: "\(response.id)") else { throw RecordingError.noJsonRpcRequestMatchingResponse diff --git a/Sources/WalletConnectUtils/JsonRpcRecord.swift b/Sources/WalletConnectUtils/JsonRpcRecord.swift index 225cca1ac..151f51651 100644 --- a/Sources/WalletConnectUtils/JsonRpcRecord.swift +++ b/Sources/WalletConnectUtils/JsonRpcRecord.swift @@ -5,12 +5,25 @@ public struct JsonRpcRecord: Codable { public let id: Int64 public let topic: String public let request: Request - public var response: JsonRpcResponseTypes? + public var response: JsonRpcResult? public let chainId: String? + public init(id: Int64, topic: String, request: JsonRpcRecord.Request, response: JsonRpcResult? = nil, chainId: String?) { + self.id = id + self.topic = topic + self.request = request + self.response = response + self.chainId = chainId + } + public struct Request: Codable { public let method: String public let params: AnyCodable + + public init(method: String, params: AnyCodable) { + self.method = method + self.params = params + } } } diff --git a/Tests/IntegrationTests/ClientDelegate.swift b/Tests/IntegrationTests/ClientDelegate.swift index fa87b54cd..f5eacaeb0 100644 --- a/Tests/IntegrationTests/ClientDelegate.swift +++ b/Tests/IntegrationTests/ClientDelegate.swift @@ -8,6 +8,7 @@ class ClientDelegate: WalletConnectClientDelegate { var onPairingSettled: ((Pairing)->())? var onSessionProposal: ((Session.Proposal)->())? var onSessionRequest: ((Request)->())? + var onSessionResponse: ((Response)->())? var onSessionRejected: ((String, Reason)->())? var onSessionDelete: (()->())? var onSessionUpgrade: ((String, Session.Permissions)->())? @@ -50,4 +51,7 @@ class ClientDelegate: WalletConnectClientDelegate { func didUpdate(pairingTopic: String, appMetadata: AppMetadata) { onPairingUpdate?(pairingTopic, appMetadata) } + func didReceive(sessionResponse: Response) { + onSessionResponse?(sessionResponse) + } } diff --git a/Tests/IntegrationTests/ClientTest.swift b/Tests/IntegrationTests/ClientTest.swift index cb56eff3c..ecf9a5a83 100644 --- a/Tests/IntegrationTests/ClientTest.swift +++ b/Tests/IntegrationTests/ClientTest.swift @@ -153,15 +153,16 @@ final class ClientTests: XCTestCase { } proposer.onSessionSettled = {[unowned self] settledSession in let requestParams = Request(id: 0, topic: settledSession.topic, method: method, params: AnyCodable(params), chainId: nil) - self.proposer.client.request(params: requestParams) { result in - switch result { - case .success(let jsonRpcResponse): - let response = try! jsonRpcResponse.result.get(String.self) - XCTAssertEqual(response, responseParams) - responseExpectation.fulfill() - case .failure(_): - XCTFail() - } + self.proposer.client.request(params: requestParams) + } + proposer.onSessionResponse = { response in + switch response.result { + case .response(let jsonRpcResponse): + let response = try! jsonRpcResponse.result.get(String.self) + XCTAssertEqual(response, responseParams) + responseExpectation.fulfill() + case .error(_): + XCTFail() } } responder.onSessionRequest = {[unowned self] sessionRequest in @@ -189,15 +190,17 @@ final class ClientTests: XCTestCase { } proposer.onSessionSettled = {[unowned self] settledSession in let requestParams = Request(id: 0, topic: settledSession.topic, method: method, params: AnyCodable(params), chainId: nil) - self.proposer.client.request(params: requestParams) { result in - switch result { - case .success(_): - XCTFail() - case .failure(let errorResponse): - XCTAssertEqual(error, errorResponse.error) - failureResponseExpectation.fulfill() - } + self.proposer.client.request(params: requestParams) + } + proposer.onSessionResponse = { response in + switch response.result { + case .response(_): + XCTFail() + case .error(let errorResponse): + XCTAssertEqual(error, errorResponse.error) + failureResponseExpectation.fulfill() } + } responder.onSessionRequest = {[unowned self] sessionRequest in let jsonrpcErrorResponse = JSONRPCErrorResponse(id: sessionRequest.id, error: error) @@ -251,7 +254,7 @@ final class ClientTests: XCTestCase { self.responder.client.approve(proposal: proposal, accounts: [account]) } responder.onSessionSettled = { [unowned self] sessionSettled in - responder.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions) + try? responder.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions) } proposer.onSessionUpgrade = { topic, permissions in XCTAssertTrue(permissions.blockchains.isSuperset(of: upgradePermissions.blockchains)) @@ -266,31 +269,6 @@ final class ClientTests: XCTestCase { waitForExpectations(timeout: defaultTimeout, handler: nil) } - func testSessionUpgradeFailsOnNonControllerRequest() { - let proposerSessionUpgradeExpectation = expectation(description: "Proposer upgrades session") - proposerSessionUpgradeExpectation.isInverted = true - let responderSessionUpgradeExpectation = expectation(description: "Responder upgrades session") - responderSessionUpgradeExpectation.isInverted = true - let account = "0x022c0c42a80bd19EA4cF0F94c4F9F96645759716" - let permissions = Session.Permissions.stub() - let upgradePermissions = Session.Permissions(blockchains: ["eip155:42"], methods: ["eth_sendTransaction"]) - let uri = try! proposer.client.connect(sessionPermissions: permissions)! - try! responder.client.pair(uri: uri) - responder.onSessionProposal = { [unowned self] proposal in - self.responder.client.approve(proposal: proposal, accounts: [account]) - } - proposer.onSessionSettled = { [unowned self] sessionSettled in - proposer.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions) - } - proposer.onSessionUpgrade = { topic, permissions in - proposerSessionUpgradeExpectation.fulfill() - } - responder.onSessionUpgrade = { topic, permissions in - responderSessionUpgradeExpectation.fulfill() - } - waitForExpectations(timeout: 3.0, handler: nil) - } - func testSuccessfulSessionUpdate() { let proposerSessionUpdateExpectation = expectation(description: "Proposer updates session on responder request") let responderSessionUpdateExpectation = expectation(description: "Responder updates session on proposer response") diff --git a/Tests/WalletConnectTests/Helpers/Error+Extension.swift b/Tests/WalletConnectTests/Helpers/Error+Extension.swift index d21783353..25845a8cb 100644 --- a/Tests/WalletConnectTests/Helpers/Error+Extension.swift +++ b/Tests/WalletConnectTests/Helpers/Error+Extension.swift @@ -7,3 +7,30 @@ extension NSError { NSError(domain: "com.walletconnect.sdk.tests.error", code: code, userInfo: nil) } } + +extension Error { + + var wcError: WalletConnectError? { + self as? WalletConnectError + } + + var isNoSessionMatchingTopicError: Bool { + guard case .noSessionMatchingTopic = wcError else { return false } + return true + } + + var isSessionNotSettledError: Bool { + guard case .sessionNotSettled = wcError else { return false } + return true + } + + var isInvalidPermissionsError: Bool { + guard case .invalidPermissions = wcError else { return false } + return true + } + + var isUnauthorizedNonControllerCallError: Bool { + guard case .unauthorizedNonControllerCall = wcError else { return false } + return true + } +} diff --git a/Tests/WalletConnectTests/Helpers/WCRequest+Extension.swift b/Tests/WalletConnectTests/Helpers/WCRequest+Extension.swift new file mode 100644 index 000000000..f68c09faf --- /dev/null +++ b/Tests/WalletConnectTests/Helpers/WCRequest+Extension.swift @@ -0,0 +1,19 @@ +@testable import WalletConnect + +extension WCRequest { + + var pairingApproveParams: PairingType.ApprovalParams? { + guard case .pairingApprove(let approveParams) = self.params else { return nil } + return approveParams + } + + var sessionProposal: SessionProposal? { + guard case .pairingPayload(let payload) = self.params else { return nil } + return payload.request.params + } + + var approveParams: SessionType.ApproveParams? { + guard case .sessionApprove(let approveParams) = self.params else { return nil } + return approveParams + } +} diff --git a/Tests/WalletConnectTests/JSONRPCSerialiserTests.swift b/Tests/WalletConnectTests/JSONRPCSerialiserTests.swift index 2e4740752..f2d4f212b 100644 --- a/Tests/WalletConnectTests/JSONRPCSerialiserTests.swift +++ b/Tests/WalletConnectTests/JSONRPCSerialiserTests.swift @@ -5,33 +5,33 @@ import Foundation import XCTest @testable import WalletConnect -final class JSONRPCSerialiserTests: XCTestCase { - var serialiser: JSONRPCSerialiser! +final class JSONRPCSerializerTests: XCTestCase { + var serializer: JSONRPCSerializer! var codec: MockedCodec! override func setUp() { codec = MockedCodec() - self.serialiser = JSONRPCSerialiser(crypto: Crypto(keychain: KeychainStorageMock()), codec: codec) + self.serializer = JSONRPCSerializer(crypto: Crypto(keychain: KeychainStorageMock()), codec: codec) } override func tearDown() { - serialiser = nil + serializer = nil } - func testSerialise() { - codec.encryptionPayload = EncryptionPayload(iv: SerialiserTestData.iv, - publicKey: SerialiserTestData.publicKey, - mac: SerialiserTestData.mac, - cipherText: SerialiserTestData.cipherText) - let serialisedMessage = try! serialiser.encrypt(json: SerialiserTestData.pairingApproveJSON, agreementKeys: SerialiserTestData.emptyAgreementSecret) - let serialisedMessageSample = SerialiserTestData.serialisedMessage - XCTAssertEqual(serialisedMessage, serialisedMessageSample) + func testSerialize() { + codec.encryptionPayload = EncryptionPayload(iv: SerializerTestData.iv, + publicKey: SerializerTestData.publicKey, + mac: SerializerTestData.mac, + cipherText: SerializerTestData.cipherText) + let serializedMessage = try! serializer.encrypt(json: SerializerTestData.pairingApproveJSON, agreementKeys: SerializerTestData.emptyAgreementSecret) + let serializedMessageSample = SerializerTestData.serializedMessage + XCTAssertEqual(serializedMessage, serializedMessageSample) } - func testDeserialise() { - let serialisedMessageSample = SerialiserTestData.serialisedMessage - codec.decodedJson = SerialiserTestData.pairingApproveJSON - let deserialisedJSONRPC: WCRequest = try! serialiser.deserialise(message: serialisedMessageSample, symmetricKey: Data(hex: "")) - XCTAssertEqual(deserialisedJSONRPC.params, SerialiserTestData.pairingApproveJSONRPCRequest.params) + func testDeserialize() { + let serializedMessageSample = SerializerTestData.serializedMessage + codec.decodedJson = SerializerTestData.pairingApproveJSON + let deserializedJSONRPC: WCRequest = try! serializer.deserialize(message: serializedMessageSample, symmetricKey: Data(hex: "")) + XCTAssertEqual(deserializedJSONRPC.params, SerializerTestData.pairingApproveJSONRPCRequest.params) } } diff --git a/Tests/WalletConnectTests/JsonRpcHistoryTests.swift b/Tests/WalletConnectTests/JsonRpcHistoryTests.swift index c807c1b46..34895bafb 100644 --- a/Tests/WalletConnectTests/JsonRpcHistoryTests.swift +++ b/Tests/WalletConnectTests/JsonRpcHistoryTests.swift @@ -36,7 +36,7 @@ final class JsonRpcHistoryTests: XCTestCase { try! sut.set(topic: recordinput.topic, request: recordinput.request) XCTAssertNil(sut.get(id: recordinput.request.id)?.response) let jsonRpcResponse = JSONRPCResponse(id: recordinput.request.id, result: AnyCodable("")) - let response = JsonRpcResponseTypes.response(jsonRpcResponse) + let response = JsonRpcResult.response(jsonRpcResponse) _ = try! sut.resolve(response: response) XCTAssertNotNil(sut.get(id: jsonRpcResponse.id)?.response) } @@ -45,7 +45,7 @@ final class JsonRpcHistoryTests: XCTestCase { let recordinput = getTestJsonRpcRecordInput() try! sut.set(topic: recordinput.topic, request: recordinput.request) let jsonRpcResponse = JSONRPCResponse(id: recordinput.request.id, result: AnyCodable("")) - let response = JsonRpcResponseTypes.response(jsonRpcResponse) + let response = JsonRpcResult.response(jsonRpcResponse) _ = try! sut.resolve(response: response) XCTAssertThrowsError(try sut.resolve(response: response)) } @@ -71,7 +71,7 @@ final class JsonRpcHistoryTests: XCTestCase { try! sut.set(topic: recordinput2.topic, request: recordinput2.request) XCTAssertEqual(sut.getPending().count, 2) let jsonRpcResponse = JSONRPCResponse(id: recordinput1.request.id, result: AnyCodable("")) - let response = JsonRpcResponseTypes.response(jsonRpcResponse) + let response = JsonRpcResult.response(jsonRpcResponse) _ = try! sut.resolve(response: response) XCTAssertEqual(sut.getPending().count, 1) } diff --git a/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerialiser.swift b/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerialiser.swift deleted file mode 100644 index 8e7a25968..000000000 --- a/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerialiser.swift +++ /dev/null @@ -1,39 +0,0 @@ -// - -import Foundation -import WalletConnectUtils -@testable import WalletConnect - -class MockedJSONRPCSerialiser: JSONRPCSerialising { - - var codec: Codec - var deserialised: Any! - var serialised: String! - - init(codec: Codec = MockedCodec()) { - self.codec = codec - } - - func serialise(topic: String, encodable: Encodable) throws -> String { - try serialise(json: try encodable.json(), agreementKeys: AgreementSecret(sharedSecret: Data(), publicKey: AgreementPrivateKey().publicKey)) - } - func tryDeserialise(topic: String, message: String) -> T? { - try? deserialise(message: message, symmetricKey: Data()) - } - func deserialiseJsonRpc(topic: String, message: String) throws -> Result, JSONRPCErrorResponse> { - .success(try deserialise(message: message, symmetricKey: Data())) - } - - func deserialise(message: String, symmetricKey: Data) throws -> T where T : Codable { - if let deserialisedModel = deserialised as? T { - return deserialisedModel - } else { - throw NSError.mock() - } - } - - func serialise(json: String, agreementKeys: AgreementSecret) throws -> String { - return serialised - } - -} diff --git a/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerializer.swift b/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerializer.swift new file mode 100644 index 000000000..fbc8c9da7 --- /dev/null +++ b/Tests/WalletConnectTests/Mocks/MockedJSONRPCSerializer.swift @@ -0,0 +1,39 @@ +// + +import Foundation +import WalletConnectUtils +@testable import WalletConnect + +class MockedJSONRPCSerializer: JSONRPCSerializing { + + var codec: Codec + var deserialized: Any! + var serialized: String! + + init(codec: Codec = MockedCodec()) { + self.codec = codec + } + + func serialize(topic: String, encodable: Encodable) throws -> String { + try serialize(json: try encodable.json(), agreementKeys: AgreementSecret(sharedSecret: Data(), publicKey: AgreementPrivateKey().publicKey)) + } + func tryDeserialize(topic: String, message: String) -> T? { + try? deserialize(message: message, symmetricKey: Data()) + } + func deserializeJsonRpc(topic: String, message: String) throws -> Result, JSONRPCErrorResponse> { + .success(try deserialize(message: message, symmetricKey: Data())) + } + + func deserialize(message: String, symmetricKey: Data) throws -> T where T : Codable { + if let deserializedModel = deserialized as? T { + return deserializedModel + } else { + throw NSError.mock() + } + } + + func serialize(json: String, agreementKeys: AgreementSecret) throws -> String { + return serialized + } + +} diff --git a/Tests/WalletConnectTests/Mocks/MockedRelay.swift b/Tests/WalletConnectTests/Mocks/MockedRelay.swift index 171f51770..ff3fdf2bd 100644 --- a/Tests/WalletConnectTests/Mocks/MockedRelay.swift +++ b/Tests/WalletConnectTests/Mocks/MockedRelay.swift @@ -20,24 +20,36 @@ class MockedWCRelay: WalletConnectRelaying { var wcRequestPublisher: AnyPublisher { wcRequestPublisherSubject.eraseToAnyPublisher() } + var didCallRequest = false var didCallSubscribe = false var didCallUnsubscribe = false + var didRespondSuccess = false + var lastErrorCode = -1 var error: Error? = nil private(set) var requests: [(topic: String, request: WCRequest)] = [] - + func request(_ wcMethod: WCMethod, onTopic topic: String, completion: ((Result, JSONRPCErrorResponse>) -> ())?) { request(topic: topic, payload: wcMethod.asRequest(), completion: completion) } func request(topic: String, payload: WCRequest, completion: ((Result, JSONRPCErrorResponse>) -> ())?) { + didCallRequest = true requests.append((topic, payload)) } - func respond(topic: String, response: JsonRpcResponseTypes, completion: @escaping ((Error?) -> ())) { + func respond(topic: String, response: JsonRpcResult, completion: @escaping ((Error?) -> ())) { completion(error) } + func respondSuccess(for payload: WCRequestSubscriptionPayload) { + didRespondSuccess = true + } + + func respondError(for payload: WCRequestSubscriptionPayload, reason: ReasonCode) { + lastErrorCode = reason.code + } + func subscribe(topic: String) { didCallSubscribe = true } @@ -48,7 +60,7 @@ class MockedWCRelay: WalletConnectRelaying { func sendSubscriptionPayloadOn(topic: String) { let payload = WCRequestSubscriptionPayload(topic: topic, - wcRequest: SerialiserTestData.pairingApproveJSONRPCRequest) + wcRequest: SerializerTestData.pairingApproveJSONRPCRequest) wcRequestPublisherSubject.send(payload) } } diff --git a/Tests/WalletConnectTests/Mocks/SessionSequenceStorageMock.swift b/Tests/WalletConnectTests/Mocks/SessionSequenceStorageMock.swift index 64852e30d..a799ace1d 100644 --- a/Tests/WalletConnectTests/Mocks/SessionSequenceStorageMock.swift +++ b/Tests/WalletConnectTests/Mocks/SessionSequenceStorageMock.swift @@ -1,4 +1,5 @@ @testable import WalletConnect +import Foundation final class SessionSequenceStorageMock: SessionSequenceStorage { @@ -14,8 +15,8 @@ final class SessionSequenceStorageMock: SessionSequenceStorage { sessions[sequence.topic] = sequence } - func getSequence(forTopic topic: String) throws -> SessionSequence? { - sessions[topic] + func getSequence(forTopic topic: String) -> SessionSequence? { + return sessions[topic] } func getAll() -> [SessionSequence] { diff --git a/Tests/WalletConnectTests/PairingEngineTests.swift b/Tests/WalletConnectTests/PairingEngineTests.swift index 2796adcd0..6f0369b2f 100644 --- a/Tests/WalletConnectTests/PairingEngineTests.swift +++ b/Tests/WalletConnectTests/PairingEngineTests.swift @@ -3,24 +3,6 @@ import XCTest import TestingUtils import WalletConnectUtils -fileprivate extension SessionPermissions { - static func stub() -> SessionPermissions { - SessionPermissions( - blockchain: Blockchain(chains: []), - jsonrpc: JSONRPC(methods: []), - notifications: Notifications(types: []) - ) - } -} - -fileprivate extension WCRequest { - - var approveParams: PairingType.ApprovalParams? { - guard case .pairingApprove(let approveParams) = self.params else { return nil } - return approveParams - } -} - func deriveTopic(publicKey: String, privateKey: AgreementPrivateKey) -> String { try! Crypto.generateAgreementSecret(from: privateKey, peerPublicKey: publicKey).derivedTopic() } @@ -88,7 +70,7 @@ final class PairingEngineTests: XCTestCase { try engine.approve(uri) // The concept of "publish" should only be known by the relayer - guard let publishTopic = relayMock.requests.first?.topic, let approval = relayMock.requests.first?.request.approveParams else { + guard let publishTopic = relayMock.requests.first?.topic, let approval = relayMock.requests.first?.request.pairingApproveParams else { XCTFail("Responder must publish an approval request."); return } @@ -124,7 +106,7 @@ final class PairingEngineTests: XCTestCase { try engine.approve(uri) let success = JSONRPCResponse(id: 0, result: AnyCodable(true)) - let response = WCResponse(topic: topicA, requestMethod: .pairingApprove, requestParams: .pairingApprove(PairingType.ApprovalParams(relay: RelayProtocolOptions(protocol: "", params: nil), responder: PairingParticipant(publicKey: ""), expiry: 0, state: nil)), result: .success(success)) + let response = WCResponse(topic: topicA, chainId: nil, requestMethod: .pairingApprove, requestParams: .pairingApprove(PairingType.ApprovalParams(relay: RelayProtocolOptions(protocol: "", params: nil), responder: PairingParticipant(publicKey: ""), expiry: 0, state: nil)), result: .response(success)) relayMock.onPairingResponse?(response) XCTAssert(storageMock.hasAcknowledgedPairing(on: topicB), "Settled pairing must advance to acknowledged state.") diff --git a/Tests/WalletConnectTests/SessionEngineTests.swift b/Tests/WalletConnectTests/SessionEngineTests.swift index 5dd35b8b0..9c480ad1a 100644 --- a/Tests/WalletConnectTests/SessionEngineTests.swift +++ b/Tests/WalletConnectTests/SessionEngineTests.swift @@ -3,37 +3,6 @@ import WalletConnectUtils import TestingUtils @testable import WalletConnect -// TODO: Move common helper methods to a shared folder -fileprivate extension Pairing { - - static func stub() -> Pairing { - Pairing(topic: String.generateTopic()!, peer: nil) - } -} - -fileprivate extension SessionPermissions { - static func stub() -> SessionPermissions { - SessionPermissions( - blockchain: Blockchain(chains: []), - jsonrpc: JSONRPC(methods: []), - notifications: Notifications(types: []) - ) - } -} - -fileprivate extension WCRequest { - - var sessionProposal: SessionProposal? { - guard case .pairingPayload(let payload) = self.params else { return nil } - return payload.request.params - } - - var approveParams: SessionType.ApproveParams? { - guard case .sessionApprove(let approveParams) = self.params else { return nil } - return approveParams - } -} - final class SessionEngineTests: XCTestCase { var engine: SessionEngine! @@ -54,9 +23,20 @@ final class SessionEngineTests: XCTestCase { storageMock = SessionSequenceStorageMock() cryptoMock = CryptoStorageProtocolMock() topicGenerator = TopicGenerator() - + } + + override func tearDown() { + relayMock = nil + subscriberMock = nil + storageMock = nil + cryptoMock = nil + topicGenerator = nil + engine = nil + } + + func setupEngine(isController: Bool) { metadata = AppMetadata(name: nil, description: nil, url: nil, icons: nil) - isController = false + self.isController = isController let logger = ConsoleLoggerMock() engine = SessionEngine( relay: relayMock, @@ -68,17 +48,10 @@ final class SessionEngineTests: XCTestCase { logger: logger, topicGenerator: topicGenerator.getTopic) } - - override func tearDown() { - relayMock = nil - subscriberMock = nil - storageMock = nil - cryptoMock = nil - topicGenerator = nil - engine = nil - } func testPropose() { + setupEngine(isController: false) + let pairing = Pairing.stub() let topicB = pairing.topic @@ -104,6 +77,7 @@ final class SessionEngineTests: XCTestCase { } func testProposeResponseFailure() { + setupEngine(isController: false) let pairing = Pairing.stub() let topicB = pairing.topic @@ -121,9 +95,10 @@ final class SessionEngineTests: XCTestCase { let error = JSONRPCErrorResponse(id: request.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")) let response = WCResponse( topic: publishTopic, + chainId: nil, requestMethod: request.method, requestParams: request.params, - result: .failure(error)) + result: .error(error)) relayMock.onResponse?(response) XCTAssert(subscriberMock.didUnsubscribe(to: topicC)) @@ -133,6 +108,7 @@ final class SessionEngineTests: XCTestCase { } func testApprove() { + setupEngine(isController: true) let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let topicB = String.generateTopic()! let topicC = String.generateTopic()! @@ -163,6 +139,8 @@ final class SessionEngineTests: XCTestCase { } func testApprovalAcknowledgementSuccess() { + setupEngine(isController: true) + let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let topicB = String.generateTopic()! let topicC = String.generateTopic()! @@ -188,9 +166,10 @@ final class SessionEngineTests: XCTestCase { let success = JSONRPCResponse(id: request.id, result: AnyCodable(true)) let response = WCResponse( topic: publishTopic, + chainId: nil, requestMethod: request.method, requestParams: request.params, - result: .success(success)) + result: .response(success)) relayMock.onResponse?(response) XCTAssertFalse(cryptoMock.hasAgreementSecret(for: topicC)) @@ -199,6 +178,8 @@ final class SessionEngineTests: XCTestCase { } func testApprovalAcknowledgementFailure() { + setupEngine(isController: true) + let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let selfPubKey = cryptoMock.privateKeyStub.publicKey.hexRepresentation let topicB = String.generateTopic()! @@ -225,9 +206,10 @@ final class SessionEngineTests: XCTestCase { let error = JSONRPCErrorResponse(id: request.id, error: JSONRPCErrorResponse.Error(code: 0, message: "")) let response = WCResponse( topic: publishTopic, + chainId: nil, requestMethod: request.method, requestParams: request.params, - result: .failure(error)) + result: .error(error)) relayMock.onResponse?(response) XCTAssertFalse(cryptoMock.hasPrivateKey(for: selfPubKey)) @@ -241,6 +223,7 @@ final class SessionEngineTests: XCTestCase { } func testReceiveApprovalResponse() { + setupEngine(isController: false) var approvedSession: Session? @@ -278,4 +261,209 @@ final class SessionEngineTests: XCTestCase { XCTAssertNotNil(approvedSession) XCTAssertEqual(approvedSession?.topic, topicD) } + + // MARK: - Update call tests + + func testUpdateSuccess() throws { + setupEngine(isController: true) + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + try engine.update(topic: session.topic, accounts: ["std:0:0"]) + XCTAssertTrue(relayMock.didCallRequest) + } + + func testUpdateErrorInvalidAccount() { + setupEngine(isController: true) + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["err"])) + } + + func testUpdateErrorIfNonController() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["std:0:0"]), "Update must fail if called by a non-controller.") + } + + func testUpdateErrorSessionNotFound() { + setupEngine(isController: true) + XCTAssertThrowsError(try engine.update(topic: "", accounts: ["std:0:0"]), "Update must fail if there is no session matching the target topic.") + } + + func testUpdateErrorSessionNotSettled() { + setupEngine(isController: true) + let session = SessionSequence.stubPreSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.update(topic: session.topic, accounts: ["std:0:0"]), "Update must fail if session is not on settled state.") + } + + // MARK: - Update peer response tests + + func testUpdatePeerSuccess() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) + XCTAssertTrue(relayMock.didRespondSuccess) + } + + func testUpdatePeerErrorAccountInvalid() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic, accounts: ["0"])) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 1003) + } + + func testUpdatePeerErrorNoSession() { + setupEngine(isController: false) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: "")) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 1301) + } + + func testUpdatePeerErrorSessionNotSettled() { + setupEngine(isController: false) + let session = SessionSequence.stubPreSettled(isPeerController: true) // Session is not fully settled + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3003) + } + + func testUpdatePeerErrorUnauthorized() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled() // Peer is not a controller + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3003) + } + + func testUpdatePeerErrorMatchingController() { + setupEngine(isController: true) // Update request received by a controller + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpdate(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3005) + } + + // TODO: Update acknowledgement tests + + // MARK: - Upgrade call tests + + func testUpgradeSuccess() throws { + setupEngine(isController: true) + let permissions = Session.Permissions.stub() + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + try engine.upgrade(topic: session.topic, permissions: permissions) + XCTAssertTrue(relayMock.didCallRequest) + // TODO: Check permissions on stored session + } + + func testUpgradeErrorSessionNotFound() { + setupEngine(isController: true) + XCTAssertThrowsError(try engine.upgrade(topic: "", permissions: Session.Permissions.stub())) { error in + XCTAssertTrue(error.isNoSessionMatchingTopicError) + } + } + + func testUpgradeErrorSessionNotSettled() { + setupEngine(isController: true) + let session = SessionSequence.stubPreSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub())) { error in + XCTAssertTrue(error.isSessionNotSettledError) + } + } + + func testUpgradeErrorInvalidPermissions() { + setupEngine(isController: true) + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub(chains: [""]))) { error in + XCTAssertTrue(error.isInvalidPermissionsError) + } + XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub(methods: [""]))) { error in + XCTAssertTrue(error.isInvalidPermissionsError) + } + XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub(notifications: [""]))) { error in + XCTAssertTrue(error.isInvalidPermissionsError) + } + } + + func testUpgradeErrorCalledByNonController() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled() + storageMock.setSequence(session) + XCTAssertThrowsError(try engine.upgrade(topic: session.topic, permissions: Session.Permissions.stub())) { error in + XCTAssertTrue(error.isUnauthorizedNonControllerCallError) + } + } + + // MARK: - Upgrade peer response tests + + func testUpgradePeerSuccess() { + setupEngine(isController: false) + var didCallbackUpgrade = false + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + engine.onSessionUpgrade = { topic, _ in + didCallbackUpgrade = true + XCTAssertEqual(topic, session.topic) + } + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) + XCTAssertTrue(didCallbackUpgrade) + XCTAssertTrue(relayMock.didRespondSuccess) + } + + func testUpgradePeerErrorInvalidPermissions() { + setupEngine(isController: false) + let invalidPermissions = SessionPermissions.stub(chains: [""]) + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic, permissions: invalidPermissions)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 1004) + } + + func testUpgradePeerErrorSessionNotFound() { + setupEngine(isController: false) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: "")) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 1301) + } + + func testUpgradePeerErrorSessionNotSettled() { + setupEngine(isController: false) + let session = SessionSequence.stubPreSettled(isPeerController: true) // Session is not fully settled + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3004) + } + + func testUpgradePeerErrorUnauthorized() { + setupEngine(isController: false) + let session = SessionSequence.stubSettled() // Peer is not a controller + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3004) + } + + func testUpgradePeerErrorMatchingController() { + setupEngine(isController: true) // Upgrade request received by a controller + let session = SessionSequence.stubSettled(isPeerController: true) + storageMock.setSequence(session) + subscriberMock.onReceivePayload?(WCRequestSubscriptionPayload.stubUpgrade(topic: session.topic)) + XCTAssertFalse(relayMock.didRespondSuccess) + XCTAssertEqual(relayMock.lastErrorCode, 3005) + } + + // TODO: Upgrade acknowledgement tests } diff --git a/Tests/WalletConnectTests/Mocks/AgreementSecret+Stub.swift b/Tests/WalletConnectTests/Stub/AgreementSecret+Stub.swift similarity index 100% rename from Tests/WalletConnectTests/Mocks/AgreementSecret+Stub.swift rename to Tests/WalletConnectTests/Stub/AgreementSecret+Stub.swift diff --git a/Tests/WalletConnectTests/Stub/Session+Stub.swift b/Tests/WalletConnectTests/Stub/Session+Stub.swift new file mode 100644 index 000000000..cc67dacf8 --- /dev/null +++ b/Tests/WalletConnectTests/Stub/Session+Stub.swift @@ -0,0 +1,39 @@ +import Foundation +@testable import WalletConnect + +extension SessionSequence { + + static func stubPreSettled(isPeerController: Bool = false) -> SessionSequence { + let peerKey = AgreementPrivateKey().publicKey.hexRepresentation + let permissions = isPeerController ? SessionPermissions.stub(controllerKey: peerKey) : SessionPermissions.stub() + return SessionSequence( + topic: String.generateTopic()!, + relay: RelayProtocolOptions.stub(), + selfParticipant: Participant.stub(), + expiryDate: Date.distantFuture, + settledState: Settled( + peer: Participant.stub(publicKey: peerKey), + permissions: permissions, + state: SessionState(accounts: ["chainstd:1:1"]), + status: .preSettled + ) + ) + } + + static func stubSettled(isPeerController: Bool = false) -> SessionSequence { + let peerKey = AgreementPrivateKey().publicKey.hexRepresentation + let permissions = isPeerController ? SessionPermissions.stub(controllerKey: peerKey) : SessionPermissions.stub() + return SessionSequence( + topic: String.generateTopic()!, + relay: RelayProtocolOptions.stub(), + selfParticipant: Participant.stub(), + expiryDate: Date.distantFuture, + settledState: Settled( + peer: Participant.stub(publicKey: peerKey), + permissions: permissions, + state: SessionState(accounts: ["chainstd:1:1"]), + status: .acknowledged + ) + ) + } +} diff --git a/Tests/WalletConnectTests/Stub/Stubs.swift b/Tests/WalletConnectTests/Stub/Stubs.swift new file mode 100644 index 000000000..11b405f85 --- /dev/null +++ b/Tests/WalletConnectTests/Stub/Stubs.swift @@ -0,0 +1,72 @@ +@testable import WalletConnect + +extension AppMetadata { + static func stub() -> AppMetadata { + AppMetadata( + name: "Wallet Connect", + description: "A protocol to connect blockchain wallets to dapps.", + url: "https://walletconnect.com/", + icons: [] + ) + } +} + +extension Pairing { + static func stub() -> Pairing { + Pairing(topic: String.generateTopic()!, peer: nil) + } +} + +extension Session.Permissions { + static func stub( + chains: Set = ["solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"], + methods: Set = ["getGenesisHash"], + notifications: [String] = ["msg"] + ) -> Session.Permissions { + Session.Permissions( + blockchains: chains, + methods: methods, + notifications: notifications + ) + } +} + +extension SessionPermissions { + static func stub( + chains: Set = ["eip155:1"], + jsonrpc: Set = ["eth_sign"], + notifications: [String] = ["a_type"], + controllerKey: String = AgreementPrivateKey().publicKey.hexRepresentation + ) -> SessionPermissions { + return SessionPermissions( + blockchain: Blockchain(chains: chains), + jsonrpc: JSONRPC(methods: jsonrpc), + notifications: Notifications(types: notifications), + controller: Controller(publicKey: controllerKey) + ) + } +} + +extension RelayProtocolOptions { + static func stub() -> RelayProtocolOptions { + RelayProtocolOptions(protocol: "", params: nil) + } +} + +extension Participant { + static func stub(publicKey: String = AgreementPrivateKey().publicKey.hexRepresentation) -> Participant { + Participant(publicKey: publicKey, metadata: AppMetadata.stub()) + } +} + +extension WCRequestSubscriptionPayload { + static func stubUpdate(topic: String, accounts: Set = ["std:0:0"]) -> WCRequestSubscriptionPayload { + let updateMethod = WCMethod.wcSessionUpdate(SessionType.UpdateParams(accounts: accounts)).asRequest() + return WCRequestSubscriptionPayload(topic: topic, wcRequest: updateMethod) + } + + static func stubUpgrade(topic: String, permissions: SessionPermissions = SessionPermissions(permissions: Session.Permissions.stub())) -> WCRequestSubscriptionPayload { + let upgradeMethod = WCMethod.wcSessionUpgrade(SessionType.UpgradeParams(permissions: permissions)).asRequest() + return WCRequestSubscriptionPayload(topic: topic, wcRequest: upgradeMethod) + } +} diff --git a/Tests/WalletConnectTests/Mocks/WalletConnectURI+Stub.swift b/Tests/WalletConnectTests/Stub/WalletConnectURI+Stub.swift similarity index 100% rename from Tests/WalletConnectTests/Mocks/WalletConnectURI+Stub.swift rename to Tests/WalletConnectTests/Stub/WalletConnectURI+Stub.swift diff --git a/Tests/WalletConnectTests/TestsData/SerialiserTestData.swift b/Tests/WalletConnectTests/TestsData/SerialiserTestData.swift index f2f4d91e1..452c9eed1 100644 --- a/Tests/WalletConnectTests/TestsData/SerialiserTestData.swift +++ b/Tests/WalletConnectTests/TestsData/SerialiserTestData.swift @@ -3,14 +3,14 @@ import Foundation @testable import WalletConnect -enum SerialiserTestData { +enum SerializerTestData { static let emptyAgreementSecret = AgreementSecret(sharedSecret: Data(hex: ""), publicKey: AgreementPrivateKey().publicKey) static let iv = Data(hex: "f0d00d4274a7e9711e4e0f21820b8877") static let publicKey = Data(hex: "45c59ad0c053925072f4503a39fe579ca8b7b8fa6bf0c7297e6db8f6585ee77f") static let mac = Data(hex: "fc6d3106fa827043279f9db08cd2e29a988c7272fa3cfdb739163bb9606822c7") static let cipherText = Data(hex: "14aa7f6034dd0213be5901b472f461769855ac1e2f6bec6a8ed1157a9da3b2df08802cbd6e0d030d86ff99011040cfc831eec3636c1d46bfc22cbe055560fea3") - static let serialisedMessage = "f0d00d4274a7e9711e4e0f21820b887745c59ad0c053925072f4503a39fe579ca8b7b8fa6bf0c7297e6db8f6585ee77ffc6d3106fa827043279f9db08cd2e29a988c7272fa3cfdb739163bb9606822c714aa7f6034dd0213be5901b472f461769855ac1e2f6bec6a8ed1157a9da3b2df08802cbd6e0d030d86ff99011040cfc831eec3636c1d46bfc22cbe055560fea3" + static let serializedMessage = "f0d00d4274a7e9711e4e0f21820b887745c59ad0c053925072f4503a39fe579ca8b7b8fa6bf0c7297e6db8f6585ee77ffc6d3106fa827043279f9db08cd2e29a988c7272fa3cfdb739163bb9606822c714aa7f6034dd0213be5901b472f461769855ac1e2f6bec6a8ed1157a9da3b2df08802cbd6e0d030d86ff99011040cfc831eec3636c1d46bfc22cbe055560fea3" static let pairingApproveJSONRPCRequest = WCRequest( id: 0, jsonrpc: "2.0", diff --git a/Tests/WalletConnectTests/WCRelayTests.swift b/Tests/WalletConnectTests/WCRelayTests.swift index 37ead33db..50e072d5d 100644 --- a/Tests/WalletConnectTests/WCRelayTests.swift +++ b/Tests/WalletConnectTests/WCRelayTests.swift @@ -9,22 +9,22 @@ import WalletConnectUtils class WalletConnectRelayTests: XCTestCase { var wcRelay: WalletConnectRelay! var networkRelayer: MockedNetworkRelayer! - var serialiser: MockedJSONRPCSerialiser! + var serializer: MockedJSONRPCSerializer! var crypto: Crypto! private var publishers = [AnyCancellable]() override func setUp() { let logger = ConsoleLoggerMock() - serialiser = MockedJSONRPCSerialiser() + serializer = MockedJSONRPCSerializer() networkRelayer = MockedNetworkRelayer() - wcRelay = WalletConnectRelay(networkRelayer: networkRelayer, jsonRpcSerialiser: serialiser, logger: logger, jsonRpcHistory: JsonRpcHistory(logger: logger, keyValueStore: KeyValueStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) + wcRelay = WalletConnectRelay(networkRelayer: networkRelayer, jsonRpcSerializer: serializer, logger: logger, jsonRpcHistory: JsonRpcHistory(logger: logger, keyValueStore: KeyValueStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) } override func tearDown() { wcRelay = nil networkRelayer = nil - serialiser = nil + serializer = nil } func testNotifiesOnEncryptedWCJsonRpcRequest() { @@ -33,7 +33,7 @@ class WalletConnectRelayTests: XCTestCase { wcRelay.wcRequestPublisher.sink { (request) in requestExpectation.fulfill() }.store(in: &publishers) - serialiser.deserialised = SerialiserTestData.pairingApproveJSONRPCRequest + serializer.deserialized = SerializerTestData.pairingApproveJSONRPCRequest networkRelayer.onMessage?(topic, testPayload) waitForExpectations(timeout: 1.001, handler: nil) } @@ -43,8 +43,8 @@ class WalletConnectRelayTests: XCTestCase { let topic = "93293932" let request = getWCSessionPayloadRequest() let sessionPayloadResponse = getWCSessionPayloadResponse() - serialiser.deserialised = sessionPayloadResponse - serialiser.serialised = try! sessionPayloadResponse.json().toHexEncodedString() + serializer.deserialized = sessionPayloadResponse + serializer.serialized = try! sessionPayloadResponse.json().toHexEncodedString() wcRelay.request(topic: topic, payload: request) { result in XCTAssertEqual(result, .success(sessionPayloadResponse)) responseExpectation.fulfill() @@ -60,8 +60,8 @@ class WalletConnectRelayTests: XCTestCase { let topic = "fefc3dc39cacbc562ed58f92b296e2d65a6b07ef08992b93db5b3cb86280635a" let request = getWCSessionPayloadRequest() let sessionPayloadResponse = getWCSessionPayloadResponse() - serialiser.deserialised = sessionPayloadResponse - serialiser.serialised = "encrypted_message" + serializer.deserialized = sessionPayloadResponse + serializer.serialized = "encrypted_message" wcRelay.request(topic: topic, payload: request) { result in XCTAssertEqual(result, .success(sessionPayloadResponse)) responseExpectation.fulfill()