diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index a51bbbe9b..37793c64a 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -62,6 +62,9 @@ 847F08012A25DBFF00B2A5A4 /* XPlatformW3WTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */; }; 8486EDD32B4F2EA6008E53C3 /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 8486EDD22B4F2EA6008E53C3 /* SwiftMessages */; }; 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8487A9472A83AD680003D5AF /* LoggingService.swift */; }; + 848BD1DA2D34DF5E007C2AEF /* SmartAccountSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848BD1D92D34DF5E007C2AEF /* SmartAccountSigner.swift */; }; + 848BD1DC2D34DF72007C2AEF /* GasAbstractionSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848BD1DB2D34DF72007C2AEF /* GasAbstractionSigner.swift */; }; + 848BD1DE2D34DF8F007C2AEF /* EOASigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848BD1DD2D34DF8F007C2AEF /* EOASigner.swift */; }; 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7A2A9BA206007EBAC2 /* Mixpanel */; }; 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7C2A9BA328007EBAC2 /* Mixpanel */; }; 849D7A93292E2169006A2BD4 /* NotifyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* NotifyTests.swift */; }; @@ -70,6 +73,14 @@ 84AEC24F2B4D1EE400E27A5B /* ActivityIndicatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEC24E2B4D1EE400E27A5B /* ActivityIndicatorManager.swift */; }; 84AEC2512B4D42C100E27A5B /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */; }; 84AEC2542B4D43CD00E27A5B /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 84AEC2532B4D43CD00E27A5B /* SwiftMessages */; }; + 84AEF0162D3DD8CA006E43E5 /* SendStableCoinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF0152D3DD8CA006E43E5 /* SendStableCoinView.swift */; }; + 84AEF0192D3DD9C0006E43E5 /* SendStableCoinModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF0182D3DD9C0006E43E5 /* SendStableCoinModule.swift */; }; + 84AEF01B2D3DD9F1006E43E5 /* SendStableCoinRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF01A2D3DD9F1006E43E5 /* SendStableCoinRouter.swift */; }; + 84AEF01D2D3DDA0A006E43E5 /* SendStableCoinPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF01C2D3DDA0A006E43E5 /* SendStableCoinPresenter.swift */; }; + 84AEF01F2D3DE7A0006E43E5 /* UpgradeToSmartAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF01E2D3DE7A0006E43E5 /* UpgradeToSmartAccountView.swift */; }; + 84AEF0222D3DE7C8006E43E5 /* UpgradeToSmartAccountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF0212D3DE7C8006E43E5 /* UpgradeToSmartAccountPresenter.swift */; }; + 84AEF0242D3DE7D4006E43E5 /* UpgradeToSmartAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF0232D3DE7D4006E43E5 /* UpgradeToSmartAccountModule.swift */; }; + 84AEF0262D3DE7E0006E43E5 /* UpgradeToSmartAccountRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEF0252D3DE7E0006E43E5 /* UpgradeToSmartAccountRouter.swift */; }; 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154D2991099000FAD54E /* BuildConfiguration.swift */; }; 84B8155B2992A18D00FAD54E /* NotifyMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */; }; 84CA52172C88965C0069BB33 /* ReownRouter in Frameworks */ = {isa = PBXBuildFile; productRef = 84CA52162C88965C0069BB33 /* ReownRouter */; }; @@ -368,6 +379,9 @@ 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPlatformW3WTests.swift; sourceTree = ""; }; 8487A92E2A7BD2F30003D5AF /* XPlatformProtocolTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = XPlatformProtocolTests.xctestplan; path = ../XPlatformProtocolTests.xctestplan; sourceTree = ""; }; 8487A9472A83AD680003D5AF /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = ""; }; + 848BD1D92D34DF5E007C2AEF /* SmartAccountSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartAccountSigner.swift; sourceTree = ""; }; + 848BD1DB2D34DF72007C2AEF /* GasAbstractionSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GasAbstractionSigner.swift; sourceTree = ""; }; + 848BD1DD2D34DF8F007C2AEF /* EOASigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EOASigner.swift; sourceTree = ""; }; 849A4F18298281E300E61ACE /* WalletAppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletAppRelease.entitlements; sourceTree = ""; }; 849A4F19298281F100E61ACE /* PNDecryptionServiceRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionServiceRelease.entitlements; sourceTree = ""; }; 849D7A92292E2169006A2BD4 /* NotifyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyTests.swift; sourceTree = ""; }; @@ -375,6 +389,14 @@ 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; 84AEC24E2B4D1EE400E27A5B /* ActivityIndicatorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorManager.swift; sourceTree = ""; }; 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; + 84AEF0152D3DD8CA006E43E5 /* SendStableCoinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStableCoinView.swift; sourceTree = ""; }; + 84AEF0182D3DD9C0006E43E5 /* SendStableCoinModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStableCoinModule.swift; sourceTree = ""; }; + 84AEF01A2D3DD9F1006E43E5 /* SendStableCoinRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStableCoinRouter.swift; sourceTree = ""; }; + 84AEF01C2D3DDA0A006E43E5 /* SendStableCoinPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendStableCoinPresenter.swift; sourceTree = ""; }; + 84AEF01E2D3DE7A0006E43E5 /* UpgradeToSmartAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeToSmartAccountView.swift; sourceTree = ""; }; + 84AEF0212D3DE7C8006E43E5 /* UpgradeToSmartAccountPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeToSmartAccountPresenter.swift; sourceTree = ""; }; + 84AEF0232D3DE7D4006E43E5 /* UpgradeToSmartAccountModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeToSmartAccountModule.swift; sourceTree = ""; }; + 84AEF0252D3DE7E0006E43E5 /* UpgradeToSmartAccountRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradeToSmartAccountRouter.swift; sourceTree = ""; }; 84B8154D2991099000FAD54E /* BuildConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; 84B8155A2992A18D00FAD54E /* NotifyMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyMessageViewModel.swift; sourceTree = ""; }; 84CE641C27981DED00142511 /* DApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -828,6 +850,28 @@ path = Push; sourceTree = ""; }; + 84AEF0172D3DD8D3006E43E5 /* SendStableCoin */ = { + isa = PBXGroup; + children = ( + 84AEF0152D3DD8CA006E43E5 /* SendStableCoinView.swift */, + 84AEF0182D3DD9C0006E43E5 /* SendStableCoinModule.swift */, + 84AEF01C2D3DDA0A006E43E5 /* SendStableCoinPresenter.swift */, + 84AEF01A2D3DD9F1006E43E5 /* SendStableCoinRouter.swift */, + ); + path = SendStableCoin; + sourceTree = ""; + }; + 84AEF0202D3DE7B1006E43E5 /* UpgradeToSmartAccount */ = { + isa = PBXGroup; + children = ( + 84AEF01E2D3DE7A0006E43E5 /* UpgradeToSmartAccountView.swift */, + 84AEF0212D3DE7C8006E43E5 /* UpgradeToSmartAccountPresenter.swift */, + 84AEF0232D3DE7D4006E43E5 /* UpgradeToSmartAccountModule.swift */, + 84AEF0252D3DE7E0006E43E5 /* UpgradeToSmartAccountRouter.swift */, + ); + path = UpgradeToSmartAccount; + sourceTree = ""; + }; 84B815592991217F00FAD54E /* PushMessages */ = { isa = PBXGroup; children = ( @@ -963,6 +1007,9 @@ isa = PBXGroup; children = ( 84F568C1279582D200D0A289 /* Signer.swift */, + 848BD1D92D34DF5E007C2AEF /* SmartAccountSigner.swift */, + 848BD1DB2D34DF72007C2AEF /* GasAbstractionSigner.swift */, + 848BD1DD2D34DF8F007C2AEF /* EOASigner.swift */, A57E71A5291CF76400325797 /* ETHSigner.swift */, A57E71A7291CF8A500325797 /* SOLSigner.swift */, ); @@ -1175,6 +1222,8 @@ 847BD1E9298A807000076C90 /* Notifications */, 84B815592991217F00FAD54E /* PushMessages */, 84D88C802CE751EE003A6C16 /* CATransactionModal */, + 84AEF0172D3DD8D3006E43E5 /* SendStableCoin */, + 84AEF0202D3DE7B1006E43E5 /* UpgradeToSmartAccount */, ); path = Wallet; sourceTree = ""; @@ -1960,11 +2009,13 @@ C56EE241293F566D004840D1 /* WalletModule.swift in Sources */, C58099352A543CD000AB58F5 /* BlinkAnimation.swift in Sources */, C56EE245293F566D004840D1 /* WalletPresenter.swift in Sources */, + 848BD1DA2D34DF5E007C2AEF /* SmartAccountSigner.swift in Sources */, C56EE240293F566D004840D1 /* ScanQRView.swift in Sources */, A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */, C56EE250293F566D004840D1 /* ScanTargetView.swift in Sources */, C56EE28F293F5757004840D1 /* MigrationConfigurator.swift in Sources */, A5A0844029D2F626000B9B17 /* DefaultCryptoProvider.swift in Sources */, + 84AEF0262D3DE7E0006E43E5 /* UpgradeToSmartAccountRouter.swift in Sources */, 847BD1DD2989494F00076C90 /* TabPage.swift in Sources */, A50D53C42ABA055700A4FD8B /* NotifyPreferencesInteractor.swift in Sources */, A50D53C22ABA055700A4FD8B /* NotifyPreferencesPresenter.swift in Sources */, @@ -1979,6 +2030,7 @@ C5B2F71029705827000DBA0E /* EthereumTransaction.swift in Sources */, A50D53C32ABA055700A4FD8B /* NotifyPreferencesRouter.swift in Sources */, A5D610C82AB31EE800C20083 /* SegmentedPicker.swift in Sources */, + 84AEF01F2D3DE7A0006E43E5 /* UpgradeToSmartAccountView.swift in Sources */, C56EE271293F56D7004840D1 /* View.swift in Sources */, A5B4F7C62ABB20AE0099AF7C /* SubscriptionView.swift in Sources */, C5B2F6FD297055B0000DBA0E /* Signer.swift in Sources */, @@ -1999,8 +2051,10 @@ A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */, C56EE270293F56D7004840D1 /* String.swift in Sources */, A51811A12A52E83100A52B15 /* SettingsRouter.swift in Sources */, + 84AEF0222D3DE7C8006E43E5 /* UpgradeToSmartAccountPresenter.swift in Sources */, C56EE279293F56D7004840D1 /* Color.swift in Sources */, 847BD1E6298A806800076C90 /* NotificationsRouter.swift in Sources */, + 848BD1DE2D34DF8F007C2AEF /* EOASigner.swift in Sources */, C579FEBA2AFCDFA6008855EB /* ConnectedSheetView.swift in Sources */, C55D3483295DD7140004314A /* AuthRequestView.swift in Sources */, C56EE243293F566D004840D1 /* ScanView.swift in Sources */, @@ -2014,6 +2068,7 @@ C55D34B02965FB750004314A /* SessionProposalRouter.swift in Sources */, C55D3495295DFA750004314A /* WelcomeRouter.swift in Sources */, C5B2F6F729705293000DBA0E /* SessionRequestRouter.swift in Sources */, + 84AEF01B2D3DD9F1006E43E5 /* SendStableCoinRouter.swift in Sources */, C56EE24F293F566D004840D1 /* WalletView.swift in Sources */, C55D34B22965FB750004314A /* SessionProposalView.swift in Sources */, C56EE248293F566D004840D1 /* ScanQR.swift in Sources */, @@ -2023,6 +2078,7 @@ C56EE289293F5757004840D1 /* Application.swift in Sources */, C56EE273293F56D7004840D1 /* UIColor.swift in Sources */, A51811982A52E21A00A52B15 /* ConfigurationService.swift in Sources */, + 848BD1DC2D34DF72007C2AEF /* GasAbstractionSigner.swift in Sources */, C5F32A322954816C00A6476E /* ConnectionDetailsPresenter.swift in Sources */, A5B4F7C32ABB20AE0099AF7C /* SubscriptionInteractor.swift in Sources */, A50D53C52ABA055700A4FD8B /* NotifyPreferencesView.swift in Sources */, @@ -2038,9 +2094,11 @@ C55D3494295DFA750004314A /* WelcomePresenter.swift in Sources */, C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, A57879712A4EDC8100F8D10B /* TextFieldView.swift in Sources */, + 84AEF01D2D3DDA0A006E43E5 /* SendStableCoinPresenter.swift in Sources */, A5D610CA2AB3249100C20083 /* ListingViewModel.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, A5D610CE2AB3594100C20083 /* ListingsAPI.swift in Sources */, + 84AEF0162D3DD8CA006E43E5 /* SendStableCoinView.swift in Sources */, 84D88C7F2CE751E7003A6C16 /* CATransactionView.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, @@ -2051,6 +2109,7 @@ A50B6A382B06697B00162B01 /* ProfilingService.swift in Sources */, A5B4F7C52ABB20AE0099AF7C /* SubscriptionRouter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, + 84AEF0242D3DE7D4006E43E5 /* UpgradeToSmartAccountModule.swift in Sources */, C5B2F6FC297055B0000DBA0E /* SOLSigner.swift in Sources */, A518119F2A52E83100A52B15 /* SettingsModule.swift in Sources */, 84D88C842CE754CC003A6C16 /* CATransactionModule.swift in Sources */, @@ -2061,6 +2120,7 @@ C5F32A2C2954814200A6476E /* ConnectionDetailsModule.swift in Sources */, C56EE249293F566D004840D1 /* ScanInteractor.swift in Sources */, C56EE28A293F5757004840D1 /* AppDelegate.swift in Sources */, + 84AEF0192D3DD9C0006E43E5 /* SendStableCoinModule.swift in Sources */, C5FFEA762ADD8956007282A2 /* BrowserModule.swift in Sources */, C56EE2A3293F6BAF004840D1 /* UIPasteboardWrapper.swift in Sources */, C5B2F6F629705293000DBA0E /* SessionRequestModule.swift in Sources */, diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8d30cd00a..9c465d4cc 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -276,8 +276,8 @@ "repositoryURL": "https://github.com/reown-com/yttrium", "state": { "branch": null, - "revision": "eb6e3f8351fc3972df37c9b72c04cd606e01bac9", - "version": "0.5.1" + "revision": "d5bea698587d8163b3fdc858539456f1573aabda", + "version": "0.6.2" } } ] diff --git a/Example/Shared/Signer/EOASigner.swift b/Example/Shared/Signer/EOASigner.swift new file mode 100644 index 000000000..286197aac --- /dev/null +++ b/Example/Shared/Signer/EOASigner.swift @@ -0,0 +1,29 @@ +import Foundation +import Commons +import WalletConnectSign +import ReownWalletKit +import Web3 + +final class EOASigner { + func sign(request: Request, importAccount: ImportAccount) async throws -> AnyCodable { + let signer = ETHSigner(importAccount: importAccount) + + switch request.method { + case "personal_sign": + return signer.personalSign(request.params) + + case "eth_signTypedData": + return signer.signTypedData(request.params) + + case "eth_sendTransaction": + return try signer.sendTransaction(request.params) + + case "solana_signTransaction": + return SOLSigner.signTransaction(request.params) + + default: + // If something is not supported, throw an error or handle it + throw Signer.Errors.notImplemented + } + } +} diff --git a/Example/Shared/Signer/GasAbstractionSigner.swift b/Example/Shared/Signer/GasAbstractionSigner.swift new file mode 100644 index 000000000..e119a57ac --- /dev/null +++ b/Example/Shared/Signer/GasAbstractionSigner.swift @@ -0,0 +1,86 @@ +import Foundation +import Commons +import WalletConnectSign +import ReownWalletKit +import Web3 + +final class GasAbstractionSigner { + func sign(request: Request, importAccount: ImportAccount, chainId: Blockchain) async throws -> AnyCodable { + print("[GasAbstractionSigner] sign() called with request method: \(request.method)") + + switch request.method { + case "personal_sign": + print("[GasAbstractionSigner] personal_sign route called — not implemented yet") + throw Signer.Errors.notImplemented + + case "eth_sendTransaction": + print("[GasAbstractionSigner] eth_sendTransaction route called") + + let calls = try request.params.get([Tx].self) + .map { + print("[GasAbstractionSigner] Tx: \($0)") + return Call(to: $0.to, value: $0.value ?? "", input: $0.data) + } + print("[GasAbstractionSigner] Prepared \(calls.count) calls") + + let eoa = try! Account(blockchain: chainId, accountAddress: importAccount.account.address) + let preparedGasAbstraction = try await WalletKit.instance.prepare7702( + EOA: eoa, + calls: calls + ) + print("[GasAbstractionSigner] preparedGasAbstraction: \(preparedGasAbstraction)") + + let signer = ETHSigner(importAccount: importAccount) + + switch preparedGasAbstraction { + case .deploymentRequired(auth: let auth, prepareDeployParams: let prepareDeployParams): + print("[GasAbstractionSigner] Deployment is required") + print("[GasAbstractionSigner] auth hash: \(auth.hash)") + + let signature = try signer.signHash(auth.hash) + + let authSig = SignedAuthorization(auth: auth.auth, signature: signature) + + let preparedSend = try await WalletKit.instance.prepareDeploy( + EOA: eoa, + authSig: authSig, + params: prepareDeployParams + ) + + let userOpSignature = try signer.signHash(preparedSend.hash) + + let userOpReceipt = try await WalletKit.instance.send( + EOA: eoa, + signature: userOpSignature, + params: preparedSend.sendParams + ) + print("[GasAbstractionSigner] userOpReceipt: \(userOpReceipt)") + + return AnyCodable(userOpReceipt) + + case .deploymentNotRequired(preparedSend: let preparedSend): + print("[GasAbstractionSigner] Deployment not required") + print("[GasAbstractionSigner] preparedSend hash: \(preparedSend.hash)") + + let signature = try signer.signHash(preparedSend.hash) + + let userOpReceipt = try await WalletKit.instance.send( + EOA: eoa, + signature: signature, + params: preparedSend.sendParams + ) + print("[GasAbstractionSigner] userOpReceipt: \(userOpReceipt)") + + return AnyCodable(userOpReceipt) + } + + case "wallet_sendCalls": + print("[GasAbstractionSigner] wallet_sendCalls route called — not implemented yet") + throw Signer.Errors.notImplemented + + default: + print("[GasAbstractionSigner] Unsupported method: \(request.method)") + throw Signer.Errors.notImplemented + } + } +} diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 910ea542d..be3c31bf7 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -1,9 +1,10 @@ + import Foundation -import Commons import WalletConnectSign import ReownWalletKit import Web3 + struct SendCallsParams: Codable { let version: String let from: String @@ -27,36 +28,51 @@ final class Signer { private init() {} - static func sign(request: Request, importAccount: ImportAccount) async throws -> AnyCodable { + /// Main entry point that decides which signer to call. + static func sign(request: Request, importAccount: ImportAccount, gasAbstracted: Bool) async throws -> AnyCodable { let requestedAddress = try await getRequestedAddress(request) - if requestedAddress == importAccount.account.address { - return try signWithEOA(request: request, importAccount: importAccount) + + // If EOA address is requested + if requestedAddress.lowercased() == importAccount.account.address.lowercased() + && !gasAbstracted { + // EOA route + let eoaSigner = EOASigner() + return try await eoaSigner.sign(request: request, importAccount: importAccount) } + + // If it's a smart account let smartAccount = try await WalletKit.instance.getSmartAccount(ownerAccount: importAccount.account) if smartAccount.address.lowercased() == requestedAddress.lowercased() { - return try await signWithSmartAccount(request: request, importAccount: importAccount) + // Smart account route + let smartAccountSigner = SmartAccountSigner() + return try await smartAccountSigner.sign(request: request, importAccount: importAccount) } + // If gas abstracted + if gasAbstracted { + let gasAbstractionSigner = GasAbstractionSigner() + return try await gasAbstractionSigner.sign(request: request, importAccount: importAccount, chainId: request.chainId) + } + + // If none of the above matched, throw an error throw Errors.accountForRequestNotFound } + // The logic for finding a requested address stays the same private static func getRequestedAddress(_ request: Request) async throws -> String { - // Attempt to decode params for transaction requests encapsulated in an array of dictionaries if let paramsArray = try? request.params.get([AnyCodable].self), let firstParam = paramsArray.first?.value as? [String: Any], let account = firstParam["from"] as? String { return account } - // Attempt to decode params for signing message requests if let paramsArray = try? request.params.get([AnyCodable].self) { if request.method == "personal_sign" || request.method == "eth_signTypedData" { - // Typically, the account address is the second parameter for personal_sign and eth_signTypedData + // Typically 2nd param for those if paramsArray.count > 1, let account = paramsArray[1].value as? String { return account } } - // Handle the `wallet_sendCalls` method if request.method == "wallet_sendCalls" { if let sendCallsParams = paramsArray.first?.value as? [String: Any], let account = sendCallsParams["from"] as? String { @@ -67,185 +83,17 @@ final class Signer { throw Errors.cantFindRequestedAddress } - - private static func signWithEOA(request: Request, importAccount: ImportAccount) throws -> AnyCodable { - let signer = ETHSigner(importAccount: importAccount) - - switch request.method { - case "personal_sign": - return signer.personalSign(request.params) - - case "eth_signTypedData": - return signer.signTypedData(request.params) - - case "eth_sendTransaction": - return try signer.sendTransaction(request.params) - - case "solana_signTransaction": - return SOLSigner.signTransaction(request.params) - - default: - throw Errors.notImplemented - } - } - - private static func signWithSmartAccount(request: Request, importAccount: ImportAccount) async throws -> AnyCodable { - - let ownerAccount = Account(blockchain: request.chainId, address: importAccount.account.address)! - - switch request.method { - case "personal_sign": - fatalError("3 step signing not yet implemented in uniffi") -// let params = try request.params.get([String].self) -// let requestedMessage = params[0] -// -// let messageToSign = Self.prepareMessageToSign(requestedMessage) -// -// let messageHash = messageToSign.sha3(.keccak256).toHexString() -// -// // Step 1 -// let prepareSignMessage: PreparedSignMessage -// do { -// prepareSignMessage = try await WalletKit.instance.prepareSignMessage(messageHash, ownerAccount: ownerAccount) -// } catch { -// print(error) -// throw error -// } -// -// // Sign prepared message -// let dataToSign = prepareSignMessage.hash.data(using: .utf8)! -// let privateKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) -// let (v, r, s) = try! privateKey.sign(message: .init(Data(dataToSign))) -// let result: String = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) -// -// // Step 2 -// let prepareSign: PreparedSign -// do { -// prepareSign = try await WalletKit.instance.doSignMessage([result], ownerAccount: ownerAccount) -// } catch { -// print(error) -// throw error -// } -// -// switch prepareSign { -// case .signature(let signature): -// return AnyCodable(signature) -// case .signStep3(let preparedSignStep3): -// // Step 3 -// let dataToSign = preparedSignStep3.hash.data(using: .utf8)! -// let privateKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) -// let (v, r, s) = try! privateKey.sign(message: .init(Data(dataToSign))) -// let result: String = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) -// -// let signature: String -// do { -// signature = try await WalletKit.instance.finalizeSignMessage([result], signStep3Params: preparedSignStep3.signStep3Params, ownerAccount: ownerAccount) -// } catch { -// print(error) -// throw error -// } -// return AnyCodable(signature) -// } - - case "eth_signTypedData": - let params = try request.params.get([String].self) - let message = params[0] - fatalError("not implemented") -// let signedMessage = try WalletKit.instance.signMessage(message) -// return AnyCodable(signedMessage) - - case "eth_sendTransaction": - struct Tx: Codable { - var to: String - var value: String - var data: String - } - let params = try request.params.get([Tx].self).map { Execution(to: $0.to, value: $0.value, data: $0.data)} - let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions(params, ownerAccount: ownerAccount) - - let signer = ETHSigner(importAccount: importAccount) - - let signature = try signer.signHash(prepareSendTransactions.hash) - - let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - - let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) - return AnyCodable(userOpHash) - - case "wallet_sendCalls": - let params = try request.params.get([SendCallsParams].self) - guard let calls = params.first?.calls else { - fatalError() - } - - let transactions = calls.map { - Execution( - to: $0.to!, - value: $0.value ?? "0", - data: $0.data ?? "" - ) - } - - let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions(transactions, ownerAccount: ownerAccount) - - let signer = ETHSigner(importAccount: importAccount) - - let signature = try signer.signHash(prepareSendTransactions.hash) - - let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - - let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) - - Task { - do { - let receipt = try await WalletKit.instance.waitForUserOperationReceipt(userOperationHash: userOpHash, ownerAccount: ownerAccount) - let message = "User Op receipt received" - AlertPresenter.present(message: message, type: .success) - } catch { - AlertPresenter.present(message: error.localizedDescription, type: .error) - } - } - - return AnyCodable(userOpHash) - - default: - throw Errors.notImplemented - } - } - - private static func prepareMessageToSign(_ messageToSign: String) -> [Byte] { - let dataToSign: [Byte] - if messageToSign.hasPrefix("0x") { - // Remove "0x" prefix and create hex data - let hexString = String(messageToSign.dropFirst(2)) - let messageData = Data(hex: hexString) - dataToSign = dataToHash(messageData) - } else { - // Plain text message, convert directly to data - let messageData = Data(messageToSign.utf8) - dataToSign = dataToHash(messageData) - } - - return dataToSign - } - - private static func dataToHash(_ data: Data) -> [Byte] { - let prefix = "\u{19}Ethereum Signed Message:\n" - let prefixData = (prefix + String(data.count)).data(using: .utf8)! - let prefixedMessageData = prefixData + data - return .init(hex: prefixedMessageData.toHexString()) - } - } extension Signer.Errors { var errorDescription: String? { switch self { - case .notImplemented: return "Requested method is not implemented" - case .accountForRequestNotFound: return "Account for request not found" - case .cantFindRequestedAddress: return "Can't find requested address" + case .notImplemented: + return "Requested method is not implemented" + case .accountForRequestNotFound: + return "Account for request not found" + case .cantFindRequestedAddress: + return "Can't find requested address" } } } - - diff --git a/Example/Shared/Signer/SmartAccountSigner.swift b/Example/Shared/Signer/SmartAccountSigner.swift new file mode 100644 index 000000000..9a65df7c1 --- /dev/null +++ b/Example/Shared/Signer/SmartAccountSigner.swift @@ -0,0 +1,141 @@ +import Foundation +import Commons +import WalletConnectSign +import ReownWalletKit +import Web3 + +final class SmartAccountSigner { + enum Errors: LocalizedError { + case notImplemented + } + + func sign(request: Request, importAccount: ImportAccount) async throws -> AnyCodable { + let requestedChainId = request.chainId + + // Set up an ownerAccount from chainId and importAccount address + let ownerAccount = Account(blockchain: requestedChainId, address: importAccount.account.address)! + + switch request.method { + case "personal_sign": + fatalError("implement 3 step signing") +// let params = try request.params.get([String].self) +// let requestedMessage = params[0] +// +// let messageToSign = Self.prepareMessageToSign(requestedMessage) +// +// let messageHash = messageToSign.sha3(.keccak256).toHexString() +// +// // Step 1 +// let prepareSignMessage: PreparedSignMessage +// do { +// prepareSignMessage = try await WalletKit.instance.prepareSignMessage(messageHash, ownerAccount: ownerAccount) +// } catch { +// print(error) +// throw error +// } +// +// // Sign prepared message +// let dataToSign = prepareSignMessage.hash.data(using: .utf8)! +// let privateKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) +// let (v, r, s) = try! privateKey.sign(message: .init(Data(dataToSign))) +// let result: String = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) +// +// // Step 2 +// let prepareSign: PreparedSign +// do { +// prepareSign = try await WalletKit.instance.doSignMessage([result], ownerAccount: ownerAccount) +// } catch { +// print(error) +// throw error +// } +// +// switch prepareSign { +// case .signature(let signature): +// return AnyCodable(signature) +// case .signStep3(let preparedSignStep3): +// // Step 3 +// let dataToSign = preparedSignStep3.hash.data(using: .utf8)! +// let privateKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) +// let (v, r, s) = try! privateKey.sign(message: .init(Data(dataToSign))) +// let result: String = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) +// +// let signature: String +// do { +// signature = try await WalletKit.instance.finalizeSignMessage([result], signStep3Params: preparedSignStep3.signStep3Params, ownerAccount: ownerAccount) +// } catch { +// print(error) +// throw error +// } +// return AnyCodable(signature) +// } + + case "eth_signTypedData": + fatalError("not implemented") + + + case "eth_sendTransaction": + let params = try request.params.get([Tx].self) + .map { Call(to: $0.to, value: $0.value!, input: $0.data) } + + let prepareSendTransactions = try await WalletKit.instance + .prepareSendTransactions(params, ownerAccount: ownerAccount) + + let signer = ETHSigner(importAccount: importAccount) + let signature = try signer.signHash(prepareSendTransactions.hash) + let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) + + let userOpHash = try await WalletKit.instance.doSendTransaction( + signatures: [ownerSignature], + doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, + ownerAccount: ownerAccount + ) + return AnyCodable(userOpHash) + + case "wallet_sendCalls": + let params = try request.params.get([SendCallsParams].self) + guard let calls = params.first?.calls else { + fatalError("No calls found") + } + + let transactions = calls.map { + Call( + to: $0.to ?? "", + value: $0.value ?? "0", + input: $0.data ?? "" + ) + } + + let prepareSendTransactions = try await WalletKit.instance + .prepareSendTransactions(transactions, ownerAccount: ownerAccount) + + let signer = ETHSigner(importAccount: importAccount) + let signature = try signer.signHash(prepareSendTransactions.hash) + let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) + + let userOpHash = try await WalletKit.instance.doSendTransaction( + signatures: [ownerSignature], + doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, + ownerAccount: ownerAccount + ) + + // Optionally handle receipt + Task { + do { + let receipt = try await WalletKit.instance.waitForUserOperationReceipt( + userOperationHash: userOpHash, + ownerAccount: ownerAccount + ) + let message = "User Op receipt received" + AlertPresenter.present(message: message, type: .success) + } catch { + AlertPresenter.present(message: error.localizedDescription, type: .error) + } + } + + return AnyCodable(userOpHash) + + default: + throw Errors.notImplemented + } + } +} diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index ca9724860..a201864bf 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -25,7 +25,10 @@ final class ConfigurationService { ) WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey) +#if DEBUG + WalletKit.instance.set7702ForLocalInfra(address: importAccount.account.address) +#endif Notify.configure( environment: BuildConfiguration.shared.apnsEnvironment, crypto: DefaultCryptoProvider() diff --git a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift index 46c5d60f4..573f9bb98 100644 --- a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift +++ b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift @@ -12,11 +12,12 @@ class WalletKitEnabler { // Use a private queue for thread-safe access to properties private let queue = DispatchQueue(label: "com.smartaccount.manager", attributes: .concurrent) - // A private backing variable for the thread-safe properties + // Private backing variables private var _isSmartAccountEnabled: Bool = false private var _isChainAbstractionEnabled: Bool = true + private var _is7702AccountEnabled: Bool = true - // Thread-safe access for setting and getting isSmartAccountEnabled + // Thread-safe access for isSmartAccountEnabled var isSmartAccountEnabled: Bool { get { return queue.sync { @@ -26,11 +27,14 @@ class WalletKitEnabler { set { queue.async(flags: .barrier) { self._isSmartAccountEnabled = newValue + if newValue { + self._is7702AccountEnabled = false + } } } } - // Thread-safe access for setting and getting isChainAbstractionEnabled + // Thread-safe access for isChainAbstractionEnabled var isChainAbstractionEnabled: Bool { get { return queue.sync { @@ -44,6 +48,23 @@ class WalletKitEnabler { } } + // Thread-safe access for is7702AccountEnabled + var is7702AccountEnabled: Bool { + get { + return queue.sync { + _is7702AccountEnabled + } + } + set { + queue.async(flags: .barrier) { + self._is7702AccountEnabled = newValue + if newValue { + self._isSmartAccountEnabled = false + } + } + } + } + // Private initializer to ensure it cannot be instantiated externally private init() {} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 1016ccc0b..6156ca075 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -8,6 +8,7 @@ struct Tx: Codable { let data: String let from: String let to: String + let value: String? } final class MainPresenter { @@ -97,9 +98,7 @@ extension MainPresenter { do { let tx = try request.params.get([Tx].self)[0] - let transaction = InitialTransaction( - chainId: request.chainId.absoluteString, - from: tx.from, + let call = Call( to: tx.to, value: "0", input: tx.data @@ -107,8 +106,7 @@ extension MainPresenter { ActivityIndicatorManager.shared.start() - let routeResponseSuccess = try await WalletKit.instance.prepare(transaction: transaction) - + let routeResponseSuccess = try await WalletKit.instance.prepare(chainId: request.chainId.absoluteString, from: tx.from, call: call) await MainActor.run { switch routeResponseSuccess { case .success(let routeResponseSuccess): diff --git a/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinModule.swift b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinModule.swift new file mode 100644 index 000000000..13fa7555e --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinModule.swift @@ -0,0 +1,20 @@ +import Foundation +import UIKit +import ReownWalletKit + +final class SendStableCoinModule { + @discardableResult + static func create( + app: Application, + importAccount: ImportAccount + ) -> UIViewController { + let router = SendStableCoinRouter(app: app) + let presenter = SendStableCoinPresenter(router: router, importAccount: importAccount) + let view = SendStableCoinView(presenter: presenter).environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + router.viewController = viewController + return viewController + } +} + + diff --git a/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinPresenter.swift new file mode 100644 index 000000000..04084e69c --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinPresenter.swift @@ -0,0 +1,166 @@ +import Foundation +import Combine +@preconcurrency import ReownWalletKit + +enum L2: String { + case Arbitrium + case Optimism + case Base + case Sepolia + + var chainId: Blockchain { + switch self { + case .Arbitrium: + return Blockchain("eip155:42161")! + case .Optimism: + return Blockchain("eip155:10")! + case .Base: + return Blockchain("eip155:8453")! + case .Sepolia: + return Blockchain("eip155:11155111")! + } + } +} + +/// Your Presenter for handling logic: building calls, checking deployment, and routing +final class SendStableCoinPresenter: ObservableObject, SceneViewModel { + @Published var selectedNetwork: L2 = .Sepolia + @Published var recipient: String = "0x2bb169662b61f3D8f8318F800F686389C8a72961" + @Published var amount: String = "1" + @Published var transactionCompleted: Bool = false + @Published var transactionResult: String? = nil + + let router: SendStableCoinRouter + let importAccount: ImportAccount + + init(router: SendStableCoinRouter, + importAccount: ImportAccount) { + self.router = router + self.importAccount = importAccount + } + + func set(network: L2) { + selectedNetwork = network + } + + /// Reusable method that checks if wallet deployment is required + func checkDeploymentRequired(for calls: [Call]) async throws -> PreparedGasAbstraction { + let eoa = try Account( + blockchain: selectedNetwork.chainId, + accountAddress: importAccount.account.address + ) + return try await WalletKit.instance.prepare7702(EOA: eoa, calls: calls) + } + + /// Called when user taps "Upgrade to Smart Account" + /// In this example, it uses the same calls as typed in the text fields. + func upgradeToSmartAccount() { + Task { + do { + let calls = try getCalls() + let preparedGasAbstraction = try await checkDeploymentRequired(for: calls) + + switch preparedGasAbstraction { + case .deploymentRequired(auth: let auth, prepareDeployParams: let params): + // If deployment is required, present the upgrade flow + router.presentUpgradeToSmartAccount( + importAccount: importAccount, + network: selectedNetwork, + prepareDeployParams: params, + auth: auth, + chainId: selectedNetwork.chainId + ) + case .deploymentNotRequired: + // If not required, hide the button & show alert + AlertPresenter.present( + message: "Upgrade not required", + type: .error + ) + } + } catch { + // If something goes wrong with checkDeploymentRequired + AlertPresenter.present( + message: error.localizedDescription, + type: .error + ) + } + } + } + + /// Called when user taps "Send" + /// Uses the presenter's `recipient` and `amount` directly + func send() async throws { + do { + + // Build calls from the presenter's fields + let calls = try getCalls() + + // Check if wallet deployment is required + let preparedGasAbstraction = try await checkDeploymentRequired(for: calls) + let eoa = try Account( + blockchain: selectedNetwork.chainId, + accountAddress: importAccount.account.address + ) + let signer = ETHSigner(importAccount: importAccount) + + switch preparedGasAbstraction { + case .deploymentRequired(auth: let auth, prepareDeployParams: let deployParams): + print("Deployment is required -> Show 'upgrade to smart account' screen") + await MainActor.run { + router.presentUpgradeToSmartAccount( + importAccount: importAccount, + network: selectedNetwork, + prepareDeployParams: deployParams, + auth: auth, + chainId: selectedNetwork.chainId + ) + } + + case .deploymentNotRequired(preparedSend: let preparedSend): + print("Deployment not required -> sign & send userOp") + + let signature = try signer.signHash(preparedSend.hash) + let userOpReceipt = try await WalletKit.instance.send( + EOA: eoa, + signature: signature, + params: preparedSend.sendParams + ) + print("[GasAbstractionSigner] userOpReceipt: \(userOpReceipt)") + await MainActor.run { + transactionCompleted = true + } + } + } catch { + AlertPresenter.present(message: error.localizedDescription, type: .error) + } + } + + /// Build the [Call] array from the presenter's current `recipient` and `amount` + private func getCalls() throws -> [Call] { +// let eoa = try Account( +// blockchain: selectedNetwork.chainId, +// accountAddress: importAccount.account.address +// ) +// +// let toAccount = try Account( +// blockchain: selectedNetwork.chainId, +// accountAddress: recipient +// ) +// +// let call = WalletKit.instance.prepareUSDCTransferCall( +// EOA: eoa, +// to: toAccount, +// amount: amount +// ) + + let call = Call( + to: "0x23d8eE973EDec76ae91669706a587b9A4aE1361A", + value: "0", + input: "" + ) + + return [call] + } +} + + diff --git a/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinRouter.swift new file mode 100644 index 000000000..efd48a9de --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinRouter.swift @@ -0,0 +1,38 @@ +import Foundation +import UIKit +import ReownWalletKit + +final class SendStableCoinRouter { + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } + + func dismiss() { + DispatchQueue.main.async { [weak self] in + self?.viewController?.dismiss() + } + } + + func presentUpgradeToSmartAccount( + importAccount: ImportAccount, + network: L2, + prepareDeployParams: PrepareDeployParams, + auth: PreparedGasAbstractionAuthorization, + chainId: Blockchain + ) { + + UpgradeToSmartAccountModule.create( + app: app, + importAccount: importAccount, + network: network, + prepareDeployParams: prepareDeployParams, + auth: auth, + chainId: chainId + ) + .present(from: viewController) + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinView.swift b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinView.swift new file mode 100644 index 000000000..c993dcc79 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/SendStableCoin/SendStableCoinView.swift @@ -0,0 +1,249 @@ +import SwiftUI +import AsyncButton + +struct SendStableCoinView: View { + @ObservedObject var presenter: SendStableCoinPresenter + + @State private var showNetworkPicker = false + + // Example placeholders; adjust as needed + let myAddressShort = "0x742...f44e" + let balanceAmount = 1000.03 + let feesApprox = "~$0.17" + + var body: some View { + VStack(spacing: 24) { + + // Top row: My Address + Upgrade Button + VStack(spacing: 8) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("My Address") + .foregroundColor(.gray) + Text(myAddressShort) + .font(.system(.body, design: .monospaced)) + } + Spacer() + Button(action: { + presenter.upgradeToSmartAccount() + }) { + Text("Upgrade to Smart Account") + .foregroundColor(.blue) + } + } + } + .padding() + .background(Color("grey-section")) + .cornerRadius(12) + + // Balance section + VStack(spacing: 8) { + HStack { + Text("Balance") + .foregroundColor(.gray) + Spacer() + Text("$1,000") + .font(.system(.body, design: .monospaced)) + } + // USDC row + HStack { + Image("usdc") // Replace with your USDC asset image name + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + Text("USD Coin") + .foregroundColor(.gray) + Spacer() + Text("\(String(format: "%.2f", balanceAmount)) USDC") + .font(.system(.body, design: .monospaced)) + } + } + .padding() + .background(Color("grey-section")) + .cornerRadius(12) + + // Transaction card + VStack(spacing: 20) { + Text("Transaction 1") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + // Recipient + VStack(alignment: .leading, spacing: 8) { + Text("Recipient") + .foregroundColor(.gray) + TextField("0x1234... or ENS", text: $presenter.recipient) + .textFieldStyle(.roundedBorder) + .disableAutocorrection(true) + .autocapitalization(.none) + } + + // Amount + network + VStack(alignment: .leading, spacing: 8) { + Text("Amount to send") + .foregroundColor(.gray) + + HStack { + TextField("0.00", text: $presenter.amount) + .keyboardType(.decimalPad) + .textFieldStyle(.roundedBorder) + + Button(action: { + showNetworkPicker = true + }) { + Text(presenter.selectedNetwork.rawValue) + .foregroundColor(.blue) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background(Color(.systemGray6)) + .cornerRadius(8) + } + .confirmationDialog("Select Network", + isPresented: $showNetworkPicker, + titleVisibility: .visible) { + Button(L2.Arbitrium.rawValue) { + presenter.set(network: .Arbitrium) + } + Button(L2.Base.rawValue) { + presenter.set(network: .Base) + } + Button(L2.Optimism.rawValue) { + presenter.set(network: .Optimism) + } + Button(L2.Sepolia.rawValue) { + presenter.set(network: .Sepolia) + } + Button("Cancel", role: .cancel) {} + } + } + } + + // Add Transaction button + Button(action: { + // TODO: handle adding additional transactions + }) { + Label("Add Transaction", systemImage: "plus") + .foregroundColor(.blue) + } + } + .padding() + .background(Color("grey-section")) + .cornerRadius(12) + + // Fees row + HStack { + Text("Fees") + .foregroundColor(.gray) + Spacer() + Text(feesApprox) + .font(.system(.body, design: .monospaced)) + } + .padding() + .background(Color("grey-section")) + .cornerRadius(12) + + Spacer() + + VStack(spacing: 12) { + AsyncButton( + options: [ + .showProgressViewOnLoading, + .disableButtonOnLoading, + .showAlertOnError, + .enableNotificationFeedback + ] + ) { + try await presenter.send() + } label: { + Text("Send") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + LinearGradient( + gradient: Gradient(colors: [.blue, .purple]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .cornerRadius(12) + } + } + .padding() + } + .padding() + // Present success screen + .sheet(isPresented: $presenter.transactionCompleted) { + SendStableCoinCompletedView(presenter: presenter) + } + } +} + + +struct SendStableCoinCompletedView: View { + @ObservedObject var presenter: SendStableCoinPresenter + + var body: some View { + ZStack { + VStack(spacing: 24) { + Spacer() + + // "Tada" or confetti image at top + Image("tada") + .resizable() + .scaledToFit() + .frame(width: 120, height: 120) + + // Title + Text("Transaction Completed") + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.white) + + // Subtitle or descriptive text + Text("You’ve successfully sent your stablecoin transaction.") + .font(.body) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + + // Show the transaction hash if present + if let txHash = presenter.transactionResult, !txHash.isEmpty { + VStack(spacing: 4) { + Text("Transaction Hash") + .font(.subheadline) + .foregroundColor(.white.opacity(0.8)) + + Text(txHash) + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal, 32) + } + } + + Spacer() + + // Done button to dismiss + Button(action: { + // Setting transactionCompleted = false hides this sheet + presenter.transactionCompleted = false + }) { + Text("Done") + .fontWeight(.bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .cornerRadius(12) + } + .padding(.horizontal, 24) + + Spacer() + } + .padding() + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift index 2d186abd6..8606daadb 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestInteractor.swift @@ -5,8 +5,10 @@ import ReownRouter final class SessionRequestInteractor { func respondSessionRequest(sessionRequest: Request, importAccount: ImportAccount) async throws -> Bool { + let gasAbstracted = WalletKitEnabler.shared.is7702AccountEnabled do { - let result = try await Signer.sign(request: sessionRequest, importAccount: importAccount) + let result = try await Signer.sign(request: sessionRequest, importAccount: importAccount, gasAbstracted: gasAbstracted) + AlertPresenter.present(message: result.description, type: .success) try await WalletKit.instance.respond( topic: sessionRequest.topic, requestId: sessionRequest.id, diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index aa04620b3..3d46bf3f5 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -20,7 +20,7 @@ final class SettingsPresenter: ObservableObject { self.importAccount = importAccount fetchSmartAccountSafe() } - + func fetchSmartAccountSafe() { Task { do { @@ -41,6 +41,12 @@ final class SettingsPresenter: ObservableObject { WalletKitEnabler.shared.isSmartAccountEnabled = enable } + /// Enables or disables the 7702 Account. + /// - Parameter enable: A Boolean value indicating whether to enable the 7702 Account. + func enable7702Account(_ enable: Bool) { + WalletKitEnabler.shared.is7702AccountEnabled = enable + } + func enableChainAbstraction(_ enable: Bool) { WalletKitEnabler.shared.isChainAbstractionEnabled = enable } @@ -82,7 +88,7 @@ final class SettingsPresenter: ObservableObject { [.init( to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", value: "0", - data: "0x68656c6c6f" + input: "0x68656c6c6f" )], ownerAccount: ownerAccount) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 4b3f07dad..3a5f26aac 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -2,13 +2,12 @@ import SwiftUI import AsyncButton import ReownAppKitUI - struct SettingsView: View { @EnvironmentObject var viewModel: SettingsPresenter @State private var copyAlert: Bool = false @State private var isSmartAccountEnabled: Bool = WalletKitEnabler.shared.isSmartAccountEnabled @State private var isChainAbstractionEnabled: Bool = WalletKitEnabler.shared.isChainAbstractionEnabled - + @State private var is7702AccountEnabled: Bool = WalletKitEnabler.shared.is7702AccountEnabled var body: some View { ScrollView { @@ -31,6 +30,33 @@ struct SettingsView: View { Toggle("", isOn: $isSmartAccountEnabled) .onChange(of: isSmartAccountEnabled) { newValue in viewModel.enableSmartAccount(newValue) + if newValue { + // Ensure mutual exclusivity + is7702AccountEnabled = false + WalletKitEnabler.shared.is7702AccountEnabled = false + } + } + .labelsHidden() + } + .padding(.horizontal, 12) + .padding(.vertical, 16) + .background(Color.Foreground100.opacity(0.05).cornerRadius(12)) + + HStack { + Text("7702 Account") + .foregroundColor(.Foreground100) + .font(.paragraph700) + + Spacer() + + Toggle("", isOn: $is7702AccountEnabled) + .onChange(of: is7702AccountEnabled) { newValue in + viewModel.enable7702Account(newValue) + if newValue { + // Ensure mutual exclusivity + isSmartAccountEnabled = false + WalletKitEnabler.shared.isSmartAccountEnabled = false + } } .labelsHidden() } @@ -114,6 +140,7 @@ struct SettingsView: View { .onAppear { viewModel.objectWillChange.send() isSmartAccountEnabled = WalletKitEnabler.shared.isSmartAccountEnabled + is7702AccountEnabled = WalletKitEnabler.shared.is7702AccountEnabled } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountModule.swift b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountModule.swift new file mode 100644 index 000000000..b0fa9b0ec --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountModule.swift @@ -0,0 +1,34 @@ +import Foundation +import UIKit +import ReownWalletKit + +final class UpgradeToSmartAccountModule { + @discardableResult + static func create( + app: Application, + importAccount: ImportAccount, + network: L2, + prepareDeployParams: PrepareDeployParams, + auth: PreparedGasAbstractionAuthorization, + chainId: Blockchain + ) -> UIViewController { + let router = UpgradeToSmartAccountRouter(app: app) + let presenter = UpgradeToSmartAccountPresenter( + router: router, + importAccount: importAccount, + network: network, + prepareDeployParams: prepareDeployParams, + auth: auth, + chainId: chainId + ) + + // Build the SwiftUI view, injecting the presenter + let view = UpgradeToSmartAccountView(presenter: presenter) + + // Wrap it in your SceneViewController or whichever container + let viewController = SceneViewController(viewModel: presenter, content: view) + router.viewController = viewController + + return viewController + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountPresenter.swift new file mode 100644 index 000000000..d5cf8fa35 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountPresenter.swift @@ -0,0 +1,64 @@ +import Foundation +import ReownWalletKit + +final class UpgradeToSmartAccountPresenter: ObservableObject, SceneViewModel { + @Published var selectedNetwork: L2 + @Published var doNotAskAgain = false + + let router: UpgradeToSmartAccountRouter + let importAccount: ImportAccount + let prepareDeployParams: PrepareDeployParams + let auth: PreparedGasAbstractionAuthorization + let chainId: Blockchain + + init(router: UpgradeToSmartAccountRouter, + importAccount: ImportAccount, + network: L2, + prepareDeployParams: PrepareDeployParams, + auth: PreparedGasAbstractionAuthorization, + chainId: Blockchain) { + self.router = router + self.importAccount = importAccount + self.selectedNetwork = network + self.prepareDeployParams = prepareDeployParams + self.auth = auth + self.chainId = chainId + } + + func signAndUpgrade() async throws { + do { + let signer = ETHSigner(importAccount: importAccount) + + let eoa = try! Account(blockchain: chainId, accountAddress: importAccount.account.address) + + let signature = try signer.signHash(auth.hash) + + let authSig = SignedAuthorization(auth: auth.auth, signature: signature) + + let preparedSend = try await WalletKit.instance.prepareDeploy( + EOA: eoa, + authSig: authSig, + params: prepareDeployParams + ) + + let userOpSignature = try signer.signHash(preparedSend.hash) + + let userOpReceipt = try await WalletKit.instance.send( + EOA: eoa, + signature: userOpSignature, + params: preparedSend.sendParams + ) + ActivityIndicatorManager.shared.stop() + AlertPresenter.present(message: "Succesfully upgraded EOA to smart account", type: .success) + router.dismiss() + } catch { + AlertPresenter.present(message: error.localizedDescription, type: .error) + print(error) + ActivityIndicatorManager.shared.stop() + } + } + + func cancel() { + router.dismiss() + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountRouter.swift new file mode 100644 index 000000000..c6828be23 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountRouter.swift @@ -0,0 +1,18 @@ +import Foundation +import UIKit + +final class UpgradeToSmartAccountRouter { + weak var viewController: UIViewController? + private let app: Application + + init(app: Application) { + self.app = app + } + + /// Dismiss this screen + func dismiss() { + DispatchQueue.main.async { [weak self] in + self?.viewController?.dismiss(animated: true) + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountView.swift b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountView.swift new file mode 100644 index 000000000..a27dcf929 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/UpgradeToSmartAccount/UpgradeToSmartAccountView.swift @@ -0,0 +1,137 @@ +import SwiftUI +import AsyncButton + +struct UpgradeToSmartAccountView: View { + @ObservedObject var presenter: UpgradeToSmartAccountPresenter + + var body: some View { + VStack(spacing: 0) { + // Top Header + HStack { + Text("Upgrade to Smart Account") + .font(.headline) + .foregroundColor(.white) + Spacer() + Button(action: { + presenter.cancel() + }) { + Image(systemName: "xmark") + .foregroundColor(.white) + } + } + .padding() + + // Explanation text + Text("To upgrade your account, you need to sign a transaction with your wallet.") + .font(.subheadline) + .foregroundColor(.gray) + .multilineTextAlignment(.center) + .padding(.horizontal, 16) + .padding(.bottom, 16) + + // Features card + VStack(alignment: .leading, spacing: 12) { + Text("Get access to advanced features") + .font(.body).bold() + + Label("Sponsored Transactions", systemImage: "checkmark.circle.fill") + .foregroundColor(.green) + Label("Bundle Transactions", systemImage: "checkmark.circle.fill") + .foregroundColor(.green) + + Text("and more in the future...") + .foregroundColor(.gray) + } + .padding() + .background(Color("grey-section")) + .cornerRadius(16) + .padding(.horizontal, 16) + .padding(.bottom, 16) + + // Wallet/Network/Fees card + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Wallet") + .foregroundColor(.gray) + Spacer() + Text(presenter.importAccount.account.address) + .font(.system(.body, design: .monospaced)) + } + Divider() + HStack { + Text("Network") + .foregroundColor(.gray) + Spacer() + Text(presenter.selectedNetwork.rawValue) + .foregroundColor(.blue) + } + Divider() + HStack { + Text("Fees") + .foregroundColor(.gray) + Spacer() + Text("FREE") + .foregroundColor(.green) + } + Divider() + HStack { + Text("Sponsored By") + .foregroundColor(.gray) + Spacer() + Text("reown") // Example sponsor + } + } + .padding() + .background(Color("grey-section")) + .cornerRadius(16) + .padding(.horizontal, 16) + .padding(.bottom, 16) + + Spacer() + + // Bottom actions row + HStack { + Button(action: { + presenter.cancel() + }) { + Text("Cancel") + .foregroundColor(.white) + } + .padding(.horizontal, 24) + .padding(.vertical, 12) + .background(Color(.systemGray3).opacity(0.3)) + .cornerRadius(12) + + Spacer() + + AsyncButton( + options: [ + .showProgressViewOnLoading, + .disableButtonOnLoading, + .showAlertOnError, + .enableNotificationFeedback + ] + ) { + try await presenter.signAndUpgrade() + } label: { + Text("Sign & Upgrade") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + LinearGradient( + gradient: Gradient(colors: [.blue, .purple]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .cornerRadius(12) + } + } + .padding(.horizontal, 16) + .padding(.bottom, 16) + } + .background(Color.black.edgesIgnoringSafeArea(.all)) // Example dark background + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 573e8d722..597cf2de2 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -73,6 +73,34 @@ final class WalletPresenter: ObservableObject { } } + func sendStableCoin() { + router.presentSendStableCoin(importAccount: importAccount) + } + + func test7702() async { + let testAccount = ImportAccount.new() + ActivityIndicatorManager.shared.start() +#if DEBUG + WalletKit.instance.set7702ForLocalInfra(address: testAccount.account.address) +#endif + print("testing 7702") + let chainId = Blockchain("eip155:11155111")! + let GASigner = GasAbstractionSigner() + + let tx = Tx(data: "", from: testAccount.account.address, to: "0x23d8eE973EDec76ae91669706a587b9A4aE1361A", value: "") + let request = try! Request(topic: "", method: "eth_sendTransaction", params: AnyCodable([tx]), chainId: chainId) + + do { + let userOpReceipt = try await GASigner.sign(request: request, importAccount: testAccount, chainId: chainId) + ActivityIndicatorManager.shared.stop() + AlertPresenter.present(message: userOpReceipt.description, type: .success) + } catch { + print(error) + AlertPresenter.present(message: error.localizedDescription, type: .error) + ActivityIndicatorManager.shared.stop() + } + } + func onScanUri() { router.presentScan { [weak self] uriString in do { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 8109c488f..c4f5b54bf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -37,6 +37,12 @@ final class WalletRouter { .present(from: viewController) } + func presentSendStableCoin(importAccount: ImportAccount) { + SendStableCoinModule.create(app: app, importAccount: importAccount) + .wrapToNavigationController() + .present(from: viewController) + } + func dismiss() { viewController.navigationController?.dismiss() } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift index 96e151f0c..a4eb3c265 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -73,6 +73,31 @@ struct WalletView: View { HStack(spacing: 20) { Spacer() + // Text button with "test 7702" + Button { + Task { try await presenter.test7702() } + } label: { + Text("test 7702") + .foregroundColor(.white) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color.blue) + .cornerRadius(8) + } + .shadow(color: .black.opacity(0.25), radius: 8, y: 4) + Spacer() + + Button { + presenter.sendStableCoin() + } label: { + Image(systemName: "paperplane.fill") + .resizable() + .frame(width: 40, height: 40) + } + .shadow(color: .black.opacity(0.25), radius: 8, y: 4) + .accessibilityIdentifier("sendStableCoin") + Button { presenter.onPasteUri() } label: { diff --git a/Package.swift b/Package.swift index 3aa2eea23..a256f9c1a 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ func buildYttriumWrapperTarget() -> Target { path: "Sources/YttriumWrapper" ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.5.1"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.6.2"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], diff --git a/Sources/ReownWalletKit/SmartAccount/GasAbstractionManager.swift b/Sources/ReownWalletKit/SmartAccount/GasAbstractionManager.swift new file mode 100644 index 000000000..095e87e3d --- /dev/null +++ b/Sources/ReownWalletKit/SmartAccount/GasAbstractionManager.swift @@ -0,0 +1,71 @@ +import Foundation +import YttriumWrapper + +/// A manager that creates or retrieves a GasAbstractionClient +/// for a given eoa Account. +class GasAbstractionClientsManager { + private var EOAToClient: [Account: Client] = [:] + private let apiKey: String + + init(pimlicoApiKey: String) { + self.apiKey = pimlicoApiKey + } + + /// Returns an existing GasAbstractionClient for the given eoa Account, + /// or creates a new one if none exists. + func getOrCreateGasAbstractionClient(for EOA: Account) -> Client { + if let existingClient = EOAToClient[EOA] { + return existingClient + } else { + let newClient = createGasAbstractionClient(EOAAccount: EOA) + EOAToClient[EOA] = newClient + return newClient + } + } + +#if DEBUG + public func set7702ForLocalInfra(address: String) { + let chainId = "eip155:11155111" + + let eoa = Account(blockchain: Blockchain(chainId)!, address: address)! + let localRpcUrl = URL(string: "http://localhost:8545")! + let localBundlerUrl = URL(string: "http://localhost:4337")! + let localPaymasterUrl = URL(string: "http://localhost:3000")! + + let gasAbstractionClient = getOrCreateGasAbstractionClient(for: eoa) + + + let newClient = gasAbstractionClient + .withRpcOverrides(rpcOverrides: [chainId: localRpcUrl.absoluteString]) + .with4337Urls( + bundlerUrl: localBundlerUrl.absoluteString, + paymasterUrl: localPaymasterUrl.absoluteString + ) + + EOAToClient[eoa] = newClient + + } +#endif + + private func createGasAbstractionClient(EOAAccount: Account) -> Client { +// let chainId = EOAAccount.reference + let projectId = Networking.projectId +// let pimlicoBundlerUrl = "https://api.pimlico.io/v2/\(chainId)/rpc?apikey=\(apiKey)" +// let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(EOAAccount.blockchainIdentifier)&projectId=\(projectId)" + + // Adjust config as needed, similarly to how you do for SafesManager +// let config = Config( +// endpoints: .init( +// rpc: .init(baseUrl: rpcUrl, apiKey: ""), +// bundler: .init(baseUrl: pimlicoBundlerUrl, apiKey: ""), +// paymaster: .init(baseUrl: pimlicoBundlerUrl, apiKey: "") +// ) +// ) + + + // Replace this initializer with however you construct a GasAbstractionClient + let client = Client(projectId: projectId) + + return client + } +} diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 0a55a7d10..f4f8d962d 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -104,7 +104,8 @@ public class WalletKitClient { private let pairingClient: PairingClientProtocol private let pushClient: PushClientProtocol private let smartAccountsManager: SafesManager? - private let chainAbstractionClient: ChainAbstractionClient? + private let chainAbstractionClient: ChainAbstractionClient + private let gasAbstractionClientsManager: GasAbstractionClientsManager? private var account: Account? @@ -113,13 +114,15 @@ public class WalletKitClient { pairingClient: PairingClientProtocol, pushClient: PushClientProtocol, smartAccountsManager: SafesManager?, - chainAbstractionClient: ChainAbstractionClient? + chainAbstractionClient: ChainAbstractionClient, + gasAbstractionClientsManager: GasAbstractionClientsManager? ) { self.signClient = signClient self.pairingClient = pairingClient self.pushClient = pushClient self.smartAccountsManager = smartAccountsManager self.chainAbstractionClient = chainAbstractionClient + self.gasAbstractionClientsManager = gasAbstractionClientsManager } /// For a wallet to approve a session proposal. @@ -273,10 +276,46 @@ public class WalletKitClient { return pairingClient.getPairings() } + // MARK: 7702 - // MARK: Yttrium + public func prepare7702(EOA: Account, calls: [Call]) async throws -> PreparedGasAbstraction { + + let gasAbstractionClient = gasAbstractionClientsManager!.getOrCreateGasAbstractionClient(for: EOA) + + return try await gasAbstractionClient.prepare(chainId: EOA.blockchainIdentifier, from: EOA.address, calls: calls) + + } + + public func prepareUSDCTransferCall(EOA: Account, to: Account, amount: String) -> Call { + let gasAbstractionClient = gasAbstractionClientsManager!.getOrCreateGasAbstractionClient(for: EOA) + + return gasAbstractionClient.prepareUsdcTransferCall(chainId: to.blockchainIdentifier, to: to.address, usdcAmount: amount) + } + +#if DEBUG + public func set7702ForLocalInfra(address: String) { + + gasAbstractionClientsManager!.set7702ForLocalInfra(address: address) + } +#endif + + public func prepareDeploy(EOA: Account, authSig: SignedAuthorization, params: PrepareDeployParams) async throws -> PreparedSend { + + let gasAbstractionClient = gasAbstractionClientsManager!.getOrCreateGasAbstractionClient(for: EOA) + + return try await gasAbstractionClient.prepareDeploy(authSig: authSig, params: params, sponsor: nil) + } + + public func send(EOA: Account, signature: PrimitiveSignature, params: SendParams) async throws -> UserOperationReceipt { + + let gasAbstractionClient = gasAbstractionClientsManager!.getOrCreateGasAbstractionClient(for: EOA) + + return try await gasAbstractionClient.send(signature: signature, params: params) + } + + // MARK: Yttrium 4337 @available(*, message: "This method is experimental. Use with caution.") - public func prepareSendTransactions(_ transactions: [Execution], ownerAccount: Account) async throws -> PreparedSendTransaction { + public func prepareSendTransactions(_ transactions: [Call], ownerAccount: Account) async throws -> PreparedSendTransaction { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled } @@ -342,55 +381,32 @@ public class WalletKitClient { @available(*, message: "This method is experimental. Use with caution.") public func status(orchestrationId: String) async throws -> StatusResponse { - guard let chainAbstractionClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } - - return try await chainAbstractionClient.status(orchestrationId: orchestrationId) } @available(*, message: "This method is experimental. Use with caution.") - public func prepare(transaction: InitialTransaction) async throws -> PrepareResponse { - guard let chainAbstractionClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } - - return try await chainAbstractionClient.prepare(initialTransaction: transaction) + public func prepare(chainId: String, from: String, call: Call) async throws -> PrepareResponse { + return try await chainAbstractionClient.prepare(chainId: chainId, from: from, call: call) } @available(*, message: "This method is experimental. Use with caution.") public func estimateFees(chainId: String) async throws -> Eip1559Estimation { - guard let chainAbstractionClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } - return try await chainAbstractionClient.estimateFees(chainId: chainId) } @available(*, message: "This method is experimental. Use with caution.") public func getUiFields(routeResponse: PrepareResponseAvailable, currency: Currency) async throws -> UiFields { - guard let chainAbstractionClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } return try await chainAbstractionClient.getUiFields(routeResponse: routeResponse, currency: currency) } @available(*, message: "This method is experimental. Use with caution.") public func erc20Balance(chainId: String, token: String, owner: String) async throws -> Ffiu256 { - guard let chainAbstractionClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } return try await chainAbstractionClient.erc20TokenBalance(chainId: chainId, token: token, owner: owner) } @available(*, message: "This method is experimental. Use with caution.") public func waitForSuccessWithTimeout(orchestrationId: String, checkIn: UInt64, timeout: UInt64 = 180) async throws -> StatusResponseCompleted { - guard let chainClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } - - return try await chainClient.waitForSuccessWithTimeout(orchestrationId: orchestrationId, checkIn: checkIn, timeout: timeout) + return try await chainAbstractionClient.waitForSuccessWithTimeout(orchestrationId: orchestrationId, checkIn: checkIn, timeout: timeout) } } diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index 625804107..cdf39f32c 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -9,8 +9,10 @@ struct WalletKitClientFactory { config: WalletKit.Config ) -> WalletKitClient { var safesManager: SafesManager? = nil + var gasAbstractionClientsManager: GasAbstractionClientsManager? = nil if let pimlicoApiKey = config.pimlicoApiKey { safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey) + gasAbstractionClientsManager = GasAbstractionClientsManager(pimlicoApiKey: pimlicoApiKey) } let chainAbstractionClient = ChainAbstractionClient(projectId: Networking.projectId) return WalletKitClient( @@ -18,7 +20,8 @@ struct WalletKitClientFactory { pairingClient: pairingClient, pushClient: pushClient, smartAccountsManager: safesManager, - chainAbstractionClient: chainAbstractionClient + chainAbstractionClient: chainAbstractionClient, + gasAbstractionClientsManager: gasAbstractionClientsManager ) } @@ -31,8 +34,10 @@ struct WalletKitClientFactory { projectId: String ) -> WalletKitClient { var safesManager: SafesManager? = nil + var gasAbstractionClientsManager: GasAbstractionClientsManager? = nil if let pimlicoApiKey = config.pimlicoApiKey { safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey) + gasAbstractionClientsManager = GasAbstractionClientsManager(pimlicoApiKey: pimlicoApiKey) } let chainAbstractionClient = ChainAbstractionClient(projectId: projectId) return WalletKitClient( @@ -40,7 +45,8 @@ struct WalletKitClientFactory { pairingClient: pairingClient, pushClient: pushClient, smartAccountsManager: safesManager, - chainAbstractionClient: chainAbstractionClient + chainAbstractionClient: chainAbstractionClient, + gasAbstractionClientsManager: gasAbstractionClientsManager ) } #endif diff --git a/Sources/WalletConnectUtils/TVFCollector.swift b/Sources/WalletConnectUtils/TVFCollector.swift new file mode 100644 index 000000000..379ad67d9 --- /dev/null +++ b/Sources/WalletConnectUtils/TVFCollector.swift @@ -0,0 +1,149 @@ +import Foundation + +// MARK: - Supporting Models + +struct EthSendTransaction: Codable { + let from: String? + let to: String? + let data: String? + let value: String? +} + +struct SolanaSignTransactionResult: Codable { + let signature: String? +} + +struct SolanaSignAndSendTransactionResult: Codable { + let signature: String? +} + +struct SolanaSignAllTransactionsResult: Codable { + let transactions: [String]? +} + +// MARK: - CollectionResult + +/// A structure for returning the collection result from TVFCollector. +struct CollectionResult { + let methods: [String] + let contractAddresses: [String]? + let chainId: String +} + +// MARK: - TVFCollector + +struct TVFCollector { + // MARK: Constants + + private static let ETH_SEND_TRANSACTION = "eth_sendTransaction" + private static let ETH_SEND_RAW_TRANSACTION = "eth_sendRawTransaction" + private static let WALLET_SEND_CALLS = "wallet_sendCalls" + private static let SOLANA_SIGN_TRANSACTION = "solana_signTransaction" + private static let SOLANA_SIGN_AND_SEND_TRANSACTION = "solana_signAndSendTransaction" + private static let SOLANA_SIGN_ALL_TRANSACTION = "solana_signAllTransactions" + + // MARK: Computed Properties + + private var evm: [String] { + [Self.ETH_SEND_TRANSACTION, + Self.ETH_SEND_RAW_TRANSACTION] + } + + private var solana: [String] { + [Self.SOLANA_SIGN_TRANSACTION, + Self.SOLANA_SIGN_AND_SEND_TRANSACTION, + Self.SOLANA_SIGN_ALL_TRANSACTION] + } + + private var wallet: [String] { + [Self.WALLET_SEND_CALLS] + } + + private var all: [String] { + evm + solana + wallet + } + + // MARK: - Core Methods + + /// Attempts to collect relevant data from the provided parameters. + /// + /// - Parameters: + /// - rpcMethod: The RPC method string. + /// - rpcParams: A JSON string containing parameters. + /// - chainId: The chain ID. + /// - Returns: A `CollectionResult` if the method is recognized, otherwise `nil`. + func collect(rpcMethod: String, + rpcParams: String, + chainId: String) -> CollectionResult? { + + // If the method is not recognized, return nil + guard all.contains(rpcMethod) else { + return nil + } + + // Attempt to decode addresses (EVM use-case) + var contractAddresses: [String]? = nil + + switch rpcMethod { + case Self.ETH_SEND_TRANSACTION: + // Try to decode JSON array of EthSendTransaction + guard let data = rpcParams.data(using: .utf8) else { break } + do { + let transactions = try JSONDecoder().decode([EthSendTransaction].self, from: data) + // Use the first "to" address if present + if let firstTo = transactions.first?.to { + contractAddresses = [firstTo] + } + } catch { + // If decoding fails, we'll leave contractAddresses as nil + } + default: + break + } + + return CollectionResult( + methods: [rpcMethod], + contractAddresses: contractAddresses, + chainId: chainId + ) + } + + /// Collects transaction hashes from an RPC result if possible. + /// + /// - Parameters: + /// - rpcMethod: The RPC method string. + /// - rpcResult: A JSON string containing the result. + /// - Returns: An array of hashes or signatures, or nil if none found. + func collectTxHashes(rpcMethod: String, + rpcResult: String) -> [String]? { + + do { + switch rpcMethod { + // EVM or wallet methods simply return the raw result as the transaction hash + case _ where evm.contains(rpcMethod) || wallet.contains(rpcMethod): + return [rpcResult] + + case Self.SOLANA_SIGN_TRANSACTION: + guard let data = rpcResult.data(using: .utf8) else { return nil } + let decoded = try JSONDecoder().decode(SolanaSignTransactionResult.self, from: data) + return decoded.signature.map { [$0] } + + case Self.SOLANA_SIGN_AND_SEND_TRANSACTION: + guard let data = rpcResult.data(using: .utf8) else { return nil } + let decoded = try JSONDecoder().decode(SolanaSignAndSendTransactionResult.self, from: data) + return decoded.signature.map { [$0] } + + case Self.SOLANA_SIGN_ALL_TRANSACTION: + guard let data = rpcResult.data(using: .utf8) else { return nil } + let decoded = try JSONDecoder().decode(SolanaSignAllTransactionsResult.self, from: data) + return decoded.transactions + + default: + return nil + } + } catch { + print("Error processing \(rpcMethod): \(error)") + return nil + } + } +} diff --git a/Tests/WalletConnectUtilsTests/TVFCollectorTests.swift b/Tests/WalletConnectUtilsTests/TVFCollectorTests.swift new file mode 100644 index 000000000..734f80cd7 --- /dev/null +++ b/Tests/WalletConnectUtilsTests/TVFCollectorTests.swift @@ -0,0 +1,133 @@ +import XCTest + +@testable import WalletConnectUtils + +final class TVFCollectorTests: XCTestCase { + + private let tvf = TVFCollector() + + // MARK: - collect tests + + func testCollectShouldReturnNilWhenRpcMethodIsNotInTheAllowedList() { + // Arrange + let rpcMethod = "unsupported_method" + let rpcParams = "{}" + let chainId = "1" + + // Act + let result = tvf.collect(rpcMethod: rpcMethod, rpcParams: rpcParams, chainId: chainId) + + // Assert + XCTAssertNil(result) + } + + func testCollectShouldParseEthSendTransactionCorrectly() { + // Arrange + let rpcMethod = "eth_sendTransaction" + let rpcParams = "[{\"to\": \"0x1234567890abcdef\", \"from\": \"0x1234567890abcdef\"}]" + let chainId = "1" + + // Act + let result = tvf.collect(rpcMethod: rpcMethod, rpcParams: rpcParams, chainId: chainId) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result?.methods, ["eth_sendTransaction"]) + XCTAssertEqual(result?.contractAddresses, ["0x1234567890abcdef"]) + XCTAssertEqual(result?.chainId, "1") + } + + func testCollectShouldReturnDefaultValueWhenParsingEthSendTransactionFails() { + // Arrange + let rpcMethod = "eth_sendTransaction" + let rpcParams = "{malformed_json}" + let chainId = "1" + + // Act + let result = tvf.collect(rpcMethod: rpcMethod, rpcParams: rpcParams, chainId: chainId) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result?.methods, ["eth_sendTransaction"]) + XCTAssertNil(result?.contractAddresses) + XCTAssertEqual(result?.chainId, "1") + } + + // MARK: - collectTxHashes tests + + func testCollectTxHashesShouldReturnRpcResultForEvmAndWalletMethods() { + // Arrange + let rpcMethod = "eth_sendTransaction" + let rpcResult = "0x123abc" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result, ["0x123abc"]) + } + + func testCollectTxHashesShouldParseSolanaSignTransactionAndReturnSignature() { + // Arrange + let rpcMethod = "solana_signTransaction" + let rpcResult = "{\"signature\": \"0xsignature123\"}" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result, ["0xsignature123"]) + } + + func testCollectTxHashesShouldReturnNilWhenParsingSolanaSignTransactionFails() { + // Arrange + let rpcMethod = "solana_signTransaction" + let rpcResult = "{malformed_json}" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNil(result) + } + + func testCollectTxHashesShouldParseSolanaSignAndSendTransactionAndReturnSignature() { + // Arrange + let rpcMethod = "solana_signAndSendTransaction" + let rpcResult = "{\"signature\": \"0xsendAndSignSignature\"}" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result, ["0xsendAndSignSignature"]) + } + + func testCollectTxHashesShouldParseSolanaSignAllTransactionsAndReturnAllTransactions() { + // Arrange + let rpcMethod = "solana_signAllTransactions" + let rpcResult = "{\"transactions\": [\"tx1\", \"tx2\"]}" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNotNil(result) + XCTAssertEqual(result, ["tx1", "tx2"]) + } + + func testCollectTxHashesShouldReturnNilForUnsupportedMethods() { + // Arrange + let rpcMethod = "unsupported_method" + let rpcResult = "some_result" + + // Act + let result = tvf.collectTxHashes(rpcMethod: rpcMethod, rpcResult: rpcResult) + + // Assert + XCTAssertNil(result) + } +}