diff --git a/.github/workflows/insider.yml b/.github/workflows/insider.yml index b95497c6..c7f98b75 100644 --- a/.github/workflows/insider.yml +++ b/.github/workflows/insider.yml @@ -24,6 +24,12 @@ jobs: - name: Set insider SUFeedURL run: /usr/libexec/PlistBuddy -c "Set :SUFeedURL https://opensource.planetable.xyz/planet-insider/appcast.xml" Planet/Info.plist + + - name: Set WalletConnectV2 Project ID + run: /usr/libexec/PlistBuddy -c "Set :WALLETCONNECTV2_PROJECT_ID ${{ secrets.WALLETCONNECTV2_PROJECT_ID }}" Planet/Info.plist + + - name: Set Etherscan API token + run: /usr/libexec/PlistBuddy -c "Set :ETHERSCAN_API_TOKEN ${{ secrets.ETHERSCAN_API_TOKEN }}" Planet/Info.plist - name: Set insider icon run: /usr/bin/sed -i '' 's/AppIcon/AppIcon-Insider/g' Planet/Release.xcconfig diff --git a/Planet.xcodeproj/project.pbxproj b/Planet.xcodeproj/project.pbxproj index 4fc8e100..907a8b6a 100644 --- a/Planet.xcodeproj/project.pbxproj +++ b/Planet.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ 2AA7974729496F730031E873 /* PFDashboardWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA7973B29496F730031E873 /* PFDashboardWindowController.swift */; }; 2AA7974829496F730031E873 /* PlanetPublishedFolders+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA7973C29496F730031E873 /* PlanetPublishedFolders+Extension.swift */; }; 2AA7974929496F730031E873 /* PFDashboardInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA7973D29496F730031E873 /* PFDashboardInspectorViewController.swift */; }; + 2AAF510A2C293DE100C0B7AA /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2AAF51092C293DE100C0B7AA /* HDWalletKit */; }; 2AB6A93B29703757007186A7 /* PlanetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB6A93A29703757007186A7 /* PlanetAPI.swift */; }; 2AB6A93E29703825007186A7 /* Swifter in Frameworks */ = {isa = PBXBuildFile; productRef = 2AB6A93D29703825007186A7 /* Swifter */; }; 2AB6A94029707980007186A7 /* PlanetSettingsAPIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB6A93F29707980007186A7 /* PlanetSettingsAPIView.swift */; }; @@ -408,6 +409,8 @@ 6A98E54C2BEDA2590008E3A4 /* PlanetSettingsGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE44EAB28CD218200944786 /* PlanetSettingsGeneralView.swift */; }; 6A98E54D2BEDA2910008E3A4 /* PlanetSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE44EAF28CD223C00944786 /* PlanetSettingsViewModel.swift */; }; 6A9C211029792464005A815E /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A9C210F29792464005A815E /* AVKit.framework */; }; + 6A9C7BAF2C2B92100056BF22 /* WCTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9C7BAE2C2B92100056BF22 /* WCTransaction.swift */; }; + 6A9C7BB02C2B92100056BF22 /* WCTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9C7BAE2C2B92100056BF22 /* WCTransaction.swift */; }; 6AA2C2022AFF538B00F6C633 /* MyArticleGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA2C2012AFF538B00F6C633 /* MyArticleGridView.swift */; }; 6AA2C2032AFF538B00F6C633 /* MyArticleGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA2C2012AFF538B00F6C633 /* MyArticleGridView.swift */; }; 6AABC6A7291A6216009FD13F /* WalletConnectV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AABC6A6291A6216009FD13F /* WalletConnectV1.swift */; }; @@ -738,6 +741,7 @@ 6A900192296C9F3800BC088E /* MyArticleSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyArticleSettingsView.swift; sourceTree = ""; }; 6A941CEC2AF5E7A300CA8261 /* PlanetStore+ServerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlanetStore+ServerInfo.swift"; sourceTree = ""; }; 6A9C210F29792464005A815E /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; + 6A9C7BAE2C2B92100056BF22 /* WCTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCTransaction.swift; sourceTree = ""; }; 6AA2BC3227DC09AE00AC96B5 /* Planet v3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Planet v3.xcdatamodel"; sourceTree = ""; }; 6AA2C2012AFF538B00F6C633 /* MyArticleGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyArticleGridView.swift; sourceTree = ""; }; 6AABC6A6291A6216009FD13F /* WalletConnectV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectV1.swift; sourceTree = ""; }; @@ -869,6 +873,7 @@ 6A7189692947FD0500279D77 /* Web3 in Frameworks */, BBC831AB2836239300CE1F67 /* PlanetSiteTemplates in Frameworks */, 6AB6E50F291527B800F17328 /* WalletConnect in Frameworks */, + 2AAF510A2C293DE100C0B7AA /* HDWalletKit in Frameworks */, 2A69F04727E1B53A00141DC0 /* FeedKit in Frameworks */, 2AC4996C27BB99DC00F1C2D1 /* Stencil in Frameworks */, 6A71896B2947FD0500279D77 /* Web3ContractABI in Frameworks */, @@ -1457,6 +1462,7 @@ 6ACF166429246BD800AE318B /* TipSelectView.swift */, 6ACF16682927267800AE318B /* WalletTransactionProgressView.swift */, 6AC33B922934159A001ABBCF /* EthereumTransaction.swift */, + 6A9C7BAE2C2B92100056BF22 /* WCTransaction.swift */, ); path = Wallet; sourceTree = ""; @@ -1653,6 +1659,7 @@ 6AFCF7DB2B7353C6002299BA /* Collections */, 6AFCF7DD2B7353C6002299BA /* DequeModule */, 6AFCF7DF2B7353C6002299BA /* OrderedCollections */, + 2AAF51092C293DE100C0B7AA /* HDWalletKit */, ); productName = Planet; productReference = 2AC4994527BB6E7500F1C2D1 /* Planet.app */; @@ -1709,6 +1716,7 @@ 6A2A4D2A2A86422700F4491A /* XCRemoteSwiftPackageReference "WrappingHStack" */, 2A27743F2A9514EE00A5DDC2 /* XCRemoteSwiftPackageReference "Zip" */, 6AFCF7DA2B7353C6002299BA /* XCRemoteSwiftPackageReference "swift-collections" */, + 2AAF51082C293DE100C0B7AA /* XCRemoteSwiftPackageReference "HDWallet" */, ); productRefGroup = 2AC4994627BB6E7500F1C2D1 /* Products */; projectDirPath = ""; @@ -1957,6 +1965,7 @@ 2A95E6FD2A1B003D001288B8 /* MyPlanetEditView.swift in Sources */, 2A996AD62A1DA6C700BEF898 /* PlanetDownloadsViewModel.swift in Sources */, 2A466FD22A809E2C009FB646 /* IndicatorViews.swift in Sources */, + 6A9C7BB02C2B92100056BF22 /* WCTransaction.swift in Sources */, 2A95E6772A19A3C4001288B8 /* URLUtils.swift in Sources */, 2ABD1CC42A1616DA0082D7EA /* AppDelegate.swift in Sources */, 2A8E085B2BF4B19B002D0C03 /* IPFSOpenWindow.swift in Sources */, @@ -2046,6 +2055,7 @@ 2AF4003927F2321F005DF1A9 /* WriterTextView.swift in Sources */, 6A6A1468282159DC009644B6 /* TemplateStore.swift in Sources */, 2AF9C5BC289263BC00327A8F /* Updater.swift in Sources */, + 6A9C7BAF2C2B92100056BF22 /* WCTransaction.swift in Sources */, 6A2827D42BD8CD29006B4A84 /* QuickPostView.swift in Sources */, 6A18F2062A0F0FA700FC4050 /* RebuildProgressView.swift in Sources */, 2A2556BC293E1F53006462D8 /* TemplateBrowserInspectorView.swift in Sources */, @@ -2571,6 +2581,14 @@ minimumVersion = 9.0.0; }; }; + 2AAF51082C293DE100C0B7AA /* XCRemoteSwiftPackageReference "HDWallet" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/WalletConnect/HDWallet"; + requirement = { + branch = develop; + kind = branch; + }; + }; 2AB6A93C29703825007186A7 /* XCRemoteSwiftPackageReference "swifter" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/httpswift/swifter"; @@ -2632,7 +2650,7 @@ repositoryURL = "https://github.com/WalletConnect/WalletConnectSwiftV2"; requirement = { kind = exactVersion; - version = 1.8.6; + version = 1.9.8; }; }; 6ADF40C229151407007A24E8 /* XCRemoteSwiftPackageReference "Starscream" */ = { @@ -2817,6 +2835,11 @@ package = 2AB6A93C29703825007186A7 /* XCRemoteSwiftPackageReference "swifter" */; productName = Swifter; }; + 2AAF51092C293DE100C0B7AA /* HDWalletKit */ = { + isa = XCSwiftPackageProductDependency; + package = 2AAF51082C293DE100C0B7AA /* XCRemoteSwiftPackageReference "HDWallet" */; + productName = HDWalletKit; + }; 2AB6A93D29703825007186A7 /* Swifter */ = { isa = XCSwiftPackageProductDependency; package = 2AB6A93C29703825007186A7 /* XCRemoteSwiftPackageReference "swifter" */; diff --git a/Planet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Planet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7e2391c8..34139b07 100644 --- a/Planet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Planet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "2d350f39146062b36abb00c558bb2f952938a1a96238113e0d424870f7d32ad3", + "originHash" : "04f5ec2c370ad854d1601a3ffab8475cd1d8774bd8d70858486dcfe0847a1e8e", "pins" : [ { "identity" : "bigint", @@ -46,6 +46,15 @@ "version" : "9.1.2" } }, + { + "identity" : "hdwallet", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/HDWallet", + "state" : { + "branch" : "develop", + "revision" : "748a85b1dfe9a2fa592bd9266c5a926e4e1d3f44" + } + }, { "identity" : "libcmark_gfm", "kind" : "remoteSourceControl", @@ -231,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/WalletConnect/WalletConnectSwiftV2", "state" : { - "revision" : "b1b2486a82c5ff72e61cad89d7c0442d5e025562", - "version" : "1.8.6" + "revision" : "addf9a3688ef5e5d9d148ecbb30ca0fd3132b908", + "version" : "1.9.8" } }, { diff --git a/Planet/Helper/KeyboardShortcutHelper.swift b/Planet/Helper/KeyboardShortcutHelper.swift index 27ad42af..73afaf5c 100644 --- a/Planet/Helper/KeyboardShortcutHelper.swift +++ b/Planet/Helper/KeyboardShortcutHelper.swift @@ -56,18 +56,26 @@ class KeyboardShortcutHelper: ObservableObject { } } else { + /* TODO: Remove this button for V1 Button { WalletManager.shared.connectV1() } label: { Text("Connect Wallet") } - } + */ - if PlanetStore.shared.walletConnectV2Ready { - Button { - WalletManager.shared.connectV2() - } label: { - Text("Connect Wallet V2") + if PlanetStore.shared.walletConnectV2Ready { + Button { + Task { @MainActor in + do { + try await WalletManager.shared.connectV2() + } catch { + debugPrint("failed to connect wallet v2: \(error)") + } + } + } label: { + Text("Connect Wallet V2") + } } } diff --git a/Planet/Helper/ViewUtils.swift b/Planet/Helper/ViewUtils.swift index 48b10fb2..036566e8 100644 --- a/Planet/Helper/ViewUtils.swift +++ b/Planet/Helper/ViewUtils.swift @@ -131,7 +131,7 @@ extension Color { ) } - func toHexString() -> String { + func toHexValue() -> String { var components: (CGFloat, CGFloat, CGFloat, CGFloat) { let c = NSColor(self).usingColorSpace(.deviceRGB)! diff --git a/Planet/IPFS/IPFSState.swift b/Planet/IPFS/IPFSState.swift index 2012992f..a73c4f95 100644 --- a/Planet/IPFS/IPFSState.swift +++ b/Planet/IPFS/IPFSState.swift @@ -41,12 +41,12 @@ class IPFSState: ObservableObject { } } self.refreshRateTimer = Timer.scheduledTimer(withTimeInterval: Self.refreshRate, repeats: true, block: { _ in - Task.detached(priority: .utility) { + Task.detached(priority: .userInitiated) { await self.updateStatus() } }) self.refreshTrafficTimer = Timer.scheduledTimer(withTimeInterval: Self.refreshTrafficRate, repeats: true, block: { _ in - Task.detached(priority: .background) { + Task.detached(priority: .userInitiated) { await self.updateTrafficStatus() } }) diff --git a/Planet/Info.plist b/Planet/Info.plist index 69f22dc7..b234a59c 100644 --- a/Planet/Info.plist +++ b/Planet/Info.plist @@ -233,9 +233,11 @@ WALLETCONNECTV2_ENABLED - + WALLETCONNECTV2_PROJECT_ID $(WALLETCONNECTV2_PROJECT_ID) + ETHERSCAN_API_TOKEN + $(ETHERSCAN_API_TOKEN) NSSupportsSuddenTermination diff --git a/Planet/Labs/Wallet/EthereumTransaction.swift b/Planet/Labs/Wallet/EthereumTransaction.swift index 2f975d96..900ecd30 100644 --- a/Planet/Labs/Wallet/EthereumTransaction.swift +++ b/Planet/Labs/Wallet/EthereumTransaction.swift @@ -51,7 +51,8 @@ class EthereumTransaction: Codable, Identifiable { to: String, toENS: String? = nil, amount: Int, - memo: String + memo: String, + created: Date? = nil ) { self.id = id self.chainID = chainID @@ -60,7 +61,11 @@ class EthereumTransaction: Codable, Identifiable { self.toENS = toENS self.amount = amount self.memo = memo - self.created = Date() + if let created = created { + self.created = created + } else { + self.created = Date() + } } static func from(path: URL) -> EthereumTransaction? { @@ -96,6 +101,15 @@ class EthereumTransaction: Codable, Identifiable { try JSONEncoder.shared.encode(self).write(to: txPath) } + func exists() -> Bool { + let walletPath = Self.walletsInfoPath().appendingPathComponent( + from, + isDirectory: true + ) + let txPath = walletPath.appendingPathComponent(id + ".json", isDirectory: false) + return FileManager.default.fileExists(atPath: txPath.path) + } + @ViewBuilder func recipientView() -> some View { if let ens = toENS { @@ -103,7 +117,7 @@ class EthereumTransaction: Codable, Identifiable { .font(.body) } else { - Text(to) + Text(to.shortWalletAddress()) .font(.footnote) } } diff --git a/Planet/Labs/Wallet/TipSelectView.swift b/Planet/Labs/Wallet/TipSelectView.swift index f77497f8..34fe4611 100644 --- a/Planet/Labs/Wallet/TipSelectView.swift +++ b/Planet/Labs/Wallet/TipSelectView.swift @@ -47,6 +47,18 @@ struct TipSelectView: View { HStack { Text("Please select the amount") Spacer() + Picker(selection: $ethereumChainId, label: Text("")) { + ForEach(EthereumChainID.allCases, id: \.id) { value in + Text( + "\(EthereumChainID.names[value.rawValue] ?? "Unknown Chain ID \(value.rawValue)")" + ) + .tag(value) + .frame(width: 120) + } + } + .pickerStyle(.menu) + .frame(width: 120) + /* TODO: Remove this V1 logic if WalletManager.shared.canSwitchNetwork() { Picker(selection: $ethereumChainId, label: Text("")) { ForEach(EthereumChainID.allCases, id: \.id) { value in @@ -70,6 +82,7 @@ struct TipSelectView: View { .help("This transaction will be sent to \(name) network") } } + */ }.padding(10) GroupBox { @@ -131,7 +144,8 @@ struct TipSelectView: View { } private func updateCurrentGasPrice() { - let web3 = Web3(rpcURL: "https://cloudflare-eth.com") + let chain = EthereumChainID(rawValue: ethereumChainId) ?? .mainnet + let web3 = Web3(rpcURL: chain.rpcURL) web3.eth.gasPrice() { response in if response.status.isSuccess, let gasPrice = response.result { print("Gas price: \(gasPrice)") @@ -154,11 +168,8 @@ struct TipSelectView: View { } let ethereumChainName = WalletManager.shared.currentNetworkName() var walletAppString: String = "" - if let walletAppName = WalletManager.shared.walletConnect.session.walletInfo?.peerMeta.name { - walletAppString = walletAppName + " on" - } else { - walletAppString = "the wallet app on" - } + let walletAppName = WalletManager.shared.getWalletAppName() + walletAppString = walletAppName + " on" let message: String if let ens = ens { message = "Sending \(tipAmountLabel) to **\(ens)** on \(ethereumChainName), please confirm from \(walletAppString) your phone" @@ -170,7 +181,10 @@ struct TipSelectView: View { PlanetStore.shared.walletTransactionProgressMessage = message PlanetStore.shared.isShowingWalletTransactionProgress = true } - WalletManager.shared.walletConnect.sendTransaction(receiver: receiver, amount: tipAmount, memo: memo, ens: ens) + // WalletManager.shared.walletConnect.sendTransaction(receiver: receiver, amount: tipAmount, memo: memo, ens: ens) + Task { + await WalletManager.shared.sendTransactionV2(receiver: receiver, amount: tipAmount, memo: memo, ens: ens, gas: currentGasPrice) + } DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { PlanetStore.shared.isShowingWalletTransactionProgress = false } diff --git a/Planet/Labs/Wallet/WCTransaction.swift b/Planet/Labs/Wallet/WCTransaction.swift new file mode 100644 index 00000000..6b6cbd66 --- /dev/null +++ b/Planet/Labs/Wallet/WCTransaction.swift @@ -0,0 +1,61 @@ +// +// WCTransaction.swift +// Planet +// +// Created by Xin Liu on 6/25/24. +// + +import Foundation + +/// https://docs.walletconnect.org/json-rpc-api-methods/ethereum#parameters-4 +public struct WCTransaction: Codable { + public var from: String + public var to: String? + public var data: String + public var gas: String? + public var gasPrice: String? + public var value: String? + public var nonce: String? + public var type: String? + public var accessList: [AccessListItem]? + public var chainId: String? + public var maxPriorityFeePerGas: String? + public var maxFeePerGas: String? + + /// https://eips.ethereum.org/EIPS/eip-2930 + public struct AccessListItem: Codable { + public var address: String + public var storageKeys: [String] + + public init(address: String, storageKeys: [String]) { + self.address = address + self.storageKeys = storageKeys + } + } + + public init(from: String, + to: String?, + data: String, + gas: String?, + gasPrice: String?, + value: String?, + nonce: String?, + type: String?, + accessList: [AccessListItem]?, + chainId: String?, + maxPriorityFeePerGas: String?, + maxFeePerGas: String?) { + self.from = from + self.to = to + self.data = data + self.gas = gas + self.gasPrice = gasPrice + self.value = value + self.nonce = nonce + self.type = type + self.accessList = accessList + self.chainId = chainId + self.maxPriorityFeePerGas = maxPriorityFeePerGas + self.maxFeePerGas = maxFeePerGas + } +} diff --git a/Planet/Labs/Wallet/WalletAccountView.swift b/Planet/Labs/Wallet/WalletAccountView.swift index 2e3e9f81..9899dc2a 100644 --- a/Planet/Labs/Wallet/WalletAccountView.swift +++ b/Planet/Labs/Wallet/WalletAccountView.swift @@ -9,6 +9,8 @@ import SwiftUI import Web3 struct WalletAccountView: View { + @EnvironmentObject private var planetStore: PlanetStore + var walletAddress: String @State private var avatarImage: NSImage? @State private var ensName: String? @@ -42,8 +44,8 @@ struct WalletAccountView: View { .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 2) } else { - Text(" ") - .font(Font.custom("Arial Rounded MT Bold", size: 24)) + Text(ViewUtils.getEmoji(from: walletAddress)) + .font(Font.custom("Arial Rounded MT Bold", size: (AVATAR_SIZE - 20))) .foregroundColor(Color.white) .contentShape(Rectangle()) .frame(width: AVATAR_SIZE, height: AVATAR_SIZE, alignment: .center) @@ -135,12 +137,15 @@ struct WalletAccountView: View { HStack(spacing: 8) { Button { - guard let session = WalletManager.shared.walletConnect.session else { - dismiss() - return +// guard let session = WalletManager.shared.walletConnect.session else { +// dismiss() +// return +// } +// try? WalletManager.shared.walletConnect.client.disconnect(from: session) +// dismiss() + Task { @MainActor in + self.planetStore.isShowingWalletDisconnectConfirmation.toggle() } - try? WalletManager.shared.walletConnect.client.disconnect(from: session) - dismiss() } label: { Text("Disconnect") }.help("Connected with \(walletAppName)") @@ -187,6 +192,19 @@ struct WalletAccountView: View { .onChange(of: currentActiveChainID) { _ in self.loadBalance() } + .confirmationDialog( + Text("Are you sure you want to disconnect?"), + isPresented: $planetStore.isShowingWalletDisconnectConfirmation + ) { + Button { + dismiss() + Task { + await WalletManager.shared.disconnectV2() + } + } label: { + Text("Disconnect") + } + } } private func loadTransactions() { @@ -218,7 +236,7 @@ struct WalletAccountView: View { private func setBasicInfo() { self.displayName = walletAddress.shortWalletAddress() - if let walletAppName = WalletManager.shared.walletConnect.session.walletInfo?.peerMeta.name + if let walletAppName = WalletManager.shared.session?.peer.name { self.walletAppName = walletAppName } @@ -260,14 +278,7 @@ struct WalletAccountView: View { // Get balance with Web3.swift let web3: Web3 let currentActiveChain = WalletManager.shared.currentNetwork() ?? .mainnet - switch currentActiveChain { - case .mainnet: - web3 = Web3(rpcURL: "https://cloudflare-eth.com") - case .goerli: - web3 = Web3(rpcURL: "https://eth-goerli.public.blastapi.io") - case .sepolia: - web3 = Web3(rpcURL: "https://eth-sepolia.public.blastapi.io") - } + web3 = Web3(rpcURL: currentActiveChain.rpcURL) web3.eth.blockNumber() { response in if response.status.isSuccess, let blockNumber = response.result { print("Block number: \(blockNumber)") diff --git a/Planet/Labs/Wallet/WalletManager.swift b/Planet/Labs/Wallet/WalletManager.swift index d210014f..cde2246d 100644 --- a/Planet/Labs/Wallet/WalletManager.swift +++ b/Planet/Labs/Wallet/WalletManager.swift @@ -5,38 +5,58 @@ // Created by Xin Liu on 11/4/22. // +import Auth +import Combine +import CoreImage.CIFilterBuiltins +import CryptoSwift import Foundation +import HDWalletKit import Starscream import WalletConnectNetworking import WalletConnectPairing import WalletConnectRelay +import WalletConnectSign import WalletConnectSwift import Web3 enum EthereumChainID: Int, Codable, CaseIterable { case mainnet = 1 - case goerli = 5 - case sepolia = 11155111 + case sepolia = 11_155_111 var id: Int { return self.rawValue } static let names: [Int: String] = [ 1: "Mainnet", - 5: "Goerli", - 11155111: "Sepolia", + 11_155_111: "Sepolia", ] static let coinNames: [Int: String] = [ 1: "ETH", - 5: "GoerliETH", - 11155111: "SEP", + 11_155_111: "SepoliaETH", ] static let etherscanURL: [Int: String] = [ 1: "https://etherscan.io", - 5: "https://goerli.etherscan.io", - 11155111: "https://sepolia.otterscan.io", + 11_155_111: "https://sepolia.otterscan.io", ] + + var rpcURL: String { + switch self { + case .mainnet: + return "https://eth.llamarpc.com" + case .sepolia: + return "https://eth-sepolia.public.blastapi.io" + } + } + + var etherscanAPI: String { + switch self { + case .mainnet: + return "https://api.etherscan.io/api" + case .sepolia: + return "https://api-sepolia.etherscan.io/api" + } + } } enum TipAmount: Int, Codable, CaseIterable { @@ -59,19 +79,26 @@ enum TipAmount: Int, Codable, CaseIterable { ] } -extension WebSocket: WebSocketConnecting { } +class WalletManager: NSObject, ObservableObject { + static let shared = WalletManager() -struct SocketFactory: WebSocketFactory { - func create(with url: URL) -> WebSocketConnecting { - return WebSocket(url: url) + enum SigningState { + case none + case signed(Cacao) + case error(Error) } -} -class WalletManager: NSObject { - static let shared = WalletManager() + static let lastWalletAddressKey: String = "PlanetLastActiveWalletAddressKey" var walletConnect: WalletConnect! + @Published private(set) var uriString: String? + @Published private(set) var state: SigningState = .none + + private var disposeBag = Set() + + @Published var session: WalletConnectSign.Session? = nil + // MARK: - Common func currentNetwork() -> EthereumChainID? { let chainId = UserDefaults.standard.integer(forKey: String.settingsEthereumChainId) @@ -84,21 +111,11 @@ class WalletManager: NSObject { return EthereumChainID.names[chainId.id] ?? "Mainnet" } - func connectedWalletChainId() -> Int? { - return self.walletConnect.session.walletInfo?.chainId - } - - func canSwitchNetwork() -> Bool { - return self.walletConnect.session.walletInfo?.peerMeta.name.contains("MetaMask") ?? false - } - func etherscanURLString(tx: String, chain: EthereumChainID? = nil) -> String { let chain = chain ?? WalletManager.shared.currentNetwork() - switch (chain) { + switch chain { case .mainnet: return "https://etherscan.io/tx/" + tx - case .goerli: - return "https://goerli.etherscan.io/tx/" + tx case .sepolia: return "https://sepolia.otterscan.io/tx/" + tx default: @@ -108,11 +125,9 @@ class WalletManager: NSObject { func etherscanURLString(address: String, chain: EthereumChainID? = nil) -> String { let chain = chain ?? WalletManager.shared.currentNetwork() - switch (chain) { + switch chain { case .mainnet: return "https://etherscan.io/address/" + address - case .goerli: - return "https://goerli.etherscan.io/address/" + address case .sepolia: return "https://sepolia.otterscan.io/address/" + address default: @@ -121,11 +136,11 @@ class WalletManager: NSObject { } func getWalletAppImageName() -> String? { - if let walletInfo = self.walletConnect.session.walletInfo { - if walletInfo.peerMeta.name.contains("MetaMask") { + if let session = self.session { + if session.peer.name.contains("MetaMask") { return "WalletAppIconMetaMask" } - if walletInfo.peerMeta.name.contains("Rainbow") { + if session.peer.name.contains("Rainbow") { return "WalletAppIconRainbow" } } @@ -133,7 +148,11 @@ class WalletManager: NSObject { } func getWalletAppName() -> String { - return self.walletConnect.session.walletInfo?.peerMeta.name ?? "Unknown Wallet" + if let session = self.session { + return session.peer.name + } + return "WalletConnect 2.0" + // return self.walletConnect.session.walletInfo?.peerMeta.name ?? "Unknown Wallet" } // MARK: - V1 @@ -161,26 +180,242 @@ class WalletManager: NSObject { // MARK: - V2 func setupV2() throws { - let metadata = AppMetadata( - name: "Planet", - description: "Build decentralized websites on ENS", - url: "https://planetable.xyz", - icons: ["https://github.com/Planetable.png"]) - - if let projectId = Bundle.main.object(forInfoDictionaryKey: "WALLETCONNECTV2_PROJECT_ID") as? String { - Networking.configure(projectId: projectId, socketFactory: SocketFactory()) + debugPrint("Setting up WalletConnect 2.0") + if let projectId = Bundle.main.object(forInfoDictionaryKey: "WALLETCONNECTV2_PROJECT_ID") + as? String + { + debugPrint("WalletConnect project id: \(projectId)") + let metadata = AppMetadata( + name: "Planet", + description: "Build decentralized websites on ENS", + url: "https://planetable.xyz", + icons: ["https://github.com/Planetable.png"], + redirect: AppMetadata.Redirect(native: "planet://", universal: nil) + ) Pair.configure(metadata: metadata) + Networking.configure(projectId: projectId, socketFactory: DefaultSocketFactory()) + + // Set up Sign + + // Sign: sessionSettlePublisher + Sign.instance.sessionSettlePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] (session: WalletConnectSign.Session) in + debugPrint("WalletConnect 2.0 Session Settled: \(session)") + self.session = session + debugPrint("WalletConnect 2.0 session found: \(session)") + if let account = session.accounts.first { + Task { @MainActor in + PlanetStore.shared.walletAddress = account.address + UserDefaults.standard.set( + account.address, + forKey: Self.lastWalletAddressKey + ) + PlanetStore.shared.isShowingWalletConnectV2QRCode = false + } + } + }.store(in: &disposeBag) + + // Sign: sessionDeletePublisher + Sign.instance.sessionDeletePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + debugPrint("WalletConnect 2.0 Session Deleted") + self.session = nil + Task { @MainActor in + PlanetStore.shared.walletAddress = "" + UserDefaults.standard.removeObject(forKey: Self.lastWalletAddressKey) + PlanetStore.shared.isShowingWalletConnectV2QRCode = false + } + }.store(in: &disposeBag) + + // Sign: sessionRejectionPublisher + Sign.instance.sessionRejectionPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] rejection in + debugPrint("WalletConnect 2.0 Session Rejection: \(rejection)") + Task { @MainActor in + PlanetStore.shared.isShowingWalletConnectV2QRCode = false + } + }.store(in: &disposeBag) + + // Sign: sessionEventPublisher + Sign.instance.sessionEventPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] (event, topic, chain) in + debugPrint( + "WalletConnect 2.0 Session Event: event: \(event) topic: \(topic) blockchain: \(chain)" + ) + }.store(in: &disposeBag) + + // Sign: sessionResponsePublisher + Sign.instance.sessionResponsePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] response in + let record = Sign.instance.getSessionRequestRecord(id: response.id)! + switch response.result { + case .response(let response): + Task { + #if DEBUG + let chain = EthereumChainID.sepolia + #else + let chain = EthereumChainID.mainnet + #endif + do { + if let hash = response.value as? String { + debugPrint("Response value: \(hash)") + // Wait for 10 seconds for the transaction + try await Task.sleep(seconds: 10) + debugPrint("Try to get transaction by response value: \(hash) on \(chain)") + if let transaction = try await self.getTransaction(by: hash, on: chain) { + debugPrint("WalletConnect 2.0 Transaction: \(transaction)") + self.saveTransaction(transaction, on: chain) + } else { + debugPrint("Failed to extract transaction from response value \(hash)") + } + } else { + debugPrint("Failed to extract response value from \(response)") + } + } catch { + debugPrint("Failed to get transaction for response value \(response): \(error)") + } + } + debugPrint("WalletConnect 2.0 Sign Response: \(response)") + debugPrint("WalletConnect 2.0 Sign Request Record: \(record)") + debugPrint("WalletConnect 2.0 Sign Request: \(record.request)") + // TODO: Save the transaction + // responseView.nameLabel.text = "Received Response\n\(record.request.//method)" + // responseView.descriptionLabel.text = try! response.get(String.self).description + case .error(let error): + debugPrint("WalletConnect 2.0 Sign Error: \(error)") + // responseView.nameLabel.text = "Received Error\n\(record.request.method)" + // responseView.descriptionLabel.text = error.message + } + Task { @MainActor in + PlanetStore.shared.isShowingWalletConnectV2QRCode = false + } + }.store(in: &disposeBag) + + // Set up Auth + Auth.configure(crypto: DefaultCryptoProvider()) + Auth.instance.authResponsePublisher.sink { [weak self] (_, result) in + DispatchQueue.main.async { + switch result { + case .success(let cacao): + self?.state = .signed(cacao) + debugPrint("WalletConnect 2.0 signed in: \(cacao)") + let iss = cacao.p.iss + debugPrint("iss: \(iss)") + if iss.contains("eip155:1:"), let address = iss.split(separator: ":").last { + let walletAddress: String = String(address) + PlanetStore.shared.walletAddress = walletAddress + UserDefaults.standard.set( + walletAddress, + forKey: Self.lastWalletAddressKey + ) + // Save pairing topic into keychain, with wallet address as key. + if let pairing = Pair.instance.getPairings().first { + do { + try KeychainHelper.shared.saveValue( + pairing.topic, + forKey: walletAddress + ) + debugPrint( + "WalletConnect 2.0 topic saved: \(pairing.topic), key (wallet address): \(walletAddress)" + ) + } + catch { + debugPrint("WalletConnect 2.0 topic not saved: \(error)") + } + } + } + case .failure(let error): + debugPrint("WalletConnect 2.0 not signed, error: \(error)") + self?.state = .error(error) + PlanetStore.shared.walletAddress = "" + } + PlanetStore.shared.isShowingWalletConnectV2QRCode = false + } + }.store(in: &disposeBag) + + // Restore previous session Task { @MainActor in + debugPrint("WalletConnect 2.0 ready") PlanetStore.shared.walletConnectV2Ready = true + + // TODO: code for handling Sign + // TODO: Since Sign can work with MetaMask, we'll remove Auth after Sign is fully working. + + if let session = Sign.instance.getSessions().first { + self.session = session + debugPrint("WalletConnect 2.0 session found: \(session)") + if let account = session.accounts.first { + let address = account.address + PlanetStore.shared.walletAddress = address + debugPrint("WalletConnect 2.0 account: \(account)") + //debugPrint("WalletConnect 2.0 session wallet address: \(address)") + } + } + else { + debugPrint("WalletConnect 2.0 no session found") + } + + /* Start: code for handling Auth */ + guard + let address: String = UserDefaults.standard.string( + forKey: Self.lastWalletAddressKey + ), address != "" + else { + debugPrint( + "WalletConnect 2.0 no previous active wallet found, ignore reconnect." + ) + return + } + do { + let topic = try KeychainHelper.shared.loadValue(forKey: address) + if topic.count > 0 { + debugPrint( + "WalletConnect 2.0 previous active wallet address found: \(address), with topic: \(topic)" + ) + // Ping + do { + try await Pair.instance.ping(topic: topic) + PlanetStore.shared.walletAddress = address + debugPrint( + "WalletConnect 2.0 pinged previous active wallet address OK: \(address)" + ) + } + catch { + debugPrint( + "WalletConnect 2.0 failed to ping previous active wallet address: \(error)" + ) + } + } + else { + debugPrint( + "WalletConnect 2.0 previous active wallet address found: \(address), but topic not found." + ) + } + } + catch { + debugPrint( + "WalletConnect 2.0 failed to restore previous active wallet address and topic: \(error)" + ) + } + /* End: code for handling Auth */ } - } else { + } + else { + debugPrint("WalletConnect 2.0 not ready, missing project id error.") throw PlanetError.WalletConnectV2ProjectIDMissingError } - } - func connectV2() { + @MainActor + func connectV2() async throws { + /* Task { + debugPrint("Attempting to create WalletConnect 2.0 session") let uri = try await Pair.instance.create() debugPrint("WalletConnect 2.0 URI: \(uri)") debugPrint("WalletConnect 2.0 URI Absolute String: \(uri.absoluteString)") @@ -189,7 +424,341 @@ class WalletManager: NSObject { PlanetStore.shared.isShowingWalletConnectV2QRCode = true } } + */ + state = .none + uriString = nil + let uri = try await Pair.instance.create() + debugPrint("WalletConnect 2.0 URI: \(uri)") + debugPrint("WalletConnect 2.0 URI Absolute String: \(uri.absoluteString)") + uriString = uri.absoluteString + // Auth, new, but MetaMask doesn't support Auth yet. + // try await Auth.instance.request(.stub(), topic: uri.topic) + + // Sign, simple old connect method that MetaMask supports too. + + let pairingTopic = uri.topic + let requiredNamespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: [ + Blockchain("eip155:1")! + ], + methods: [ + "eth_sendTransaction", + "personal_sign", + "eth_signTypedData", + ], + events: [] + ) + ] + let optionalNamespaces: [String: ProposalNamespace] = [ + "eip155": ProposalNamespace( + chains: [ + Blockchain("eip155:11155111")! + ], + methods: [ + "eth_sendTransaction", + "eth_signTransaction", + "get_balance", + "personal_sign", + ], + events: [] + ) + ] + + try await Sign.instance.connect( + requiredNamespaces: requiredNamespaces, + optionalNamespaces: optionalNamespaces, + topic: pairingTopic + ) + + PlanetStore.shared.walletConnectV2ConnectionURL = uri.absoluteString + PlanetStore.shared.isShowingWalletConnectV2QRCode = true + } + + func disconnectV2() async { + // Disconnect from current session + // This action is verified with OKX wallet + if let session = session { + let topic = session.topic + do { + try await Sign.instance.disconnect(topic: topic) + debugPrint("WalletConnect 2.0 disconnected session: \(topic)") + } + catch { + debugPrint("WalletConnect 2.0 failed to disconnect session: \(error)") + } + } + // Clean up all sessions + let sessions = Sign.instance.getSessions() + debugPrint("WalletConnect 2.0 sessions: \(sessions.count) found") + do { + try await Sign.instance.cleanup() + self.session = nil + Task { @MainActor in + PlanetStore.shared.walletAddress = "" + } + } + catch { + debugPrint("WalletConnect 2.0 failed to perform Sign.instance.cleanup(): \(error)") + } + + // Disconnect all pairings if any + let pairings = Pair.instance.getPairings() + debugPrint("WalletConnect 2.0 pairings: \(pairings.count) found") + for pairing in pairings { + debugPrint("WalletConnect 2.0 about to disconnect pairing: \(pairing)") + do { + try await Pair.instance.disconnect(topic: pairing.topic) + debugPrint("WalletConnect 2.0 disconnected pairing: \(pairing)") + } + catch { + debugPrint("WalletConnect 2.0 failed to disconnect pairing: \(error)") + } + } + + guard let address: String = UserDefaults.standard.string(forKey: Self.lastWalletAddressKey), + address != "" + else { + debugPrint("WalletConnect 2.0 no previous active wallet found") + return + } + do { + let topic = try KeychainHelper.shared.loadValue(forKey: address) + try await Pair.instance.disconnect(topic: topic) + debugPrint("WalletConnect 2.0 disconnected previous active wallet address: \(address)") + } + catch { + debugPrint("WalletConnect 2.0 failed to disconnect Auth pairing: \(error)") + } + Task { @MainActor in + self.session = nil + PlanetStore.shared.walletAddress = "" + } + UserDefaults.standard.removeObject(forKey: Self.lastWalletAddressKey) + do { + try KeychainHelper.shared.delete(forKey: address) + } + catch { + debugPrint( + "WalletConnect 2.0 failed to delete topic in Keychain for previous active wallet address: \(error)" + ) + } + } + + func sendTransactionV2(receiver: String, amount: Int, memo: String, ens: String? = nil, gas: Int? = nil) async { + if let session = self.session { + /* example code to sign a message + let method = "personal_sign" + let walletAddress = session.accounts[0].address + let requestParams = AnyCodable(["0x4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031363533333933373535313531", walletAddress]) + */ + let method = "eth_sendTransaction" + let walletAddress = session.accounts[0].address + let tx = self.tipTransaction( + from: walletAddress, + to: receiver, + amount: amount, + memo: memo, + gas: gas + ) + let requestParams = AnyCodable([ + tx + ]) + #if DEBUG + // Send transaction on Sepolia testnet + let request = Request( + topic: session.topic, + method: method, + params: requestParams, + chainId: Blockchain("eip155:11155111")! + ) + #else + // Send transaction on Ethereum mainnet + let request = Request( + topic: session.topic, + method: method, + params: requestParams, + chainId: Blockchain("eip155:1")! + ) + #endif + do { + try await Sign.instance.request(params: request) + } + catch { + debugPrint("WalletConnect 2.0 sendTransactionV2 error: \(error)") + } + } + } + + func tipTransaction(from sender: String, to receiver: String, amount: Int, memo: String, gas: Int? = nil) + -> Client.Transaction + { + let tipAmount = amount * 10_000_000_000_000_000 // Tip Amount: X * 0.01 ETH + let value = String(tipAmount, radix: 16) + var memoEncoded: String = memo.asTransactionData() + #if DEBUG + let chainId = 11_155_111 + #else + let chainId = 1 + #endif + let gasPrice: String? = gas?.gweiToHex() + return Client.Transaction( + from: sender, + to: receiver, + data: memoEncoded, + gas: nil, + gasPrice: gasPrice, + value: "0x\(value)", + nonce: nil, + type: nil, + accessList: nil, + chainId: String(format: "0x%x", chainId), + maxPriorityFeePerGas: nil, + maxFeePerGas: nil + ) + } + + func getTransaction(by hash: String, on chain: EthereumChainID = .mainnet) async throws + -> EthereumTransactionObject? + { + let web3 = Web3(rpcURL: chain.rpcURL) + do { + let transactionHash = try EthereumData(ethereumValue: hash) + return try await withCheckedThrowingContinuation { continuation in + web3.eth.getTransactionByHash(blockHash: transactionHash) { response in + if response.status.isSuccess, let transaction = response.result { + debugPrint( + "Transaction on \(chain): \(transaction?.from.hex(eip55: true)) -> \(transaction?.to?.hex(eip55: true) ?? "") \(transaction?.value)" + ) + continuation.resume(returning: transaction) + } + else if let error = response.error { + debugPrint("Error: \(error)") + continuation.resume(throwing: error) + } + else { + debugPrint("Transaction not found") + continuation.resume(returning: nil) + } + } + } + } + catch { + debugPrint("Error: \(error)") + throw error + } + } + + func saveTransaction(_ tx: EthereumTransactionObject, on chain: EthereumChainID) { + do { + debugPrint("About to save transaction on \(chain): \(tx.hash.hex()) \(tx)") + let memo = tx.input.hex().hexToString() ?? tx.input.hex() + var ens: String? = nil + if memo.contains(".eth") { + ens = memo.split(separator: ":").last?.split(separator: "/").first?.description + } + let record = EthereumTransaction( + id: tx.hash.hex(), + chainID: chain.id, + from: tx.from.hex(eip55: true), + to: tx.to?.hex(eip55: true) ?? "", + toENS: ens, + amount: Int(tx.value.quantity / 10_000_000_000_000_000), + memo: memo + ) + try record.save() + } catch { + debugPrint("Failed to save transaction on \(chain): \(tx)") + } } + + func getTransactions(for address: String) async { + if let apiToken = Bundle.main.object(forInfoDictionaryKey: "ETHERSCAN_API_TOKEN") as? String { + for chain in EthereumChainID.allCases { + debugPrint("Fetching transactions for \(address) on \(chain)") + var etherscanAPIPrefix = chain.etherscanAPI + var apiCall = etherscanAPIPrefix + "?module=account&action=txlist&address=\(address)&startblock=0&endblock=99999999&page=1&offset=0&sort=asc&apikey=\(apiToken)" + if let url = URL(string: apiCall) { + do { + let (data, _) = try await URLSession.shared.data(from: url) + let decoder = JSONDecoder() + let response = try decoder.decode(EtherscanResponse.self, from: data) + debugPrint("Transactions: \(response.result.count)") + for tx in response.result { + debugPrint("Transaction on \(chain): \(tx)") + saveEtherscanTransaction(tx, on: chain) + } + } catch { + debugPrint("Error: \(error)") + } + } + } + } + } + + func saveEtherscanTransaction(_ tx: EtherscanTransaction, on chain: EthereumChainID) { + do { + debugPrint("About to save etherscan transaction on \(chain): \(tx.hash) \(tx)") + let memo = tx.input.hexToString() ?? tx.input + var ens: String? = nil + if memo.contains(".eth") { + ens = memo.split(separator: ":").last?.split(separator: "/").first?.description + } + let amount: Int + if let value = Int(tx.value) { + amount = value / 10_000_000_000_000_000 + } else { + amount = 0 + } + let created = Date(timeIntervalSince1970: Double(tx.timeStamp) ?? Date().timeIntervalSince1970) + let record = EthereumTransaction( + id: tx.hash, + chainID: chain.id, + from: tx.from, + to: tx.to, + toENS: ens, + amount: amount, + memo: memo, + created: created + ) + if record.exists() { + debugPrint("Transaction already exists: \(record)") + } else { + try record.save() + } + } catch { + debugPrint("Failed to save transaction on \(chain): \(tx)") + } + } +} + +struct EtherscanResponse: Codable { + let status: String + let message: String + let result: [EtherscanTransaction] +} + +struct EtherscanTransaction: Codable { + let blockNumber: String + let timeStamp: String + let hash: String + let nonce: String + let blockHash: String + let transactionIndex: String + let from: String + let to: String + let value: String + let gas: String + let gasPrice: String + let isError: String + let txreceipt_status: String + let input: String + let contractAddress: String + let cumulativeGasUsed: String + let gasUsed: String + let confirmations: String + let methodId: String + let functionName: String } // MARK: - WalletConnectDelegate @@ -204,7 +773,8 @@ extension WalletManager: WalletConnectDelegate { func didConnect() { Task { @MainActor in PlanetStore.shared.isShowingWalletConnectV1QRCode = false - PlanetStore.shared.walletAddress = self.walletConnect.session.walletInfo?.accounts[0] ?? "" + PlanetStore.shared.walletAddress = + self.walletConnect.session.walletInfo?.accounts[0] ?? "" debugPrint("Wallet Address: \(PlanetStore.shared.walletAddress)") debugPrint("Session: \(self.walletConnect.session)") } @@ -221,7 +791,8 @@ extension PlanetStore { func hasWalletAddress() -> Bool { if walletAddress.count > 0 { return true - } else { + } + else { return false } } @@ -234,8 +805,128 @@ extension Int { var ethers: Float = Float(self) / 100 return String(format: "%.2f Ξ", ethers) } - + func stringValue() -> String { return String(self) } } + +extension RequestParams { + static func stub( + domain: String = "service.invalid", + chainId: String = "eip155:1", + nonce: String = "32891756", + aud: String = "https://service.invalid/login", + nbf: String? = nil, + exp: String? = nil, + statement: String? = + "I accept the ServiceOrg Terms of Service: https://service.invalid/tos", + requestId: String? = nil, + resources: [String]? = [ + "ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", + "https://example.com/my-web2-claim.json", + ] + ) -> RequestParams { + return RequestParams( + domain: domain, + chainId: chainId, + nonce: nonce, + aud: aud, + nbf: nbf, + exp: exp, + statement: statement, + requestId: requestId, + resources: resources + ) + } +} + +extension WebSocket: WebSocketConnecting {} + +struct DefaultSocketFactory: WebSocketFactory { + func create(with url: URL) -> WebSocketConnecting { + let socket = WebSocket(url: url) + let queue = DispatchQueue(label: "com.walletconnect.sdk.sockets", attributes: .concurrent) + socket.callbackQueue = queue + return socket + } +} + +struct DefaultCryptoProvider: CryptoProvider { + public func recoverPubKey(signature: EthereumSignature, message: Data) throws -> Data { + let publicKey = try EthereumPublicKey( + message: message.bytes, + v: EthereumQuantity(quantity: BigUInt(signature.v)), + r: EthereumQuantity(signature.r), + s: EthereumQuantity(signature.s) + ) + return Data(publicKey.rawPublicKey) + } + + public func keccak256(_ data: Data) -> Data { + let digest = SHA3(variant: .keccak256) + let hash = digest.calculate(for: [UInt8](data)) + return Data(hash) + } +} + +extension Data { + public func toHexString() -> String { + return map({ String(format: "%02x", $0) }).joined() + } +} + +extension String { + public func asTransactionData() -> String { + let data = self.data(using: .utf8)! + return "0x" + data.toHexString() + } +} + +extension String { + func hexToString() -> String? { + var hex = self + // Remove the "0x" prefix if it exists + if hex.hasPrefix("0x") { + hex = String(hex.dropFirst(2)) + } + + // Convert hex string to Data + guard let data = Data(hexString: hex) else { + return nil + } + + // Convert Data to String + return String(data: data, encoding: .utf8) + } +} + +extension Data { + init?(hexString: String) { + let length = hexString.count / 2 + var data = Data(capacity: length) + var hex = hexString + + for _ in 0.. String { + // Convert Gwei to Wei (1 Gwei = 10^9 Wei) + let wei = self * 1_000_000_000 + // Convert Wei to a hexadecimal string + let hexString = String(wei, radix: 16) + return "0x\(hexString)" + } +} diff --git a/Planet/PlanetAppDelegate.swift b/Planet/PlanetAppDelegate.swift index 3f7366a8..252dd9fe 100644 --- a/Planet/PlanetAppDelegate.swift +++ b/Planet/PlanetAppDelegate.swift @@ -120,14 +120,14 @@ class PlanetAppDelegate: NSObject, NSApplicationDelegate { // Connect Wallet V1 - WalletManager.shared.setupV1() +// WalletManager.shared.setupV1() // Connect Wallet V2 if let wc2Enabled: Bool = Bundle.main.object(forInfoDictionaryKey: "WALLETCONNECTV2_ENABLED") as? Bool, wc2Enabled == true { do { try WalletManager.shared.setupV2() } catch { - debugPrint("WalletConnectV2: Failed to prepare the connection: \(error)") + debugPrint("WalletConnect 2.0 Failed to prepare the connection: \(error)") } } } diff --git a/Planet/Views/My/MyPlanetEditView.swift b/Planet/Views/My/MyPlanetEditView.swift index d52dda98..27136be7 100644 --- a/Planet/Views/My/MyPlanetEditView.swift +++ b/Planet/Views/My/MyPlanetEditView.swift @@ -612,7 +612,7 @@ struct MyPlanetEditView: View { ColorPicker("", selection: $selectedColor) .onChange(of: selectedColor) { color in - let hex = color.toHexString() + let hex = color.toHexValue() userSettings["highlightColor"] = hex } diff --git a/Planet/Views/PlanetMainView.swift b/Planet/Views/PlanetMainView.swift index 321c2a18..8c89dab1 100644 --- a/Planet/Views/PlanetMainView.swift +++ b/Planet/Views/PlanetMainView.swift @@ -109,6 +109,7 @@ struct PlanetMainView: View { } .sheet(isPresented: $planetStore.isShowingWalletAccount) { WalletAccountView(walletAddress: planetStore.walletAddress) + .environmentObject(planetStore) } .sheet(isPresented: $planetStore.isQuickSharing) { PlanetQuickShareView() @@ -130,7 +131,11 @@ struct PlanetMainView: View { isPresented: $planetStore.isShowingWalletDisconnectConfirmation ) { Button() { - try? WalletManager.shared.walletConnect.client.disconnect(from: WalletManager.shared.walletConnect.session) + Task { + await WalletManager.shared.disconnectV2() + } + // V1: + // try? WalletManager.shared.walletConnect.client.disconnect(from: WalletManager.shared.walletConnect.session) } label: { Text("Disconnect") } diff --git a/Planet/Views/Sidebar/AccountBadgeView.swift b/Planet/Views/Sidebar/AccountBadgeView.swift index 1b64bb0e..01adaa9a 100644 --- a/Planet/Views/Sidebar/AccountBadgeView.swift +++ b/Planet/Views/Sidebar/AccountBadgeView.swift @@ -182,19 +182,27 @@ struct AccountBadgeView: View { } } + /* Test transaction + Task { + let hash = "0xd514f7f145cb8dc8d8b015b6125b28645ceacede00b260cf4967f060c32fa73a" + do { + if let tx = try await WalletManager.shared.getTransaction(by: hash, on: .sepolia) { + WalletManager.shared.saveTransaction(tx, on: .sepolia) + } + } catch { + debugPrint("Failed to get test transaction \(hash): \(error)") + } + } + */ + Task { + await WalletManager.shared.getTransactions(for: walletAddress) + } // Get balance with Web3.swift let web3: Web3 let currentActiveChain = EthereumChainID.allCases.first(where: { $0.id == currentActiveChainID })! - switch currentActiveChain { - case .mainnet: - web3 = Web3(rpcURL: "https://eth.llamarpc.com") - case .goerli: - web3 = Web3(rpcURL: "https://eth-goerli.public.blastapi.io") - case .sepolia: - web3 = Web3(rpcURL: "https://eth-sepolia.public.blastapi.io") - } + web3 = Web3(rpcURL: currentActiveChain.rpcURL) web3.eth.blockNumber { response in if response.status.isSuccess, let blockNumber = response.result { print("Block number: \(blockNumber)")