diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 0e21edcae..e3f1b5e5f 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -52,8 +52,8 @@ runs: -project Example/ExampleApp.xcodeproj \ -scheme Wallet \ -clonedSourcePackagesDirPath SourcePackagesCache \ - -derivedDataPath DerivedDataCache \ - -sdk iphonesimulator" + -destination 'platform=iOS Simulator,name=iPhone 13' \ + -derivedDataPath DerivedDataCache" # DApp build - name: Build Example Dapp @@ -63,8 +63,8 @@ runs: -project Example/ExampleApp.xcodeproj \ -scheme DApp \ -clonedSourcePackagesDirPath SourcePackagesCache \ - -derivedDataPath DerivedDataCache \ - -sdk iphonesimulator" + -destination 'platform=iOS Simulator,name=iPhone 13' \ + -derivedDataPath DerivedDataCache" # UI tests - name: UI Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf2b26eae..8ff4a9a95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ concurrency: jobs: build: - runs-on: macos-latest + runs-on: macos-12 strategy: matrix: test-type: [unit-tests, integration-tests, build-example-wallet, build-example-dapp] @@ -27,6 +27,8 @@ jobs: - name: Setup Xcode Version uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '13.4.1' - uses: actions/cache@v2 with: @@ -41,8 +43,8 @@ jobs: - name: Resolve Dependencies shell: bash run: " - xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme DApp -clonedSourcePackagesDirPath SourcePackagesCache; \ - xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme WalletConnect -clonedSourcePackagesDirPath SourcePackagesCache" + xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme DApp -clonedSourcePackagesDirPath SourcePackagesCache -derivedDataPath DerivedDataCache -destination 'platform=iOS Simulator,name=iPhone 13'; \ + xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme WalletConnect -clonedSourcePackagesDirPath SourcePackagesCache -derivedDataPath DerivedDataCache -destination 'platform=iOS Simulator,name=iPhone 13'" - uses: ./.github/actions/ci with: diff --git a/Example/DApp/Assets.xcassets/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/Contents.json b/Example/DApp/Assets.xcassets/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/Contents.json new file mode 100644 index 000000000..e40d0ae29 --- /dev/null +++ b/Example/DApp/Assets.xcassets/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "solana (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/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/solana (1).png b/Example/DApp/Assets.xcassets/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/solana (1).png new file mode 100644 index 000000000..1e7c649f8 Binary files /dev/null and b/Example/DApp/Assets.xcassets/solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ.imageset/solana (1).png differ diff --git a/Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift b/Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift index bd9711469..fa2d7b4d3 100644 --- a/Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift +++ b/Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift @@ -7,7 +7,7 @@ class AccountRequestViewController: UIViewController, UITableViewDelegate, UITab private let session: Session private let chainId: String private let account: String - private let methods = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] + private let methods: [String] private let accountRequestView = { AccountRequestView() }() @@ -16,6 +16,7 @@ class AccountRequestViewController: UIViewController, UITableViewDelegate, UITab self.session = session self.chainId = accountDetails.chain self.account = accountDetails.account + self.methods = accountDetails.methods super.init(nibName: nil, bundle: nil) } @@ -37,7 +38,7 @@ class AccountRequestViewController: UIViewController, UITableViewDelegate, UITab } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - methods.count + return methods.count } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 15e50a9fd..0d4932cc7 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -81,9 +81,28 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let pairingTopic = activePairings[indexPath.row].topic - let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] - let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] - let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] + let namespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: [ + Blockchain("eip155:1")!, + Blockchain("eip155:137")! + ], + methods: [ + "eth_sendTransaction", + "personal_sign", + "eth_signTypedData" + ], events: [], extensions: nil + ), + "solana": ProposalNamespace( + chains: [ + Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")!, + ], + methods: [ + "solana_signMessage", + "solana_signTransaction", + ], events: [], extensions: nil + ) + ] Task { _ = try await Sign.instance.connect(requiredNamespaces: namespaces, topic: pairingTopic) connectWithExampleWallet() diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index 7abd2b7d4..f0881e48f 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -15,7 +15,12 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { }() private var publishers = [AnyCancellable]() - let chains = [Chain(name: "Ethereum", id: "eip155:1"), Chain(name: "Polygon", id: "eip155:137")] + let chains = [ + Chain(name: "Ethereum", id: "eip155:1"), + Chain(name: "Polygon", id: "eip155:137"), + Chain(name: "Solana", id: "solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ") + ] + override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Available Chains" @@ -31,9 +36,28 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { @objc private func connect() { print("[PROPOSER] Connecting to a pairing...") - let methods: Set = ["eth_sendTransaction", "personal_sign", "eth_signTypedData"] - let blockchains: Set = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] - let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] + let namespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: [ + Blockchain("eip155:1")!, + Blockchain("eip155:137")! + ], + methods: [ + "eth_sendTransaction", + "personal_sign", + "eth_signTypedData" + ], events: [], extensions: nil + ), + "solana": ProposalNamespace( + chains: [ + Blockchain("solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ")!, + ], + methods: [ + "solana_signMessage", + "solana_signTransaction", + ], events: [], extensions: nil + ) + ] Task { let uri = try await Pair.instance.create() try await Sign.instance.connect(requiredNamespaces: namespaces, topic: uri.topic) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index ffb400057..fc6357d2b 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0D828E436A3001BACF9 /* InputConfig.swift */; }; A51AC0DD28E43727001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DB28E436E6001BACF9 /* InputConfig.swift */; }; A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DE28E4379F001BACF9 /* InputConfig.swift */; }; + A5434023291E6A270068F706 /* SolanaSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A5434022291E6A270068F706 /* SolanaSwift */; }; A55CAAB028B92AFF00844382 /* ScanModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAB28B92AFF00844382 /* ScanModule.swift */; }; A55CAAB128B92AFF00844382 /* ScanPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */; }; A55CAAB228B92AFF00844382 /* ScanRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAD28B92AFF00844382 /* ScanRouter.swift */; }; @@ -91,6 +92,8 @@ A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA362873D8EE00AA7720 /* UIColor.swift */; }; A578FA392873FCE000AA7720 /* ChatScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA382873FCE000AA7720 /* ChatScrollView.swift */; }; A578FA3D2874002400AA7720 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA3C2874002400AA7720 /* View.swift */; }; + A57E71A6291CF76400325797 /* EthereumSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57E71A5291CF76400325797 /* EthereumSigner.swift */; }; + A57E71A8291CF8A500325797 /* SolanaSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57E71A7291CF8A500325797 /* SolanaSigner.swift */; }; A58E7CEB28729F550082D443 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7CEA28729F550082D443 /* AppDelegate.swift */; }; A58E7CED28729F550082D443 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7CEC28729F550082D443 /* SceneDelegate.swift */; }; A58E7CF428729F550082D443 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A58E7CF328729F550082D443 /* Assets.xcassets */; }; @@ -282,6 +285,8 @@ A578FA362873D8EE00AA7720 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; A578FA382873FCE000AA7720 /* ChatScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScrollView.swift; sourceTree = ""; }; A578FA3C2874002400AA7720 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + A57E71A5291CF76400325797 /* EthereumSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumSigner.swift; sourceTree = ""; }; + A57E71A7291CF8A500325797 /* SolanaSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SolanaSigner.swift; sourceTree = ""; }; A58E7CE828729F550082D443 /* Showcase.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Showcase.app; sourceTree = BUILT_PRODUCTS_DIR; }; A58E7CEA28729F550082D443 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A58E7CEC28729F550082D443 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -370,6 +375,7 @@ buildActionMask = 2147483647; files = ( A5AE354728A1A2AC0059AE8A /* Web3 in Frameworks */, + A5434023291E6A270068F706 /* SolanaSwift in Frameworks */, 764E1D5826F8DBAB00A1FB15 /* WalletConnect in Frameworks */, A5D85226286333D500DAF5C3 /* Starscream in Frameworks */, A5C4DD8728A2DE88006A626D /* WalletConnectRouter in Frameworks */, @@ -434,10 +440,10 @@ 761248182819FA8B00CB6D48 /* Shared */ = { isa = PBXGroup; children = ( + A57E71A4291CF73300325797 /* Signer */, 761C64A526FCB0AA004239D1 /* SessionInfo.swift */, 84F568C32795832A00D0A289 /* EthereumTransaction.swift */, A5A4FC5D283D23CA00BBEC1E /* Array.swift */, - 84F568C1279582D200D0A289 /* Signer.swift */, 765056262821989600F9AE79 /* Color+Extension.swift */, 84494387278D9C1B00CC26BB /* UIAlertController.swift */, 76235E882820198B004ED0AA /* UIKit+Previews.swift */, @@ -755,6 +761,16 @@ path = SwiftUI; sourceTree = ""; }; + A57E71A4291CF73300325797 /* Signer */ = { + isa = PBXGroup; + children = ( + 84F568C1279582D200D0A289 /* Signer.swift */, + A57E71A5291CF76400325797 /* EthereumSigner.swift */, + A57E71A7291CF8A500325797 /* SolanaSigner.swift */, + ); + path = Signer; + sourceTree = ""; + }; A58E7CE928729F550082D443 /* Showcase */ = { isa = PBXGroup; children = ( @@ -1132,6 +1148,7 @@ A5D85225286333D500DAF5C3 /* Starscream */, A5AE354628A1A2AC0059AE8A /* Web3 */, A5C4DD8628A2DE88006A626D /* WalletConnectRouter */, + A5434022291E6A270068F706 /* SolanaSwift */, ); productName = ExampleApp; productReference = 764E1D3C26F8D3FC00A1FB15 /* WalletConnect Wallet.app */; @@ -1263,6 +1280,7 @@ packageReferences = ( A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */, A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */, + A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -1339,9 +1357,11 @@ 76235E8B28201C9C004ED0AA /* Utilities.swift in Sources */, 76744CF726FE4D5400B77ED9 /* ActiveSessionItem.swift in Sources */, A5A4FC5A283CC08600BBEC1E /* SessionNamespaceViewModel.swift in Sources */, + A57E71A8291CF8A500325797 /* SolanaSigner.swift in Sources */, 764E1D4226F8D3FC00A1FB15 /* SceneDelegate.swift in Sources */, 84F568C2279582D200D0A289 /* Signer.swift in Sources */, A5A4FC56283CBB7800BBEC1E /* SessionDetailView.swift in Sources */, + A57E71A6291CF76400325797 /* EthereumSigner.swift in Sources */, 7600223B2819FC0B0011DD38 /* ProposalView.swift in Sources */, 761248172819F9E600CB6D48 /* WalletView.swift in Sources */, A5A4FC58283CBB9F00BBEC1E /* SessionDetailViewModel.swift in Sources */, @@ -1983,6 +2003,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/flypaper0/solana-swift"; + requirement = { + branch = "feature/available-13"; + kind = branch; + }; + }; A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/WalletConnect/Web3.swift"; @@ -2018,6 +2046,11 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; }; + A5434022291E6A270068F706 /* SolanaSwift */ = { + isa = XCSwiftPackageProductDependency; + package = A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */; + productName = SolanaSwift; + }; A5629AE92877F2D600094373 /* WalletConnectChat */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectChat; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fbf7253f..8893d902d 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,22 +37,40 @@ "version": "0.1.7" } }, + { + "package": "SolanaSwift", + "repositoryURL": "https://github.com/flypaper0/solana-swift", + "state": { + "branch": "feature/available-13", + "revision": "a98811518e0a90c2dfc60c30cfd3ec85c33b6790", + "version": null + } + }, { "package": "Starscream", "repositoryURL": "https://github.com/daltoniam/Starscream", "state": { "branch": null, - "revision": "e6b65c6d9077ea48b4a7bdda8994a1d3c6969c8d", - "version": "3.1.1" + "revision": "a063fda2b8145a231953c20e7a646be254365396", + "version": "3.1.2" } }, { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "package": "Task_retrying", + "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git", "state": { "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" + "revision": "645eaaf207a6f39ab4b469558d916ae23df199b5", + "version": "1.0.3" + } + }, + { + "package": "TweetNacl", + "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", + "state": { + "branch": null, + "revision": "f8fd111642bf2336b11ef9ea828510693106e954", + "version": "1.1.0" } }, { diff --git a/Example/ExampleApp/Request/RequestViewController.swift b/Example/ExampleApp/Request/RequestViewController.swift index f05ab4b0a..05f240675 100644 --- a/Example/ExampleApp/Request/RequestViewController.swift +++ b/Example/ExampleApp/Request/RequestViewController.swift @@ -43,15 +43,6 @@ class RequestViewController: UIViewController { } private func getParamsDescription() -> String { - let method = sessionRequest.method - if method == "personal_sign" { - return try! sessionRequest.params.get([String].self).description - } else if method == "eth_signTypedData" { - return try! sessionRequest.params.get([String].self).description - } else if method == "eth_sendTransaction" { - let params = try! sessionRequest.params.get([EthereumTransaction].self) - return params[0].description - } - fatalError("not implemented") + return String(describing: sessionRequest.params.value) } } diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift index f1b2a05bc..71ec2b634 100644 --- a/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift +++ b/Example/ExampleApp/SessionDetails/SessionDetailViewController.swift @@ -23,7 +23,7 @@ final class SessionDetailViewController: UIHostingController private func showSessionRequest(_ request: Request) { let viewController = RequestViewController(request) viewController.onSign = { [unowned self] in - let result = Signer.signEth(request: request) + let result = Signer.sign(request: request) respondOnSign(request: request, response: result) reload() } diff --git a/Example/ExampleApp/Shared/Signer.swift b/Example/ExampleApp/Shared/Signer.swift deleted file mode 100644 index d1f2a9fc6..000000000 --- a/Example/ExampleApp/Shared/Signer.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Web3 -import Foundation -import WalletConnectUtils -import WalletConnectSign - -class Signer { - static let privateKey: EthereumPrivateKey = try! EthereumPrivateKey(hexPrivateKey: "0xe56da0e170b5e09a8bb8f1b693392c7d56c3739a9c75740fbc558a2877868540") - private init() {} - static func signEth(request: Request) -> AnyCodable { - let method = request.method - if method == "personal_sign" { - let params = try! request.params.get([String].self) - let messageToSign = params[0] - 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) - return AnyCodable(result) - } else if method == "eth_signTypedData" { - // TODO - let result = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" - return AnyCodable(result) - } else if method == "eth_sendTransaction" { - let params = try! request.params.get([EthereumTransaction].self) - var transaction = params[0] - transaction.gas = EthereumQuantity(quantity: BigUInt("1234")) - print(transaction.description) - let signedTx = try! transaction.sign(with: self.privateKey, chainId: 4) - let (r, s, v) = (signedTx.r, signedTx.s, signedTx.v) - let result = r.hex() + s.hex().dropFirst(2) + String(v.quantity, radix: 16) - return AnyCodable(result) - } - fatalError("not implemented") - } - - private static func dataToHash(_ message: String) -> Bytes { - let prefix = "\u{19}Ethereum Signed Message:\n" - let messageData = Data(hex: message) - let prefixData = (prefix + String(messageData.count)).data(using: .utf8)! - let prefixedMessageData = prefixData + messageData - let dataToHash: Bytes = .init(hex: prefixedMessageData.toHexString()) - return dataToHash - } -} diff --git a/Example/ExampleApp/Shared/Signer/EthereumSigner.swift b/Example/ExampleApp/Shared/Signer/EthereumSigner.swift new file mode 100644 index 000000000..6f1178872 --- /dev/null +++ b/Example/ExampleApp/Shared/Signer/EthereumSigner.swift @@ -0,0 +1,50 @@ +import Foundation +import Commons +import Web3 + +struct EthereumSigner { + + private init() {} + + static var address: String { + return privateKey.address.hex(eip55: true) + } + + private static let privateKey: EthereumPrivateKey = { + return try! EthereumPrivateKey(hexPrivateKey: "0xe56da0e170b5e09a8bb8f1b693392c7d56c3739a9c75740fbc558a2877868540") + }() + + static func personalSign(_ params: AnyCodable) -> AnyCodable { + let params = try! params.get([String].self) + let messageToSign = params[0] + let dataToHash = dataToHash(messageToSign) + let (v, r, s) = try! privateKey.sign(message: .init(hex: dataToHash.toHexString())) + let result = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) + return AnyCodable(result) + } + + static func signTypedData(_ params: AnyCodable) -> AnyCodable { + let result = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" + return AnyCodable(result) + } + + static func sendTransaction(_ params: AnyCodable) -> AnyCodable { + let params = try! params.get([EthereumTransaction].self) + var transaction = params[0] + transaction.gas = EthereumQuantity(quantity: BigUInt("1234")) + print(transaction.description) + let signedTx = try! transaction.sign(with: self.privateKey, chainId: 4) + let (r, s, v) = (signedTx.r, signedTx.s, signedTx.v) + let result = r.hex() + s.hex().dropFirst(2) + String(v.quantity, radix: 16) + return AnyCodable(result) + } + + private static func dataToHash(_ message: String) -> Bytes { + let prefix = "\u{19}Ethereum Signed Message:\n" + let messageData = Data(hex: message) + let prefixData = (prefix + String(messageData.count)).data(using: .utf8)! + let prefixedMessageData = prefixData + messageData + let dataToHash: Bytes = .init(hex: prefixedMessageData.toHexString()) + return dataToHash + } +} diff --git a/Example/ExampleApp/Shared/Signer/Signer.swift b/Example/ExampleApp/Shared/Signer/Signer.swift new file mode 100644 index 000000000..dde8e101f --- /dev/null +++ b/Example/ExampleApp/Shared/Signer/Signer.swift @@ -0,0 +1,26 @@ +import Foundation +import Commons +import WalletConnectSign + +class Signer { + + private init() {} + + static func sign(request: Request) -> AnyCodable { + switch request.method { + case "personal_sign": + return EthereumSigner.personalSign(request.params) + + case "eth_signTypedData": + return EthereumSigner.signTypedData(request.params) + + case "eth_sendTransaction": + return EthereumSigner.sendTransaction(request.params) + + case "solana_signTransaction": + return SolanaSigner.signTransaction(request.params) + default: + fatalError("not implemented") + } + } +} diff --git a/Example/ExampleApp/Shared/Signer/SolanaSigner.swift b/Example/ExampleApp/Shared/Signer/SolanaSigner.swift new file mode 100644 index 000000000..c65ec83f1 --- /dev/null +++ b/Example/ExampleApp/Shared/Signer/SolanaSigner.swift @@ -0,0 +1,43 @@ +import Foundation +import Commons +import SolanaSwift +import TweetNacl + +struct SolanaSigner { + + static var address: String { + return account.publicKey.base58EncodedString + } + + private static let account: Account = { + let key = "4eN1YZm598FtdigriE5int7Gf5dxs58rzVh3ftRwxjkYXxkiDiweuvkop2Kr5Td174DcbVdDxzjWqQ96uir3NYka" + return try! Account(secretKey: Data(Base58.decode(key))) + }() + + private init() {} + + static func signTransaction(_ params: AnyCodable) -> AnyCodable { + let transaction = try! params.get(SolSignTransaction.self) + let message = try! transaction.transaction.compileMessage() + let serializedMessage = try! message.serialize() + let signature = try! NaclSign.signDetached( + message: serializedMessage, + secretKey: account.secretKey + ) + return AnyCodable(["signature": Base58.encode(signature)]) + } +} + +fileprivate struct SolSignTransaction: Codable { + let instructions: [TransactionInstruction] + let recentBlockhash: String + let feePayer: PublicKey + + var transaction: Transaction { + return Transaction( + instructions: instructions, + recentBlockhash: recentBlockhash, + feePayer: feePayer + ) + } +} diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index a888df7bd..034998740 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -8,7 +8,11 @@ import CryptoSwift import Combine final class WalletViewController: UIViewController { - lazy var account = Signer.privateKey.address.hex(eip55: true) + lazy var accounts = [ + "eip155": EthereumSigner.address, + "solana": SolanaSigner.address + ] + var sessionItems: [ActiveSessionItem] = [] var currentProposal: Session.Proposal? private var publishers = [AnyCancellable]() @@ -72,7 +76,7 @@ final class WalletViewController: UIViewController { private func showSessionRequest(_ request: Request) { let requestVC = RequestViewController(request) requestVC.onSign = { [unowned self] in - let result = Signer.signEth(request: request) + let result = Signer.sign(request: request) respondOnSign(request: request, response: result) reloadSessionDetailsIfNeeded() } @@ -215,10 +219,10 @@ extension WalletViewController: ProposalViewControllerDelegate { proposal.requiredNamespaces.forEach { let caip2Namespace = $0.key let proposalNamespace = $0.value - let accounts = Set(proposalNamespace.chains.compactMap { Account($0.absoluteString + ":\(account)") }) + let accounts = Set(proposalNamespace.chains.compactMap { Account($0.absoluteString + ":\(self.accounts[$0.namespace]!)") }) let extensions: [SessionNamespace.Extension]? = proposalNamespace.extensions?.map { element in - let accounts = Set(element.chains.compactMap { Account($0.absoluteString + ":\(account)") }) + let accounts = Set(element.chains.compactMap { Account($0.absoluteString + ":\(self.accounts[$0.namespace]!)") }) return SessionNamespace.Extension(accounts: accounts, methods: element.methods, events: element.events) } let sessionNamespace = SessionNamespace(accounts: accounts, methods: proposalNamespace.methods, events: proposalNamespace.events, extensions: extensions) diff --git a/Sources/JSONRPC/RPCResponse.swift b/Sources/JSONRPC/RPCResponse.swift index b6fddfb80..09b0a099c 100644 --- a/Sources/JSONRPC/RPCResponse.swift +++ b/Sources/JSONRPC/RPCResponse.swift @@ -19,7 +19,7 @@ public struct RPCResponse: Equatable { public let outcome: RPCResult - internal init(id: RPCID?, outcome: RPCResult) { + public init(id: RPCID?, outcome: RPCResult) { self.jsonrpc = "2.0" self.id = id self.outcome = outcome diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 2beaca2db..4aa117c12 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -95,7 +95,7 @@ public class NetworkingInteractor: NetworkInteracting { public func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + let message = try serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.requestConfig.tag, prompt: protocolMethod.requestConfig.prompt, ttl: protocolMethod.requestConfig.ttl) } diff --git a/Sources/WalletConnectRelay/AppStateObserving.swift b/Sources/WalletConnectRelay/AppStateObserving.swift index bc708c74b..3db4ff629 100644 --- a/Sources/WalletConnectRelay/AppStateObserving.swift +++ b/Sources/WalletConnectRelay/AppStateObserving.swift @@ -1,14 +1,23 @@ import Foundation -#if os(iOS) + +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit #endif -protocol AppStateObserving { +enum ApplicationState { + case background, foreground +} + +protocol AppStateObserving: AnyObject { + var currentState: ApplicationState { get } var onWillEnterForeground: (() -> Void)? {get set} var onWillEnterBackground: (() -> Void)? {get set} } class AppStateObserver: AppStateObserving { + @objc var onWillEnterForeground: (() -> Void)? @objc var onWillEnterBackground: (() -> Void)? @@ -17,6 +26,16 @@ class AppStateObserver: AppStateObserving { subscribeNotificationCenter() } + var currentState: ApplicationState { +#if canImport(UIKit) + let isActive = UIApplication.shared.applicationState == .active + return isActive ? .foreground : .background +#elseif canImport(AppKit) + let isActive = NSApplication.shared.isActive + return isActive ? .foreground : .background +#endif + } + private func subscribeNotificationCenter() { #if os(iOS) NotificationCenter.default.addObserver( diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index fcf9b588f..10dff9898 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -103,6 +103,7 @@ final class Dispatcher: NSObject, Dispatching { } socket.onDisconnect = { [unowned self] _ in self.socketConnectionStatusPublisherSubject.send(.disconnected) + self.socketConnectionHandler.handleDisconnection() } } } diff --git a/Sources/WalletConnectRelay/NetworkMonitoring.swift b/Sources/WalletConnectRelay/NetworkMonitoring.swift index 66fe0a88c..c4200171f 100644 --- a/Sources/WalletConnectRelay/NetworkMonitoring.swift +++ b/Sources/WalletConnectRelay/NetworkMonitoring.swift @@ -1,7 +1,7 @@ import Foundation import Network -protocol NetworkMonitoring { +protocol NetworkMonitoring: AnyObject { var onSatisfied: (() -> Void)? {get set} var onUnsatisfied: (() -> Void)? {get set} func startMonitoring() diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index 1f6b5d28d..4a5b368a0 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.0.5"} +{"version": "1.0.6"} diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 273b3a175..5e95f79b8 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -4,26 +4,30 @@ import UIKit import Foundation import Combine -class AutomaticSocketConnectionHandler: SocketConnectionHandler { - enum Error: Swift.Error { - case manualSocketConnectionForbidden - case manualSocketDisconnectionForbidden +class AutomaticSocketConnectionHandler { + + enum Errors: Error { + case manualSocketConnectionForbidden, manualSocketDisconnectionForbidden } - private var appStateObserver: AppStateObserving - let socket: WebSocketConnecting - private var networkMonitor: NetworkMonitoring + + private let socket: WebSocketConnecting + private let appStateObserver: AppStateObserving + private let networkMonitor: NetworkMonitoring private let backgroundTaskRegistrar: BackgroundTaskRegistering private var publishers = Set() - init(networkMonitor: NetworkMonitoring = NetworkMonitor(), - socket: WebSocketConnecting, - appStateObserver: AppStateObserving = AppStateObserver(), - backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar()) { + init( + socket: WebSocketConnecting, + networkMonitor: NetworkMonitoring = NetworkMonitor(), + appStateObserver: AppStateObserving = AppStateObserver(), + backgroundTaskRegistrar: BackgroundTaskRegistering = BackgroundTaskRegistrar() + ) { self.appStateObserver = appStateObserver self.socket = socket self.networkMonitor = networkMonitor self.backgroundTaskRegistrar = backgroundTaskRegistrar + setUpStateObserving() setUpNetworkMonitoring() @@ -36,40 +40,49 @@ class AutomaticSocketConnectionHandler: SocketConnectionHandler { } appStateObserver.onWillEnterForeground = { [unowned self] in - if !socket.isConnected { - socket.connect() - } + reconnectIfNeeded() } } private func setUpNetworkMonitoring() { networkMonitor.onSatisfied = { [weak self] in - self?.handleNetworkSatisfied() + self?.reconnectIfNeeded() } networkMonitor.startMonitoring() } - func registerBackgroundTask() { + private func registerBackgroundTask() { backgroundTaskRegistrar.register(name: "Finish Network Tasks") { [unowned self] in endBackgroundTask() } } - func endBackgroundTask() { + private func endBackgroundTask() { socket.disconnect() } + private func reconnectIfNeeded() { + if !socket.isConnected { + socket.connect() + } + } +} + +// MARK: - SocketConnectionHandler + +extension AutomaticSocketConnectionHandler: SocketConnectionHandler { + func handleConnect() throws { - throw Error.manualSocketConnectionForbidden + throw Errors.manualSocketConnectionForbidden } func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { - throw Error.manualSocketDisconnectionForbidden + throw Errors.manualSocketDisconnectionForbidden } - func handleNetworkSatisfied() { - if !socket.isConnected { - socket.connect() + func handleDisconnection() { + if appStateObserver.currentState == .foreground { + reconnectIfNeeded() } } } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift index 25f293d05..bed340e44 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/ManualSocketConnectionHandler.swift @@ -14,4 +14,9 @@ class ManualSocketConnectionHandler: SocketConnectionHandler { func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { socket.disconnect() } + + func handleDisconnection() { + // No operation + // ManualSocketConnectionHandler does not support reconnection logic + } } diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift index 0b5e0673b..035d1d1df 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/SocketConnectionHandler.swift @@ -3,4 +3,5 @@ import Foundation protocol SocketConnectionHandler { func handleConnect() throws func handleDisconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws + func handleDisconnection() } diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 438b7f29f..8ac916277 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -87,12 +87,15 @@ final class ApproveEngine { let result = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) let response = RPCResponse(id: payload.id, result: result) - try await networkingInteractor.respond(topic: payload.topic, response: response, protocolMethod: SessionProposeProtocolMethod()) + + async let proposeResponse: () = networkingInteractor.respond(topic: payload.topic, response: response, protocolMethod: SessionProposeProtocolMethod()) + + async let settleRequest: () = settle(topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces) + + let _ = try await [proposeResponse, settleRequest] try pairing.updateExpiry() pairingStore.setPairing(pairing) - - try await settle(topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces) } func reject(proposerPubKey: String, reason: SignReasonCode) async throws { @@ -138,12 +141,16 @@ final class ApproveEngine { logger.debug("Sending session settle request") - try await networkingInteractor.subscribe(topic: topic) sessionStore.setSession(session) let protocolMethod = SessionSettleProtocolMethod() let request = RPCRequest(method: protocolMethod.method, params: settleParams) - try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + + async let subscription: () = networkingInteractor.subscribe(topic: topic) + async let settleRequest: () = networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + + let _ = try await [settleRequest, subscription] + onSessionSettle?(session.publicRepresentation()) } } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index b921bb6b4..52c5fcb9f 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -63,7 +63,7 @@ final class SessionEngine { guard sessionStore.hasSession(forTopic: topic) else { throw Errors.sessionNotFound(topic: topic) } - let response = RPCResponse(id: requestId, result: response) + let response = RPCResponse(id: requestId, outcome: response) try await networkingInteractor.respond(topic: topic, response: response, protocolMethod: SessionRequestProtocolMethod()) } @@ -121,12 +121,23 @@ private extension SessionEngine { func setupResponseSubscriptions() { networkingInteractor.responseSubscription(on: SessionRequestProtocolMethod()) - .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in onSessionResponse?(Response( id: payload.id, topic: payload.topic, chainId: payload.request.chainId.absoluteString, - result: payload.response + result: .response(payload.response) + )) + } + .store(in: &publishers) + + networkingInteractor.responseErrorSubscription(on: SessionRequestProtocolMethod()) + .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in + onSessionResponse?(Response( + id: payload.id, + topic: payload.topic, + chainId: payload.request.chainId.absoluteString, + result: .error(payload.error) )) } .store(in: &publishers) diff --git a/Tests/JSONRPCTests/RPCResponseTests.swift b/Tests/JSONRPCTests/RPCResponseTests.swift index d067761b3..e98c0cf9a 100644 --- a/Tests/JSONRPCTests/RPCResponseTests.swift +++ b/Tests/JSONRPCTests/RPCResponseTests.swift @@ -10,7 +10,8 @@ private func makeResultResponses() -> [RPCResponse] { RPCResponse(id: Int64.random(), result: String.random()), RPCResponse(id: Int64.random(), result: (1...10).map { String($0) }), RPCResponse(id: Int64.random(), result: EmptyCodable()), - RPCResponse(id: String.random(), result: Int.random()) + RPCResponse(id: String.random(), result: Int.random()), + RPCResponse(id: RPCID(String.random()), outcome: .response(AnyCodable(Int.random()))) ] } @@ -19,7 +20,8 @@ private func makeErrorResponses() -> [RPCResponse] { RPCResponse(id: Int64.random(), error: JSONRPCError.stub()), RPCResponse(id: String.random(), error: JSONRPCError.stub(data: AnyCodable(Int.random()))), RPCResponse(id: Int64.random(), errorCode: Int.random(), message: String.random(), associatedData: AnyCodable(String.random())), - RPCResponse(id: String.random(), errorCode: Int.random(), message: String.random(), associatedData: nil) + RPCResponse(id: String.random(), errorCode: Int.random(), message: String.random(), associatedData: nil), + RPCResponse(id: RPCID(String.random()), outcome: .error(JSONRPCError.stub())) ] } diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index d37cec47f..1207b4335 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -6,25 +6,26 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { var sut: AutomaticSocketConnectionHandler! var webSocketSession: WebSocketMock! var networkMonitor: NetworkMonitoringMock! - var appStateObserver: AppStateObserving! + var appStateObserver: AppStateObserverMock! var backgroundTaskRegistrar: BackgroundTaskRegistrarMock! + override func setUp() { webSocketSession = WebSocketMock() networkMonitor = NetworkMonitoringMock() appStateObserver = AppStateObserverMock() backgroundTaskRegistrar = BackgroundTaskRegistrarMock() sut = AutomaticSocketConnectionHandler( - networkMonitor: networkMonitor, socket: webSocketSession, + networkMonitor: networkMonitor, appStateObserver: appStateObserver, backgroundTaskRegistrar: backgroundTaskRegistrar) } func testConnectsOnConnectionSatisfied() { webSocketSession.disconnect() - XCTAssertFalse(sut.socket.isConnected) + XCTAssertFalse(webSocketSession.isConnected) networkMonitor.onSatisfied?() - XCTAssertTrue(sut.socket.isConnected) + XCTAssertTrue(webSocketSession.isConnected) } func testHandleConnectThrows() { @@ -38,7 +39,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testReconnectsOnEnterForeground() { webSocketSession.disconnect() appStateObserver.onWillEnterForeground?() - XCTAssertTrue(sut.socket.isConnected) + XCTAssertTrue(webSocketSession.isConnected) } func testRegisterTaskOnEnterBackground() { @@ -49,8 +50,24 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testDisconnectOnEndBackgroundTask() { appStateObserver.onWillEnterBackground?() - XCTAssertTrue(sut.socket.isConnected) + XCTAssertTrue(webSocketSession.isConnected) backgroundTaskRegistrar.completion!() - XCTAssertFalse(sut.socket.isConnected) + XCTAssertFalse(webSocketSession.isConnected) + } + + func testReconnectOnDisconnectForeground() { + appStateObserver.currentState = .foreground + XCTAssertTrue(webSocketSession.isConnected) + webSocketSession.disconnect() + sut.handleDisconnection() + XCTAssertTrue(webSocketSession.isConnected) + } + + func testReconnectOnDisconnectBackground() { + appStateObserver.currentState = .background + XCTAssertTrue(webSocketSession.isConnected) + webSocketSession.disconnect() + sut.handleDisconnection() + XCTAssertFalse(webSocketSession.isConnected) } } diff --git a/Tests/RelayerTests/Mocks/AppStateObserverMock.swift b/Tests/RelayerTests/Mocks/AppStateObserverMock.swift index 5eb39c927..0da2d08d8 100644 --- a/Tests/RelayerTests/Mocks/AppStateObserverMock.swift +++ b/Tests/RelayerTests/Mocks/AppStateObserverMock.swift @@ -2,6 +2,7 @@ import Foundation @testable import WalletConnectRelay class AppStateObserverMock: AppStateObserving { + var currentState: ApplicationState = .foreground var onWillEnterForeground: (() -> Void)? var onWillEnterBackground: (() -> Void)? }