From 9e3afc87bc3151947b0fbc0390407e8e9e137256 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 11 Sep 2024 17:12:51 -0700 Subject: [PATCH 1/2] feat: Safe integration --- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../ApplicationLayer/AppDelegate.swift | 1 + .../WalletApp/BusinessLayer/AccountMock.swift | 81 ++++++++++++++++++- .../Wallet/Main/MainModule.swift | 4 + .../Wallet/Settings/SettingsPresenter.swift | 25 +++++- .../Wallet/Settings/SettingsView.swift | 25 ++++++ 6 files changed, 135 insertions(+), 7 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 401cba6b..c40c2116 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -96,13 +96,13 @@ "repositoryURL": "https://github.com/apple/swift-docc-plugin", "state": { "branch": null, - "revision": "26ac5758409154cc448d7ab82389c520fa8a8247", - "version": "1.3.0" + "revision": "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", + "version": "1.4.3" } }, { "package": "SymbolKit", - "repositoryURL": "https://github.com/apple/swift-docc-symbolkit", + "repositoryURL": "https://github.com/swiftlang/swift-docc-symbolkit", "state": { "branch": null, "revision": "b45d1f2ed151d057b54504d653e0da5552844e34", diff --git a/Example/WalletApp/ApplicationLayer/AppDelegate.swift b/Example/WalletApp/ApplicationLayer/AppDelegate.swift index 7b3607e9..34f4e4e2 100644 --- a/Example/WalletApp/ApplicationLayer/AppDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/AppDelegate.swift @@ -11,6 +11,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { let entryPointAddress = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" // v0.7 on Sepolia let chainId = 11155111 // Sepolia SmartAccount.instance.configure(entryPoint: entryPointAddress, chainId: chainId) + SmartAccountSafe.instance.configure(entryPoint: entryPointAddress, chainId: chainId) return true } diff --git a/Example/WalletApp/BusinessLayer/AccountMock.swift b/Example/WalletApp/BusinessLayer/AccountMock.swift index 4c97688c..f16b3346 100644 --- a/Example/WalletApp/BusinessLayer/AccountMock.swift +++ b/Example/WalletApp/BusinessLayer/AccountMock.swift @@ -14,7 +14,7 @@ class AccountClientMock: YttriumWrapper.AccountClientProtocol { private var config: Yttrium.Config - required init(ownerAddress: String, entryPoint: String, chainId: Int, config: Yttrium.Config) { + required init(ownerAddress: String, entryPoint: String, chainId: Int, config: Yttrium.Config, safe: Bool) { self.ownerAddress = ownerAddress self.entryPoint = entryPoint self.chainId = chainId @@ -113,7 +113,8 @@ class SmartAccount { entryPoint: config.entryPoint, chainId: config.chainId, // config: pimlicoSepolia - config: localConfig + config: localConfig, + safe: false ) client.register(privateKey: privateKey) @@ -138,3 +139,79 @@ class SmartAccount { let chainId: Int } } + +class SmartAccountSafe { + + static var instance = SmartAccountSafe() + + private var client: AccountClient? { + didSet { + if let _ = client { + clientSetContinuation?.resume() + } + } + } + + private var clientSetContinuation: CheckedContinuation? + + private var config: Config? + + private init() {} + + public func configure(entryPoint: String, chainId: Int) { + self.config = Config( + entryPoint: entryPoint, + chainId: chainId + ) + } + + public func register(owner: String, privateKey: String) { + guard let config = self.config else { + fatalError("Error - you must call SmartAccount.configure(entryPoint:chainId:onSign:) before accessing the shared instance.") + } + assert(owner.count == 40) + + let localConfig = YttriumWrapper.Config.local() + +// let PIMLICO_BUNDLER_URL = "https://api.pimlico.io/v2/11155111/rpc?apikey=" +// let PIMLICO_RPC_URL = "https://rpc.ankr.com/eth_sepolia" +// let pimlicoSepolia = YttriumWrapper.Config( +// endpoints: .init( +// rpc: .init(baseURL: PIMLICO_RPC_URL), +// bundler: .init(baseURL: PIMLICO_BUNDLER_URL), +// paymaster: .init(baseURL: PIMLICO_BUNDLER_URL) +// ) +// ) + + let client = AccountClient( + ownerAddress: owner, + entryPoint: config.entryPoint, + chainId: config.chainId, +// config: pimlicoSepolia + config: localConfig, + safe: true + ) + client.register(privateKey: privateKey) + + self.client = client + } + + + public func getClient() async -> AccountClient { + if let client = client { + return client + } + + await withCheckedContinuation { continuation in + self.clientSetContinuation = continuation + } + + return client! + } + + struct Config { + let entryPoint: String + let chainId: Int + } +} + diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift index e53fe133..dbbbfda5 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift @@ -25,6 +25,10 @@ final class MainModule { owner: ownerAddress, privateKey: privateKey ) + SmartAccountSafe.instance.register( + owner: ownerAddress, + privateKey: privateKey + ) // SmartAccount.instance.register(onSign: { (messageToSign: String) in // func dataToHash(_ data: Data) -> Bytes { // let prefix = "\u{19}Ethereum Signed Message:\n" diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 746cdce8..9774c353 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -10,6 +10,7 @@ final class SettingsPresenter: ObservableObject { private let accountStorage: AccountStorage private var disposeBag = Set() @Published var smartAccount: String = "Loading..." + @Published var smartAccountSafe: String = "Loading..." init(interactor: SettingsInteractor, router: SettingsRouter, accountStorage: AccountStorage) { defer { setupInitialState() } @@ -17,9 +18,9 @@ final class SettingsPresenter: ObservableObject { self.router = router self.accountStorage = accountStorage fetchSmartAccount() - + fetchSmartAccountSafe() } - + func fetchSmartAccount() { Task { do { @@ -39,6 +40,26 @@ final class SettingsPresenter: ObservableObject { private func getSmartAccount() async throws -> String { try await SmartAccount.instance.getClient().getAccount().absoluteString } + + func fetchSmartAccountSafe() { + Task { + do { + let smartAccount = try await getSmartAccountSafe() + DispatchQueue.main.async { + self.smartAccountSafe = smartAccount + } + } catch { + DispatchQueue.main.async { + self.smartAccountSafe = "Failed to load" + } + print("Failed to get smart account safe: \(error)") + } + } + } + + private func getSmartAccountSafe() async throws -> String { + try await SmartAccountSafe.instance.getClient().getAccount().absoluteString + } var account: String { guard let importAccount = accountStorage.importAccount else { return .empty } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 3a5ac102..5f35e427 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -15,6 +15,7 @@ struct SettingsView: View { header(title: "Account") row(title: "CAIP-10", subtitle: viewModel.account) row(title: "Smart Account", subtitle: viewModel.smartAccount) + row(title: "Smart Account Safe", subtitle: viewModel.smartAccountSafe) row(title: "Private key", subtitle: viewModel.privateKey) } .padding(.horizontal, 20) @@ -52,6 +53,20 @@ struct SettingsView: View { .stroke(Color.green, lineWidth: 1) ) .padding(.bottom, 24) + + AsyncButton { + try await sendTransactionSafe() + } label: { + Text("Send Transaction Safe") + .foregroundColor(.green) + .frame(maxWidth: .infinity) + } + .frame(height: 44.0) + .overlay( + RoundedRectangle(cornerRadius: Radius.m) + .stroke(Color.green, lineWidth: 1) + ) + .padding(.bottom, 24) AsyncButton { try await viewModel.logoutPressed() @@ -87,6 +102,16 @@ struct SettingsView: View { data: "0x68656c6c6f" )) } + + @discardableResult + func sendTransactionSafe() async throws -> String { + let client = await SmartAccountSafe.instance.getClient() + return try await client.sendTransaction(.init( + to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + value: "0", + data: "0x68656c6c6f" + )) + } func header(title: String) -> some View { HStack { From 66655750dd4114faf2f9d25a1e2f677abc1a0c75 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 12 Sep 2024 08:22:26 -0700 Subject: [PATCH 2/2] fix: default to local config --- .../WalletApp/BusinessLayer/SmartAccount.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/SmartAccount.swift b/Example/WalletApp/BusinessLayer/SmartAccount.swift index b0271d13..e58a0a3e 100644 --- a/Example/WalletApp/BusinessLayer/SmartAccount.swift +++ b/Example/WalletApp/BusinessLayer/SmartAccount.swift @@ -56,12 +56,17 @@ class SmartAccount { ) ) + let pickedConfig = if !(InputConfig.pimlicoBundlerUrl?.isEmpty ?? true) && !(InputConfig.rpcUrl?.isEmpty ?? true) { + pimlicoSepolia + } else { + localConfig + } + let client = AccountClient( ownerAddress: owner, entryPoint: config.entryPoint, chainId: config.chainId, - config: pimlicoSepolia, -// config: localConfig + config: pickedConfig, safe: false ) client.register(privateKey: privateKey) @@ -133,12 +138,17 @@ class SmartAccountSafe { ) ) + let pickedConfig = if !(InputConfig.pimlicoBundlerUrl?.isEmpty ?? true) && !(InputConfig.rpcUrl?.isEmpty ?? true) { + pimlicoSepolia + } else { + localConfig + } + let client = AccountClient( ownerAddress: owner, entryPoint: config.entryPoint, chainId: config.chainId, - config: pimlicoSepolia, -// config: localConfig + config: pickedConfig, safe: true ) client.register(privateKey: privateKey)