From fcba6d7db2b661269cd54dc6630ec98e81c87955 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 15 Oct 2024 08:13:33 +0200 Subject: [PATCH 01/73] savepoint --- .../xcschemes/ReownWalletKit.xcscheme | 67 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 18 ----- Package.resolved | 22 +----- Package.swift | 45 +++++++------ .../ReownWalletKit/Web3WalletImports.swift | 1 + 5 files changed, 95 insertions(+), 58 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ReownWalletKit.xcscheme diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ReownWalletKit.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ReownWalletKit.xcscheme new file mode 100644 index 000000000..9cb403919 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ReownWalletKit.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 664728376..421868a5e 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -109,15 +109,6 @@ "version": "1.0.0" } }, - { - "package": "swift-dotenv", - "repositoryURL": "https://github.com/thebarndog/swift-dotenv.git", - "state": { - "branch": null, - "revision": "f6e7ca817d35eeccb20b62c87ee75963b01b29dc", - "version": "2.0.1" - } - }, { "package": "swift-qrcode-generator", "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", @@ -189,15 +180,6 @@ "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", "version": "1.0.2" } - }, - { - "package": "yttrium", - "repositoryURL": "https://github.com/reown-com/yttrium", - "state": { - "branch": null, - "revision": "04d1589c42c510d0f2487e58b0af9619a6f070b4", - "version": "0.1.0" - } } ] }, diff --git a/Package.resolved b/Package.resolved index 64598b065..76976ae40 100644 --- a/Package.resolved +++ b/Package.resolved @@ -28,15 +28,6 @@ "version": "1.0.0" } }, - { - "package": "swift-dotenv", - "repositoryURL": "https://github.com/thebarndog/swift-dotenv.git", - "state": { - "branch": null, - "revision": "f6e7ca817d35eeccb20b62c87ee75963b01b29dc", - "version": "2.0.1" - } - }, { "package": "swift-qrcode-generator", "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", @@ -46,15 +37,6 @@ "version": "1.0.3" } }, - { - "package": "swift-snapshot-testing", - "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", - "state": { - "branch": null, - "revision": "f29e2014f6230cf7d5138fc899da51c7f513d467", - "version": "1.10.0" - } - }, { "package": "SwiftImageReadWrite", "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", @@ -66,10 +48,10 @@ }, { "package": "CoinbaseWalletSDK", - "repositoryURL": "https://github.com/WalletConnect/wallet-mobile-sdk", + "repositoryURL": "https://github.com/MobileWalletProtocol/wallet-mobile-sdk", "state": { "branch": null, - "revision": "b6dfb7d6b8447c7c5b238a10443a1ac28223f38f", + "revision": "84b3d3f25a2e3b140ec12bb0d22c35b58f817d44", "version": "1.0.0" } } diff --git a/Package.swift b/Package.swift index f22ade41e..4a775415c 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true // Define dependencies array @@ -13,25 +13,30 @@ var dependencies: [Package.Dependency] = [ .package(name: "CoinbaseWalletSDK", url: "https://github.com/MobileWalletProtocol/wallet-mobile-sdk", .upToNextMinor(from: "1.0.0")), // .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", .upToNextMinor(from: "1.10.0")), ] -var yttriumTarget: Target! -// Conditionally add Yttrium dependency -if yttriumDebug { - var yttriumSwiftSettings: [SwiftSetting] = [] - dependencies.append(.package(path: "../yttrium/crates/ffi/YttriumCore")) - yttriumSwiftSettings.append(.define("YTTRIUM_DEBUG")) - yttriumTarget = .target( - name: "YttriumWrapper", - dependencies: [.product(name: "YttriumCore", package: "YttriumCore")], - path: "Sources/YttriumWrapper", - swiftSettings: yttriumSwiftSettings - ) -} else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .upToNextMinor(from: "0.1.0"))) - yttriumTarget = .target( - name: "YttriumWrapper", - dependencies: [.product(name: "Yttrium", package: "yttrium")], - path: "Sources/YttriumWrapper" - ) + + +let yttriumTarget = buildYttriumWrapperTarget() + +func buildYttriumWrapperTarget() -> Target { + // Conditionally add Yttrium dependency + if yttriumDebug { + var yttriumSwiftSettings: [SwiftSetting] = [] + dependencies.append(.package(path: "../yttrium/crates/ffi/YttriumCore")) + yttriumSwiftSettings.append(.define("YTTRIUM_DEBUG")) + return .target( + name: "YttriumWrapper", + dependencies: [.product(name: "YttriumCore", package: "YttriumCore")], + path: "Sources/YttriumWrapper", + swiftSettings: yttriumSwiftSettings + ) + } else { + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .upToNextMinor(from: "0.1.0"))) + return .target( + name: "YttriumWrapper", + dependencies: [.product(name: "Yttrium", package: "yttrium")], + path: "Sources/YttriumWrapper" + ) + } } let package = Package( diff --git a/Sources/ReownWalletKit/Web3WalletImports.swift b/Sources/ReownWalletKit/Web3WalletImports.swift index d5390a7f7..1669f6219 100644 --- a/Sources/ReownWalletKit/Web3WalletImports.swift +++ b/Sources/ReownWalletKit/Web3WalletImports.swift @@ -2,4 +2,5 @@ @_exported import WalletConnectSign @_exported import WalletConnectPush @_exported import WalletConnectVerify +//@_exported import YttriumWrapper #endif From ae6028d1770d3748d4336cb777988a902b8e3566 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 15 Oct 2024 15:28:53 +0200 Subject: [PATCH 02/73] add safes manager --- Example/ExampleApp.xcodeproj/project.pbxproj | 4 - .../BusinessLayer/SmartAccount.swift | 94 ------------------- .../Wallet/Main/MainModule.swift | 31 ------ .../SmartAccount/SafesManager.swift | 46 +++++++++ Sources/ReownWalletKit/WalletKit.swift | 9 +- Sources/ReownWalletKit/WalletKitClient.swift | 14 ++- .../WalletKitClientFactory.swift | 15 ++- Sources/ReownWalletKit/Web3WalletConfig.swift | 2 + .../ReownWalletKit/Web3WalletImports.swift | 2 +- 9 files changed, 78 insertions(+), 139 deletions(-) delete mode 100644 Example/WalletApp/BusinessLayer/SmartAccount.swift create mode 100644 Sources/ReownWalletKit/SmartAccount/SafesManager.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 1b665b3b9..c284f847e 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -49,7 +49,6 @@ 84733CD32C1C2A4B001B2850 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AEC2502B4D42C100E27A5B /* AlertPresenter.swift */; }; 84733CD42C1C2C24001B2850 /* ProfilingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50B6A372B06697B00162B01 /* ProfilingService.swift */; }; 84733CD52C1C2CEB001B2850 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C56EE25D293F56D6004840D1 /* InputConfig.swift */; }; - 84733CDA2C258BDB001B2850 /* SmartAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84733CD92C258BDB001B2850 /* SmartAccount.swift */; }; 847BD1D62989492500076C90 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1D12989492500076C90 /* MainViewController.swift */; }; 847BD1D82989492500076C90 /* MainModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1D32989492500076C90 /* MainModule.swift */; }; 847BD1D92989492500076C90 /* MainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1D42989492500076C90 /* MainPresenter.swift */; }; @@ -351,7 +350,6 @@ 846E35A32C0065B600E63DF4 /* ConfigPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigPresenter.swift; sourceTree = ""; }; 846E35A52C0065C100E63DF4 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = ""; }; 846E35A72C006C5600E63DF4 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 84733CD92C258BDB001B2850 /* SmartAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartAccount.swift; sourceTree = ""; }; 847BD1D12989492500076C90 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 847BD1D32989492500076C90 /* MainModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainModule.swift; sourceTree = ""; }; 847BD1D42989492500076C90 /* MainPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainPresenter.swift; sourceTree = ""; }; @@ -1014,7 +1012,6 @@ isa = PBXGroup; children = ( A5D610CC2AB3592F00C20083 /* ListingsSertice */, - 84733CD92C258BDB001B2850 /* SmartAccount.swift */, 8454EF092C9421B600B5529E /* SmartAccountManager.swift */, ); path = BusinessLayer; @@ -1998,7 +1995,6 @@ C5B2F6F729705293000DBA0E /* SessionRequestRouter.swift in Sources */, C56EE24F293F566D004840D1 /* WalletView.swift in Sources */, C55D34B22965FB750004314A /* SessionProposalView.swift in Sources */, - 84733CDA2C258BDB001B2850 /* SmartAccount.swift in Sources */, C56EE248293F566D004840D1 /* ScanQR.swift in Sources */, 847BD1EB298A87AB00076C90 /* SubscriptionsViewModel.swift in Sources */, C55D349B2965BC2F0004314A /* TagsView.swift in Sources */, diff --git a/Example/WalletApp/BusinessLayer/SmartAccount.swift b/Example/WalletApp/BusinessLayer/SmartAccount.swift deleted file mode 100644 index 65811a722..000000000 --- a/Example/WalletApp/BusinessLayer/SmartAccount.swift +++ /dev/null @@ -1,94 +0,0 @@ -import Foundation -import YttriumWrapper -import WalletConnectUtils - -extension YttriumWrapper.AccountClient { - - func getAccount() async throws -> Account { - let chain = try Blockchain(namespace: "eip155", reference: chainId) - let address = try await getAddress() - return try Account(blockchain: chain, accountAddress: address) - } -} - -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 pimlicoBundlerUrl = "https://\(InputConfig.pimlicoBundlerUrl!)" - let rpcUrl = "https://\(InputConfig.rpcUrl!)" - let pimlicoSepolia = YttriumWrapper.Config( - endpoints: .init( - rpc: .init(baseURL: rpcUrl), - bundler: .init(baseURL: pimlicoBundlerUrl), - paymaster: .init(baseURL: pimlicoBundlerUrl) - ) - ) - - 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: pickedConfig, - 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 82d3a0eb5..250d2bf20 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainModule.swift @@ -21,36 +21,5 @@ final class MainModule { static func configureSmartAccountOnSign(importAccount: ImportAccount) { let privateKey = importAccount.privateKey let ownerAddress = String(importAccount.account.address.dropFirst(2)) - 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" -// let prefixData = (prefix + String(data.count)).data(using: .utf8)! -// let prefixedMessageData = prefixData + data -// return .init(hex: prefixedMessageData.toHexString()) -// } -// -// let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) -// -// // Determine if the message is hex-encoded or plain text -// let dataToSign: Bytes -// if messageToSign.hasPrefix("0x") { -// // Hex-encoded message, remove "0x" and convert -// let messageData = Data(hex: String(messageToSign.dropFirst(2))) -// dataToSign = dataToHash(messageData) -// } else { -// // Plain text message, convert directly to data -// let messageData = Data(messageToSign.utf8) -// dataToSign = dataToHash(messageData) -// } -// -// // Sign the data -// let (v, r, s) = try! prvKey.sign(message: .init(Data(dataToSign))) -// let result = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) -// return .success(result) -// }) } } diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift new file mode 100644 index 000000000..031a6f75b --- /dev/null +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -0,0 +1,46 @@ + +import Foundation + +class SafesManager { + var ownerToClient: [Account: AccountClient] = [:] + let entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" // v0.7 on Sepolia + let rpcUrl: String + let bundlerUrl: String + + init(bundlerUrl: String, rpcUrl: String) { + self.bundlerUrl = bundlerUrl + self.rpcUrl = rpcUrl + } + + func getOrCreateSafe(for owner: Account) -> AccountClient { + if let client = ownerToClient[owner] { + return client + } else { + // to do check if chain is supported + let safe = createSafe(ownerAccount: owner) + ownerToClient[owner] = safe + return safe + } + } + + private func createSafe(ownerAccount: Account) -> AccountClient { + + let pimlicoBundlerUrl = "https://\(bundlerUrl)" + let rpcUrl = "https://\(rpcUrl)" + let pimlicoSepolia = YttriumWrapper.Config( + endpoints: .init( + rpc: .init(baseURL: rpcUrl), + bundler: .init(baseURL: pimlicoBundlerUrl), + paymaster: .init(baseURL: pimlicoBundlerUrl) + ) + ) + // use YttriumWrapper.Config.local() for local foundry node + return AccountClient( + ownerAddress: ownerAccount.address, + entryPoint: entryPoint, + chainId: Int(ownerAccount.blockchain.reference)!, + config: pimlicoSepolia, + safe: true + ) + } +} diff --git a/Sources/ReownWalletKit/WalletKit.swift b/Sources/ReownWalletKit/WalletKit.swift index 2ebe969f9..e53c234e7 100644 --- a/Sources/ReownWalletKit/WalletKit.swift +++ b/Sources/ReownWalletKit/WalletKit.swift @@ -26,7 +26,8 @@ public class WalletKit { return WalletKitClientFactory.create( signClient: Sign.instance, pairingClient: Pair.instance as! PairingClient, - pushClient: Push.instance + pushClient: Push.instance, + config: config ) }() @@ -42,11 +43,13 @@ public class WalletKit { metadata: AppMetadata, crypto: CryptoProvider, pushHost: String = "echo.walletconnect.com", - environment: APNSEnvironment = .production + environment: APNSEnvironment = .production, + bundlerUrl: String? = nil, + rpcUrl: String? = nil ) { Pair.configure(metadata: metadata) Push.configure(pushHost: pushHost, environment: environment) Sign.configure(crypto: crypto) - WalletKit.config = WalletKit.Config(crypto: crypto) + WalletKit.config = WalletKit.Config(crypto: crypto, bundlerUrl: bundlerUrl, rpcUrl: rpcUrl) } } diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 39232cb8d..3126d8a6a 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -98,17 +98,20 @@ public class WalletKitClient { private let signClient: SignClientProtocol private let pairingClient: PairingClientProtocol private let pushClient: PushClientProtocol - + private let smartAccountsManager: SafesManager? + private var account: Account? init( signClient: SignClientProtocol, pairingClient: PairingClientProtocol, - pushClient: PushClientProtocol + pushClient: PushClientProtocol, + smartAccountsManager: SafesManager? ) { self.signClient = signClient self.pairingClient = pairingClient self.pushClient = pushClient + self.smartAccountsManager = smartAccountsManager } /// For a wallet to approve a session proposal. @@ -261,8 +264,15 @@ public class WalletKitClient { public func getPairings() -> [Pairing] { return pairingClient.getPairings() } + + + // MARK: Yttrium +// public func prepareSendTransactions(_ transactions: [Transaction]) async throws -> PreparedSendTransaction { +// +// } } + #if DEBUG extension WalletKitClient { public func register(deviceToken: String, enableEncrypted: Bool = false) async throws { diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index 1cd7e5c6b..5066abb3c 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -1,15 +1,22 @@ import Foundation -public struct WalletKitClientFactory { - public static func create( +struct WalletKitClientFactory { + static func create( signClient: SignClientProtocol, pairingClient: PairingClientProtocol, - pushClient: PushClientProtocol + pushClient: PushClientProtocol, + config: WalletKit.Config ) -> WalletKitClient { + var safesManager: SafesManager? = nil + if let bundlerUrl = config.bundlerUrl, + let rpcUrl = config.rpcUrl { + safesManager = SafesManager(bundlerUrl: bundlerUrl, rpcUrl: rpcUrl) + } return WalletKitClient( signClient: signClient, pairingClient: pairingClient, - pushClient: pushClient + pushClient: pushClient, + smartAccountsManager: safesManager ) } } diff --git a/Sources/ReownWalletKit/Web3WalletConfig.swift b/Sources/ReownWalletKit/Web3WalletConfig.swift index 686d99231..98c25c64c 100644 --- a/Sources/ReownWalletKit/Web3WalletConfig.swift +++ b/Sources/ReownWalletKit/Web3WalletConfig.swift @@ -3,5 +3,7 @@ import Foundation extension WalletKit { struct Config { let crypto: CryptoProvider + let bundlerUrl: String? + let rpcUrl: String? } } diff --git a/Sources/ReownWalletKit/Web3WalletImports.swift b/Sources/ReownWalletKit/Web3WalletImports.swift index 1669f6219..b7fd5d4b0 100644 --- a/Sources/ReownWalletKit/Web3WalletImports.swift +++ b/Sources/ReownWalletKit/Web3WalletImports.swift @@ -2,5 +2,5 @@ @_exported import WalletConnectSign @_exported import WalletConnectPush @_exported import WalletConnectVerify -//@_exported import YttriumWrapper +@_exported import YttriumWrapper #endif From a842034406281db00cd2bc8925fd0c2979ec5cc7 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 16 Oct 2024 15:45:16 +0200 Subject: [PATCH 03/73] Yttrium integration into WalletKit --- Example/ExampleApp.xcodeproj/project.pbxproj | 8 +- Example/Shared/InputConfig.swift | 4 +- Example/Shared/Signer/Signer.swift | 78 ++++++++----------- .../SmartAccountEnabler copy.swift | 43 ++++++++++ ...anager.swift => SmartAccountEnabler.swift} | 11 +-- .../SessionProposalInteractor.swift | 5 +- .../Wallet/Settings/SettingsPresenter.swift | 23 +++--- .../Wallet/Settings/SettingsView.swift | 2 +- Package.swift | 2 +- .../SmartAccount/SafesManager.swift | 13 ++-- Sources/ReownWalletKit/WalletKit.swift | 4 +- Sources/ReownWalletKit/WalletKitClient.swift | 39 +++++++++- .../WalletKitClientFactory.swift | 4 +- Sources/ReownWalletKit/Web3WalletConfig.swift | 2 +- 14 files changed, 153 insertions(+), 85 deletions(-) create mode 100644 Example/WalletApp/BusinessLayer/SmartAccountEnabler copy.swift rename Example/WalletApp/BusinessLayer/{SmartAccountManager.swift => SmartAccountEnabler.swift} (77%) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index c284f847e..af28d2ad5 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 84474A0129B9EB74005F520B /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 84474A0029B9EB74005F520B /* Starscream */; }; 84474A0229B9ECA2005F520B /* DefaultSocketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */; }; 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 8448F1D327E4726F0000B866 /* WalletConnect */; }; - 8454EF0A2C9421B600B5529E /* SmartAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454EF092C9421B600B5529E /* SmartAccountManager.swift */; }; 845B8D8C2934B36C0084A966 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B8D8B2934B36C0084A966 /* Account.swift */; }; 846E359F2C00654F00E63DF4 /* ConfigModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846E359E2C00654F00E63DF4 /* ConfigModule.swift */; }; 846E35A22C0065AD00E63DF4 /* ConfigRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846E35A12C0065AD00E63DF4 /* ConfigRouter.swift */; }; @@ -65,6 +64,7 @@ 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8487A9472A83AD680003D5AF /* LoggingService.swift */; }; 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7A2A9BA206007EBAC2 /* Mixpanel */; }; 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7C2A9BA328007EBAC2 /* Mixpanel */; }; + 849BA7532CBFCB5100B953F4 /* SmartAccountEnabler copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849BA7522CBFCB5100B953F4 /* SmartAccountEnabler copy.swift */; }; 849D7A93292E2169006A2BD4 /* NotifyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* NotifyTests.swift */; }; 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A6E3C22A386BBC008A0571 /* Publisher.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; @@ -341,7 +341,6 @@ 844511C92C8BA69D00A6A86C /* AppKitLab.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppKitLab.entitlements; sourceTree = ""; }; 844749F329B9E5B9005F520B /* RelayIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RelayIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 844749F529B9E5B9005F520B /* RelayClientEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayClientEndToEndTests.swift; sourceTree = ""; }; - 8454EF092C9421B600B5529E /* SmartAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartAccountManager.swift; sourceTree = ""; }; 845AA7D929BA1EBA00F33739 /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = IntegrationTests.xctestplan; path = ExampleApp.xcodeproj/IntegrationTests.xctestplan; sourceTree = ""; }; 845AA7DC29BB424800F33739 /* SmokeTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SmokeTests.xctestplan; sourceTree = ""; }; 845B8D8B2934B36C0084A966 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; @@ -366,6 +365,7 @@ 8487A9472A83AD680003D5AF /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.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 = ""; }; + 849BA7522CBFCB5100B953F4 /* SmartAccountEnabler copy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SmartAccountEnabler copy.swift"; sourceTree = ""; }; 849D7A92292E2169006A2BD4 /* NotifyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyTests.swift; sourceTree = ""; }; 84A6E3C22A386BBC008A0571 /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; @@ -1011,8 +1011,8 @@ A5D610CB2AB358ED00C20083 /* BusinessLayer */ = { isa = PBXGroup; children = ( + 849BA7522CBFCB5100B953F4 /* SmartAccountEnabler copy.swift */, A5D610CC2AB3592F00C20083 /* ListingsSertice */, - 8454EF092C9421B600B5529E /* SmartAccountManager.swift */, ); path = BusinessLayer; sourceTree = ""; @@ -2017,6 +2017,7 @@ C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, A57879712A4EDC8100F8D10B /* TextFieldView.swift in Sources */, A5D610CA2AB3249100C20083 /* ListingViewModel.swift in Sources */, + 849BA7532CBFCB5100B953F4 /* SmartAccountEnabler copy.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, A5D610CE2AB3594100C20083 /* ListingsAPI.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, @@ -2026,7 +2027,6 @@ 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, A50D53C12ABA055700A4FD8B /* NotifyPreferencesModule.swift in Sources */, A50B6A382B06697B00162B01 /* ProfilingService.swift in Sources */, - 8454EF0A2C9421B600B5529E /* SmartAccountManager.swift in Sources */, A5B4F7C52ABB20AE0099AF7C /* SubscriptionRouter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, C5B2F6FC297055B0000DBA0E /* SOLSigner.swift in Sources */, diff --git a/Example/Shared/InputConfig.swift b/Example/Shared/InputConfig.swift index 1deb5cf71..9099ad59c 100644 --- a/Example/Shared/InputConfig.swift +++ b/Example/Shared/InputConfig.swift @@ -17,8 +17,8 @@ struct InputConfig { return config(for: "MIXPANEL_TOKEN") } - static var pimlicoBundlerUrl: String? { - return config(for: "PIMLICO_BUNDLER_URL") + static var pimlicoApiKey: String? { + return config(for: "PIMLICO_API_KEY") } static var rpcUrl: String? { diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 453048a14..178896516 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -1,7 +1,7 @@ import Foundation import Commons import WalletConnectSign -import YttriumWrapper +import ReownWalletKit struct SendCallsParams: Codable { let version: String @@ -16,43 +16,28 @@ struct SendCallsParams: Codable { } } -enum SmartAccountType { - case simple - case safe -} final class Signer { enum Errors: Error { case notImplemented - case unknownSmartAccountType + case accountForRequestNotFound } private init() {} static func sign(request: Request, importAccount: ImportAccount) async throws -> AnyCodable { - if let accountType = try await getRequestedSmartAccountType(request) { - return try await signWithSmartAccount(request: request, accountType: accountType) - } else { + let requestedAddress = try await getRequestedAddress(request) + if requestedAddress == importAccount.account.address { return try signWithEOA(request: request, importAccount: importAccount) } - } - - private static func getRequestedSmartAccountType(_ request: Request) async throws -> SmartAccountType? { - let account = try await getRequestedAccount(request) - if account == nil { - return nil - } - - let safeSmartAccountAddress = try await SmartAccountSafe.instance.getClient().getAddress() - - if account?.lowercased() == safeSmartAccountAddress.lowercased() { - return .safe + let smartAccount = try await WalletKit.instance.getSmartAccount(ownerAccount: importAccount.account) + if smartAccount.address == requestedAddress { + return try await signWithSmartAccount(request: request, importAccount: importAccount) } - - return nil + throw Errors.accountForRequestNotFound } - private static func getRequestedAccount(_ request: Request) async throws -> String? { + 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], @@ -78,7 +63,7 @@ final class Signer { } } - return nil + throw cantGetRequestedAddress } private static func signWithEOA(request: Request, importAccount: ImportAccount) throws -> AnyCodable { @@ -102,32 +87,35 @@ final class Signer { } } - private static func signWithSmartAccount(request: Request, accountType: SmartAccountType) async throws -> AnyCodable { - let client: AccountClientProtocol - switch accountType { - case .safe: - client = await SmartAccountSafe.instance.getClient() - default: - fatalError("Only safe is currently supported") - } + 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": let params = try request.params.get([String].self) let message = params[0] - let signedMessage = try client.signMessage(message) + let signedMessage = try WalletKit.instance.signMessage(message) return AnyCodable(signedMessage) case "eth_signTypedData": let params = try request.params.get([String].self) let message = params[0] - let signedMessage = try client.signMessage(message) + let signedMessage = try WalletKit.instance.signMessage(message) return AnyCodable(signedMessage) case "eth_sendTransaction": let params = try request.params.get([YttriumWrapper.Transaction].self) - let result = try await client.sendTransactions(params) - return AnyCodable(result) + 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], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) + return AnyCodable(userOpHash) case "wallet_sendCalls": let params = try request.params.get([SendCallsParams].self) @@ -143,13 +131,15 @@ final class Signer { ) } - let userOpHash = try await client.sendTransactions(transactions) + let prepareSendTransactions = try await accountClient.prepareSendTransactions(transactions) - Task { - let userOpReceipt = try await SmartAccountSafe.instance.getClient().waitForUserOperationReceipt(userOperationHash: userOpHash) - guard let userOpReceiptSting = userOpReceipt.jsonString else { return } - AlertPresenter.present(message: userOpReceiptSting, type: .info) - } + let signer = ETHSigner(importAccount: importAccount) + + let signature = try signer.signHash(prepareSendTransactions.hash) + + let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) + + let userOpHash = try await accountClient.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams) return AnyCodable(userOpHash) @@ -163,7 +153,7 @@ extension Signer.Errors: LocalizedError { var errorDescription: String? { switch self { case .notImplemented: return "Requested method is not implemented" - case .unknownSmartAccountType: return "Unknown smart account type" + case .accountForRequestNotFound: return "Account for request not found" } } } diff --git a/Example/WalletApp/BusinessLayer/SmartAccountEnabler copy.swift b/Example/WalletApp/BusinessLayer/SmartAccountEnabler copy.swift new file mode 100644 index 000000000..8bc1495f6 --- /dev/null +++ b/Example/WalletApp/BusinessLayer/SmartAccountEnabler copy.swift @@ -0,0 +1,43 @@ +import Foundation +import ReownWalletKit + +class SmartAccountEnabler { + enum Errors: Error { + case smartAccountNotEnabled + } + + // Singleton instance + static let shared = SmartAccountEnabler() + + // Use a private queue for thread-safe access to the isSmartAccountEnabled property + private let queue = DispatchQueue(label: "com.smartaccount.manager", attributes: .concurrent) + + // A private backing variable for the thread-safe property + private var _isSmartAccountEnabled: Bool = false + + // Thread-safe access for setting and getting isSmartAccountEnabled + var isSmartAccountEnabled: Bool { + get { + return queue.sync { + _isSmartAccountEnabled + } + } + set { + queue.async(flags: .barrier) { + self._isSmartAccountEnabled = newValue + } + } + } + + // Private initializer to ensure it cannot be instantiated externally + private init() {} + + // Function to get smart account addresses + func getSmartAccountsAddresses(ownerAccount: Account) async throws -> [String] { + guard isSmartAccountEnabled else { + throw Errors.smartAccountNotEnabled + } + let safeAccount = try await WalletKit.instance.getSmartAccount(ownerAccount: ownerAccount) + return [safeAccount.address] + } +} diff --git a/Example/WalletApp/BusinessLayer/SmartAccountManager.swift b/Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift similarity index 77% rename from Example/WalletApp/BusinessLayer/SmartAccountManager.swift rename to Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift index 60fec7c37..cc2a6353f 100644 --- a/Example/WalletApp/BusinessLayer/SmartAccountManager.swift +++ b/Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift @@ -1,12 +1,13 @@ import Foundation +import ReownWalletKit -class SmartAccountManager { +class SmartAccountEnabler { enum Errors: Error { case smartAccountNotEnabled } // Singleton instance - static let shared = SmartAccountManager() + static let shared = SmartAccountEnabler() // Use a private queue for thread-safe access to the isSmartAccountEnabled property private let queue = DispatchQueue(label: "com.smartaccount.manager", attributes: .concurrent) @@ -32,11 +33,11 @@ class SmartAccountManager { private init() {} // Function to get smart account addresses - func getSmartAccountsAddresses() async throws -> [String] { + func getSmartAccountsAddresses(ownerAccount: Account) async throws -> [String] { guard isSmartAccountEnabled else { throw Errors.smartAccountNotEnabled } - let safeAccountAddress = try await SmartAccountSafe.instance.getClient().getAddress() - return [safeAccountAddress] + let safeAccount = try await WalletKit.instance.getSmartAccount(ownerAccount: ) + return [safeAccount.address] } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift index 68a010e41..ac1581dcf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift @@ -16,9 +16,10 @@ final class SessionProposalInteractor { var supportedAccounts: [Account] var sessionProperties = [String: String]() - if SmartAccountManager.shared.isSmartAccountEnabled { + if SmartAccountEnabler.shared.isSmartAccountEnabled { let sepolia = Blockchain("eip155:11155111")! - let smartAccountAddresses = try await SmartAccountManager.shared.getSmartAccountsAddresses() + let sepoliaOwnerAccount = Account(blockchain: sepolia, address: EOAAccount.address)! + let smartAccountAddresses = try await SmartAccountEnabler.shared.getSmartAccountsAddresses(ownerAccount: sepoliaOwnerAccount) supportedAccounts = smartAccountAddresses.map { Account(blockchain: sepolia, address: $0)! } sessionProperties = getSessionProperties(addresses: smartAccountAddresses) } else { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 2abc4440b..34afdb42d 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -40,11 +40,11 @@ final class SettingsPresenter: ObservableObject { } func enableSmartAccount(_ enable: Bool) { - SmartAccountManager.shared.isSmartAccountEnabled = enable + SmartAccountEnabler.shared.isSmartAccountEnabled = enable } private func getSmartAccountSafe() async throws -> String { - try await SmartAccountSafe.instance.getClient().getAccount().absoluteString + try await WalletKit.instance.getSmartAccount(ownerAccount: importAccount.account).absoluteString } var account: String { @@ -72,23 +72,24 @@ final class SettingsPresenter: ObservableObject { } func sendTransaction() async throws -> String { - let client = await SmartAccountSafe.instance.getClient() - let prepareSendTransactions = try await client.prepareSendTransactions([.init( - to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - value: "0", - data: "0x68656c6c6f" - )]) + let ownerAccount = importAccount.account - let owner = client.ownerAddress + let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions( + [.init( + to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + value: "0", + data: "0x68656c6c6f" + )], + ownerAccount: ownerAccount) let signer = ETHSigner(importAccount: importAccount) let signature = try signer.signHash(prepareSendTransactions.hash) - let ownerSignature = OwnerSignature(owner: owner, signature: signature) + let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - return try await client.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams) + return try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) } func logoutPressed() async throws { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 938702d6f..8ddbaa68e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -95,7 +95,7 @@ struct SettingsView: View { } .onAppear { viewModel.objectWillChange.send() - isSmartAccountEnabled = SmartAccountManager.shared.isSmartAccountEnabled + isSmartAccountEnabled = SmartAccountEnabler.shared.isSmartAccountEnabled } } diff --git a/Package.swift b/Package.swift index 4a775415c..3e3db747b 100644 --- a/Package.swift +++ b/Package.swift @@ -93,7 +93,7 @@ let package = Package( resources: [.process("Resources/PrivacyInfo.xcprivacy")]), .target( name: "ReownWalletKit", - dependencies: ["WalletConnectSign", "WalletConnectPush", "WalletConnectVerify"], + dependencies: ["WalletConnectSign", "WalletConnectPush", "WalletConnectVerify", "YttriumWrapper"], path: "Sources/ReownWalletKit", resources: [.process("Resources/PrivacyInfo.xcprivacy")]), .target( diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index 031a6f75b..92a2ef591 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -3,12 +3,11 @@ import Foundation class SafesManager { var ownerToClient: [Account: AccountClient] = [:] - let entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" // v0.7 on Sepolia let rpcUrl: String - let bundlerUrl: String + let apiKey: String - init(bundlerUrl: String, rpcUrl: String) { - self.bundlerUrl = bundlerUrl + init(pimlicoApiKey: String, rpcUrl: String) { + self.apiKey = pimlicoApiKey self.rpcUrl = rpcUrl } @@ -24,8 +23,8 @@ class SafesManager { } private func createSafe(ownerAccount: Account) -> AccountClient { - - let pimlicoBundlerUrl = "https://\(bundlerUrl)" + let chainId = ownerAccount.reference + let pimlicoBundlerUrl = "https://api.pimlico.io/v2/\(chainId)/rpc?apikey=\(apiKey)" let rpcUrl = "https://\(rpcUrl)" let pimlicoSepolia = YttriumWrapper.Config( endpoints: .init( @@ -37,7 +36,7 @@ class SafesManager { // use YttriumWrapper.Config.local() for local foundry node return AccountClient( ownerAddress: ownerAccount.address, - entryPoint: entryPoint, + entryPoint: "", // remove the entrypoint chainId: Int(ownerAccount.blockchain.reference)!, config: pimlicoSepolia, safe: true diff --git a/Sources/ReownWalletKit/WalletKit.swift b/Sources/ReownWalletKit/WalletKit.swift index e53c234e7..ef5b02c84 100644 --- a/Sources/ReownWalletKit/WalletKit.swift +++ b/Sources/ReownWalletKit/WalletKit.swift @@ -44,12 +44,12 @@ public class WalletKit { crypto: CryptoProvider, pushHost: String = "echo.walletconnect.com", environment: APNSEnvironment = .production, - bundlerUrl: String? = nil, + pimlicoApiKey: String? = nil, rpcUrl: String? = nil ) { Pair.configure(metadata: metadata) Push.configure(pushHost: pushHost, environment: environment) Sign.configure(crypto: crypto) - WalletKit.config = WalletKit.Config(crypto: crypto, bundlerUrl: bundlerUrl, rpcUrl: rpcUrl) + WalletKit.config = WalletKit.Config(crypto: crypto, pimlicoApiKey: pimlicoApiKey, rpcUrl: rpcUrl) } } diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 3126d8a6a..d87eae5a5 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -7,6 +7,9 @@ import Combine /// /// Access via `WalletKit.instance` public class WalletKitClient { + enum Errors: Error { + case smartAccountNotEnabled + } // MARK: - Public Properties /// Publisher that sends session proposal @@ -267,9 +270,39 @@ public class WalletKitClient { // MARK: Yttrium -// public func prepareSendTransactions(_ transactions: [Transaction]) async throws -> PreparedSendTransaction { -// -// } + public func prepareSendTransactions(_ transactions: [Transaction], ownerAccount: Account) async throws -> PreparedSendTransaction { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + return try await client.prepareSendTransactions(transactions) + } + + public func doSendTransaction(signatures: [OwnerSignature], params: String, ownerAccount: Account) async throws -> String { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + return try await client.doSendTransaction(signatures: signatures, params: params) + } + + public func getSmartAccount(ownerAccount: Account) async throws -> Account { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + let address = try await client.getAddress() + // it's safe to force unwrap here because we know that the address and the chain are valid + return Account(blockchain: ownerAccount.blockchain, address: address)! + } + + public func waitForUserOperationReceipt(userOperationHash: String, ownerAccount: Account) async throws -> UserOperationReceipt { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + return try await client.waitForUserOperationReceipt(userOperationHash: userOperationHash) + } } diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index 5066abb3c..e4f102839 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -8,9 +8,9 @@ struct WalletKitClientFactory { config: WalletKit.Config ) -> WalletKitClient { var safesManager: SafesManager? = nil - if let bundlerUrl = config.bundlerUrl, + if let pimlicoApiKey = config.pimlicoApiKey, let rpcUrl = config.rpcUrl { - safesManager = SafesManager(bundlerUrl: bundlerUrl, rpcUrl: rpcUrl) + safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey, rpcUrl: rpcUrl) } return WalletKitClient( signClient: signClient, diff --git a/Sources/ReownWalletKit/Web3WalletConfig.swift b/Sources/ReownWalletKit/Web3WalletConfig.swift index 98c25c64c..a16a665a8 100644 --- a/Sources/ReownWalletKit/Web3WalletConfig.swift +++ b/Sources/ReownWalletKit/Web3WalletConfig.swift @@ -3,7 +3,7 @@ import Foundation extension WalletKit { struct Config { let crypto: CryptoProvider - let bundlerUrl: String? + let pimlicoApiKey: String? let rpcUrl: String? } } From 6da1b2a346fd739da34a0d96b2e6ef8e64d5c37c Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 17 Oct 2024 08:17:35 +0200 Subject: [PATCH 04/73] savepoint --- Example/Shared/Signer/Signer.swift | 18 +++++++++++------- .../ApplicationLayer/AppDelegate.swift | 3 --- .../ConfigurationService.swift | 2 +- .../ApplicationLayer/SceneDelegate.swift | 2 +- Example/WalletApp/Other/Info.plist | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 178896516..a3baf9b28 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -21,6 +21,7 @@ final class Signer { enum Errors: Error { case notImplemented case accountForRequestNotFound + case cantFindRequestedAddress } private init() {} @@ -63,7 +64,7 @@ final class Signer { } } - throw cantGetRequestedAddress + throw Errors.cantFindRequestedAddress } private static func signWithEOA(request: Request, importAccount: ImportAccount) throws -> AnyCodable { @@ -95,14 +96,16 @@ final class Signer { case "personal_sign": let params = try request.params.get([String].self) let message = params[0] - let signedMessage = try WalletKit.instance.signMessage(message) - return AnyCodable(signedMessage) + fatalError("not implemented") +// let signedMessage = try WalletKit.instance.signMessage(message) +// return AnyCodable(signedMessage) case "eth_signTypedData": let params = try request.params.get([String].self) let message = params[0] - let signedMessage = try WalletKit.instance.signMessage(message) - return AnyCodable(signedMessage) + fatalError("not implemented") +// let signedMessage = try WalletKit.instance.signMessage(message) +// return AnyCodable(signedMessage) case "eth_sendTransaction": let params = try request.params.get([YttriumWrapper.Transaction].self) @@ -131,7 +134,7 @@ final class Signer { ) } - let prepareSendTransactions = try await accountClient.prepareSendTransactions(transactions) + let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions(transactions, ownerAccount: ownerAccount) let signer = ETHSigner(importAccount: importAccount) @@ -139,7 +142,7 @@ final class Signer { let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - let userOpHash = try await accountClient.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams) + let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) return AnyCodable(userOpHash) @@ -154,6 +157,7 @@ extension Signer.Errors: LocalizedError { 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" } } } diff --git a/Example/WalletApp/ApplicationLayer/AppDelegate.swift b/Example/WalletApp/ApplicationLayer/AppDelegate.swift index 993235565..e595b71d5 100644 --- a/Example/WalletApp/ApplicationLayer/AppDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/AppDelegate.swift @@ -8,9 +8,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - let entryPointAddress = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" // v0.7 on Sepolia - let chainId = 11155111 // Sepolia - SmartAccountSafe.instance.configure(entryPoint: entryPointAddress, chainId: chainId) return true } diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index a9fc7c826..c0e59672c 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -24,7 +24,7 @@ final class ConfigurationService { redirect: try! AppMetadata.Redirect(native: "walletapp://", universal: "https://lab.web3modal.com/wallet", linkMode: true) ) - WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) + WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey, rpcUrl: InputConfig.rpcUrl) Notify.configure( environment: BuildConfiguration.shared.apnsEnvironment, diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index 4b885cca4..8efacb2ca 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -138,7 +138,7 @@ private extension SceneDelegate { redirect: try! AppMetadata.Redirect(native: "walletapp://", universal: "https://lab.web3modal.com/wallet", linkMode: true) ) - WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) + WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey, rpcUrl: InputConfig.rpcUrl) } } diff --git a/Example/WalletApp/Other/Info.plist b/Example/WalletApp/Other/Info.plist index 12593cb44..786e8c3cd 100644 --- a/Example/WalletApp/Other/Info.plist +++ b/Example/WalletApp/Other/Info.plist @@ -30,8 +30,8 @@ INSendMessageIntent - PIMLICO_BUNDLER_URL - $(PIMLICO_BUNDLER_URL) + PIMLICO_API_KEY + $(PIMLICO_API_KEY) PROJECT_ID $(PROJECT_ID) RPC_URL From 1028f23653f24cbd9e412c6c00b65d13d7f805d6 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 18 Oct 2024 08:35:31 +0200 Subject: [PATCH 05/73] add yttrium to podspec --- Example/Shared/Signer/Signer.swift | 4 ++-- .../Wallet/Settings/SettingsPresenter.swift | 6 +++--- Sources/ReownWalletKit/SmartAccount/SafesManager.swift | 5 ++++- Sources/ReownWalletKit/WalletKitClient.swift | 4 ++-- reown-swift.podspec | 1 + 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index a3baf9b28..f0ad2272d 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -117,7 +117,7 @@ final class Signer { let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) + let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) return AnyCodable(userOpHash) case "wallet_sendCalls": @@ -142,7 +142,7 @@ final class Signer { let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) + let userOpHash = try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) return AnyCodable(userOpHash) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 34afdb42d..6aa73cd26 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -11,7 +11,6 @@ final class SettingsPresenter: ObservableObject { private let router: SettingsRouter 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, importAccount: ImportAccount) { @@ -73,7 +72,8 @@ final class SettingsPresenter: ObservableObject { func sendTransaction() async throws -> String { - let ownerAccount = importAccount.account + // hardcoded sepolia + let ownerAccount = try! Account(blockchain: Blockchain("eip155:11155111")!, accountAddress: importAccount.account.address) let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions( [.init( @@ -89,7 +89,7 @@ final class SettingsPresenter: ObservableObject { let ownerSignature = OwnerSignature(owner: ownerAccount.address, signature: signature) - return try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], params: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) + return try await WalletKit.instance.doSendTransaction(signatures: [ownerSignature], doSendTransactionParams: prepareSendTransactions.doSendTransactionParams, ownerAccount: ownerAccount) } func logoutPressed() async throws { diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index 92a2ef591..0b0413179 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -34,12 +34,15 @@ class SafesManager { ) ) // use YttriumWrapper.Config.local() for local foundry node - return AccountClient( + let x = AccountClient( ownerAddress: ownerAccount.address, entryPoint: "", // remove the entrypoint chainId: Int(ownerAccount.blockchain.reference)!, config: pimlicoSepolia, safe: true ) + // TODO remove registration + x.register(privateKey: "ff89825a799afce0d5deaa079cdde227072ec3f62973951683ac8cc033092156") + return x } } diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index d87eae5a5..634f8843a 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -278,12 +278,12 @@ public class WalletKitClient { return try await client.prepareSendTransactions(transactions) } - public func doSendTransaction(signatures: [OwnerSignature], params: String, ownerAccount: Account) async throws -> String { + public func doSendTransaction(signatures: [OwnerSignature], doSendTransactionParams: String, ownerAccount: Account) async throws -> String { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled } let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - return try await client.doSendTransaction(signatures: signatures, params: params) + return try await client.doSendTransaction(signatures: signatures, params: doSendTransactionParams) } public func getSmartAccount(ownerAccount: Account) async throws -> Account { diff --git a/reown-swift.podspec b/reown-swift.podspec index 6279b33b7..8b32821a7 100644 --- a/reown-swift.podspec +++ b/reown-swift.podspec @@ -26,6 +26,7 @@ Pod::Spec.new do |spec| spec.subspec 'WalletKit' do |ss| ss.source_files = 'Sources/ReownWalletKit/**/*.{h,m,swift}' + ss.dependency 'YttriumWrapper', '~> 0.2.1' ss.dependency 'reown-swift/WalletConnectSign' ss.dependency 'reown-swift/WalletConnectPush' ss.dependency 'reown-swift/WalletConnectVerify' From 76a8caac81effd2069458eb7f734917d5a33fd73 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 28 Oct 2024 08:50:56 +0100 Subject: [PATCH 06/73] fix imports --- Sources/ReownWalletKit/SmartAccount/SafesManager.swift | 1 + Sources/ReownWalletKit/WalletKitClient.swift | 1 + reown-swift.podspec | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index 0b0413179..549d19fe6 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -1,5 +1,6 @@ import Foundation +import YttriumWrapper class SafesManager { var ownerToClient: [Account: AccountClient] = [:] diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 634f8843a..08f0ced79 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import YttriumWrapper /// Web3 Wallet Client /// diff --git a/reown-swift.podspec b/reown-swift.podspec index 8b32821a7..5c84b8674 100644 --- a/reown-swift.podspec +++ b/reown-swift.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |spec| spec.subspec 'WalletKit' do |ss| ss.source_files = 'Sources/ReownWalletKit/**/*.{h,m,swift}' - ss.dependency 'YttriumWrapper', '~> 0.2.1' + ss.dependency 'YttriumWrapper', '~> 0.2.0' ss.dependency 'reown-swift/WalletConnectSign' ss.dependency 'reown-swift/WalletConnectPush' ss.dependency 'reown-swift/WalletConnectVerify' From cf6e079b349224fc63fff7e16bd65f98f1604c02 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 28 Oct 2024 10:00:22 +0100 Subject: [PATCH 07/73] savepoint --- reown-swift.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reown-swift.podspec b/reown-swift.podspec index 5c84b8674..743fa9491 100644 --- a/reown-swift.podspec +++ b/reown-swift.podspec @@ -117,7 +117,7 @@ Pod::Spec.new do |spec| ss.source_files = 'Sources/WalletConnectRelay/**/*.{h,m,swift}' ss.dependency 'reown-swift/WalletConnectJWT' ss.resource_bundles = { - 'WalletConnect_WalletConnectRelay' => [ + 'reown_WalletConnectRelay' => [ 'Sources/WalletConnectRelay/PackageConfig.json' ] } From 5639d1961df61e2565ca3e8bcf26b64d991bec9c Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 28 Oct 2024 11:38:16 +0100 Subject: [PATCH 08/73] remove rpc url injection --- Example/Shared/InputConfig.swift | 4 ---- .../WalletApp/ApplicationLayer/ConfigurationService.swift | 2 +- Example/WalletApp/ApplicationLayer/SceneDelegate.swift | 2 +- Sources/ReownWalletKit/SmartAccount/SafesManager.swift | 7 +++---- Sources/ReownWalletKit/WalletKit.swift | 5 ++--- Sources/ReownWalletKit/WalletKitClientFactory.swift | 5 ++--- Sources/ReownWalletKit/Web3WalletConfig.swift | 1 - 7 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Example/Shared/InputConfig.swift b/Example/Shared/InputConfig.swift index 9099ad59c..825461c40 100644 --- a/Example/Shared/InputConfig.swift +++ b/Example/Shared/InputConfig.swift @@ -21,10 +21,6 @@ struct InputConfig { return config(for: "PIMLICO_API_KEY") } - static var rpcUrl: String? { - return config(for: "RPC_URL") - } - private static func config(for key: String) -> String? { return Bundle.main.object(forInfoDictionaryKey: key) as? String } diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index c0e59672c..5772e3e0b 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -24,7 +24,7 @@ final class ConfigurationService { redirect: try! AppMetadata.Redirect(native: "walletapp://", universal: "https://lab.web3modal.com/wallet", linkMode: true) ) - WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey, rpcUrl: InputConfig.rpcUrl) + WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey) Notify.configure( environment: BuildConfiguration.shared.apnsEnvironment, diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift index 8efacb2ca..875350c92 100644 --- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift +++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift @@ -138,7 +138,7 @@ private extension SceneDelegate { redirect: try! AppMetadata.Redirect(native: "walletapp://", universal: "https://lab.web3modal.com/wallet", linkMode: true) ) - WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey, rpcUrl: InputConfig.rpcUrl) + WalletKit.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment, pimlicoApiKey: InputConfig.pimlicoApiKey) } } diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index 549d19fe6..fa3f97ab7 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -4,12 +4,10 @@ import YttriumWrapper class SafesManager { var ownerToClient: [Account: AccountClient] = [:] - let rpcUrl: String let apiKey: String - init(pimlicoApiKey: String, rpcUrl: String) { + init(pimlicoApiKey: String) { self.apiKey = pimlicoApiKey - self.rpcUrl = rpcUrl } func getOrCreateSafe(for owner: Account) -> AccountClient { @@ -25,8 +23,9 @@ class SafesManager { private func createSafe(ownerAccount: Account) -> AccountClient { let chainId = ownerAccount.reference + let projectId = Networking.projectId let pimlicoBundlerUrl = "https://api.pimlico.io/v2/\(chainId)/rpc?apikey=\(apiKey)" - let rpcUrl = "https://\(rpcUrl)" + let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(ownerAccount.blockchainIdentifier)&projectId=\(projectId)" let pimlicoSepolia = YttriumWrapper.Config( endpoints: .init( rpc: .init(baseURL: rpcUrl), diff --git a/Sources/ReownWalletKit/WalletKit.swift b/Sources/ReownWalletKit/WalletKit.swift index ef5b02c84..9d091d44f 100644 --- a/Sources/ReownWalletKit/WalletKit.swift +++ b/Sources/ReownWalletKit/WalletKit.swift @@ -44,12 +44,11 @@ public class WalletKit { crypto: CryptoProvider, pushHost: String = "echo.walletconnect.com", environment: APNSEnvironment = .production, - pimlicoApiKey: String? = nil, - rpcUrl: String? = nil + pimlicoApiKey: String? = nil ) { Pair.configure(metadata: metadata) Push.configure(pushHost: pushHost, environment: environment) Sign.configure(crypto: crypto) - WalletKit.config = WalletKit.Config(crypto: crypto, pimlicoApiKey: pimlicoApiKey, rpcUrl: rpcUrl) + WalletKit.config = WalletKit.Config(crypto: crypto, pimlicoApiKey: pimlicoApiKey) } } diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index e4f102839..693a70111 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -8,9 +8,8 @@ struct WalletKitClientFactory { config: WalletKit.Config ) -> WalletKitClient { var safesManager: SafesManager? = nil - if let pimlicoApiKey = config.pimlicoApiKey, - let rpcUrl = config.rpcUrl { - safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey, rpcUrl: rpcUrl) + if let pimlicoApiKey = config.pimlicoApiKey { + safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey) } return WalletKitClient( signClient: signClient, diff --git a/Sources/ReownWalletKit/Web3WalletConfig.swift b/Sources/ReownWalletKit/Web3WalletConfig.swift index a16a665a8..c348239e7 100644 --- a/Sources/ReownWalletKit/Web3WalletConfig.swift +++ b/Sources/ReownWalletKit/Web3WalletConfig.swift @@ -4,6 +4,5 @@ extension WalletKit { struct Config { let crypto: CryptoProvider let pimlicoApiKey: String? - let rpcUrl: String? } } From a9ec912179fa686454581b245bde264af8898222 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 28 Oct 2024 12:37:33 +0100 Subject: [PATCH 09/73] bump up version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 3e3db747b..138ce5d41 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ func buildYttriumWrapperTarget() -> Target { swiftSettings: yttriumSwiftSettings ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .upToNextMinor(from: "0.1.0"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .upToNextMinor(from: "0.2.0"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From c4fc620d80e015cd9689265df3bb2881fc851c45 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 29 Oct 2024 10:58:58 +0100 Subject: [PATCH 10/73] test ci --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 138ce5d41..93f7762c6 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = true +let yttriumDebug = false // Define dependencies array From ba5919bcb07871d48a000865d8fdf202c0dae66a Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 29 Oct 2024 11:01:30 +0100 Subject: [PATCH 11/73] set exact yttrium version --- .../xcshareddata/swiftpm/Package.resolved | 18 ++++++++++++++++++ Package.swift | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 421868a5e..12f0f134c 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -109,6 +109,15 @@ "version": "1.0.0" } }, + { + "package": "swift-dotenv", + "repositoryURL": "https://github.com/thebarndog/swift-dotenv.git", + "state": { + "branch": null, + "revision": "f6e7ca817d35eeccb20b62c87ee75963b01b29dc", + "version": "2.0.1" + } + }, { "package": "swift-qrcode-generator", "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", @@ -180,6 +189,15 @@ "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", "version": "1.0.2" } + }, + { + "package": "yttrium", + "repositoryURL": "https://github.com/reown-com/yttrium", + "state": { + "branch": null, + "revision": "f5f6d032edc3fe2115f7d3c9eba4aa96461d246d", + "version": "0.2.0" + } } ] }, diff --git a/Package.swift b/Package.swift index 93f7762c6..8c74a5229 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ func buildYttriumWrapperTarget() -> Target { swiftSettings: yttriumSwiftSettings ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .upToNextMinor(from: "0.2.0"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.0"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From c58984a8a80f9973acf82ee2b8b9c134f8166e32 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 30 Oct 2024 11:45:13 +0100 Subject: [PATCH 12/73] savepoint --- Example/Shared/Signer/Signer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index f0ad2272d..08fed31dc 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -129,8 +129,8 @@ final class Signer { let transactions = calls.map { YttriumWrapper.Transaction( to: $0.to!, - value: $0.value!, - data: $0.data! + value: $0.value ?? "0", + data: $0.data ?? "" ) } From aaa3566c57050ca3454cb7f1baf9e9d30f060d03 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 30 Oct 2024 11:55:37 +0100 Subject: [PATCH 13/73] update config file --- Configuration.xcconfig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Configuration.xcconfig b/Configuration.xcconfig index 37814dca8..0bb3602ca 100644 --- a/Configuration.xcconfig +++ b/Configuration.xcconfig @@ -23,8 +23,5 @@ EXPLORER_HOST = explorer-api.walletconnect.com // MIXPANEL_TOKEN = MIXPANEL_TOKEN -// pimloco bundler url including api key -// PIMLICO_BUNDLER_URL = PIMLICO_BUNDLER_URL - -// rpc url -// RPC_URL = RPC_URL +// pimloco api key +// PIMLICO_API_KEY = PIMLICO_API_KEY From e3d7ee58c7b097d8f0bb245cba7361208bd6f5a9 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 31 Oct 2024 09:03:36 +0100 Subject: [PATCH 14/73] query receipt --- .../xcshareddata/swiftpm/Package.resolved | 18 ------------------ Example/Shared/Signer/Signer.swift | 10 ++++++++++ Package.swift | 2 +- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 12f0f134c..421868a5e 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -109,15 +109,6 @@ "version": "1.0.0" } }, - { - "package": "swift-dotenv", - "repositoryURL": "https://github.com/thebarndog/swift-dotenv.git", - "state": { - "branch": null, - "revision": "f6e7ca817d35eeccb20b62c87ee75963b01b29dc", - "version": "2.0.1" - } - }, { "package": "swift-qrcode-generator", "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", @@ -189,15 +180,6 @@ "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", "version": "1.0.2" } - }, - { - "package": "yttrium", - "repositoryURL": "https://github.com/reown-com/yttrium", - "state": { - "branch": null, - "revision": "f5f6d032edc3fe2115f7d3c9eba4aa96461d246d", - "version": "0.2.0" - } } ] }, diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 08fed31dc..54a2f4e97 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -144,6 +144,16 @@ final class Signer { 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, transaction hash: \(receipt.receipt.transactionHash)" + AlertPresenter.present(message: message, type: .success) + } catch { + AlertPresenter.present(message: error.localizedDescription, type: .error) + } + } + return AnyCodable(userOpHash) default: diff --git a/Package.swift b/Package.swift index 8c74a5229..278c3e64b 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true // Define dependencies array From 180975a4f969cb234a950b2342fac78a320c2d18 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 31 Oct 2024 09:06:27 +0100 Subject: [PATCH 15/73] fix tests --- .../XPlatform/Web3Wallet/XPlatformW3WTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift index 712cd36fb..cbbffc570 100644 --- a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift +++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift @@ -65,7 +65,8 @@ final class XPlatformW3WTests: XCTestCase { walletKitClient = WalletKitClientFactory.create( signClient: signClient, pairingClient: pairingClient, - pushClient: PushClientMock()) + pushClient: PushClientMock(), + config: WalletKit.Config(crypto: DefaultCryptoProvider(), pimlicoApiKey: nil)) } func testSessionSettle() async throws { From b922830ec1643e7925905f79102ec4ab612fed53 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 4 Nov 2024 08:30:44 +0100 Subject: [PATCH 16/73] update yttrium version --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 278c3e64b..649dbd3ba 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = true +let yttriumDebug = false // Define dependencies array @@ -30,7 +30,7 @@ func buildYttriumWrapperTarget() -> Target { swiftSettings: yttriumSwiftSettings ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.0"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.7"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From 0c1b9de1739b98fa30516de132f17fe3139a82b4 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 8 Nov 2024 15:51:05 +0100 Subject: [PATCH 17/73] savepoint --- Package.swift | 4 +- Sources/ReownWalletKit/WalletKitClient.swift | 1 + .../Verifier/MessageVerifier.swift | 47 ++++++++++++------- .../Verifier/MessageVerifierFactory.swift | 3 +- 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/Package.swift b/Package.swift index 649dbd3ba..4a8f5606c 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true // Define dependencies array @@ -88,7 +88,7 @@ let package = Package( targets: [ .target( name: "WalletConnectSign", - dependencies: ["WalletConnectPairing", "WalletConnectVerify", "WalletConnectSigner", "Events"], + dependencies: ["WalletConnectPairing", "WalletConnectVerify", "WalletConnectSigner", "Events", "YttriumWrapper"], path: "Sources/WalletConnectSign", resources: [.process("Resources/PrivacyInfo.xcprivacy")]), .target( diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 08f0ced79..7fc1b0a40 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -293,6 +293,7 @@ public class WalletKitClient { } let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) let address = try await client.getAddress() + // it's safe to force unwrap here because we know that the address and the chain are valid return Account(blockchain: ownerAccount.blockchain, address: address)! } diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index d1a9680a6..cf7ddbb60 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -1,4 +1,5 @@ import Foundation +import YttriumWrapper public struct MessageVerifier { @@ -9,9 +10,16 @@ public struct MessageVerifier { private let eip191Verifier: EIP191Verifier private let eip1271Verifier: EIP1271Verifier - init(eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier) { + private let crypto: CryptoProvider + + init( + eip191Verifier: EIP191Verifier, + eip1271Verifier: EIP1271Verifier, + crypto: CryptoProvider + ) { self.eip191Verifier = eip191Verifier self.eip1271Verifier = eip1271Verifier + self.crypto = crypto } public func verify(signature: CacaoSignature, @@ -38,21 +46,28 @@ public struct MessageVerifier { let signatureData = Data(hex: signature.s) - switch signature.t { - case .eip191: - return try await eip191Verifier.verify( - signature: signatureData, - message: messageData.prefixed, - address: address - ) - case .eip1271: - return try await eip1271Verifier.verify( - signature: signatureData, - message: messageData.prefixed, - address: address, - chainId: chainId - ) - } + let erc6492Client = Erc6492Client("".intoRustString()) + + let messageHash = crypto.keccak256(messageData) + + returns false if sig is not valid + _ = try await erc6492Client.verify_signature(signature.s.intoRustString(), address.intoRustString(), messageHash.toHexString().intoRustString()) + +// switch signature.t { +// case .eip191: +// return try await eip191Verifier.verify( +// signature: signatureData, +// message: messageData.prefixed, +// address: address +// ) +// case .eip1271: +// return try await eip1271Verifier.verify( +// signature: signatureData, +// message: messageData.prefixed, +// address: address, +// chainId: chainId +// ) +// } } public func verify(signature: String, diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift index 2dcc4255c..5bea00997 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift @@ -13,6 +13,7 @@ public struct MessageVerifierFactory { } public func create(projectId: String) -> MessageVerifier { - return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto)) + + return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto), crypto: crypto) } } From 6a30c3c5422578024daf21e81c24825037ad8ff8 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 12 Nov 2024 11:03:06 +0100 Subject: [PATCH 18/73] add erc6492Client --- .../Auth/Signer/CacaoSignerTests.swift | 7 +- .../Verifier/MessageVerifier.swift | 114 +++++++++++------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift b/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift index c35cd57d5..782c6370a 100644 --- a/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift +++ b/Example/IntegrationTests/Auth/Signer/CacaoSignerTests.swift @@ -57,6 +57,11 @@ class CacaoSignerTest: XCTestCase { } func testCacaoVerify() async throws { - try await verifier.verify(signature: signature, message: message, address: "0x15bca56b6e2728aec2532df9d436bd1600e86688", chainId: "eip155:1") + do { + try await verifier.verify(signature: signature, message: message, address: "0x15bca56b6e2728aec2532df9d436bd1600e86688", chainId: "eip155:1") + } catch { + print(error) + throw error + } } } diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index cf7ddbb60..5aaa81705 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -3,13 +3,34 @@ import YttriumWrapper public struct MessageVerifier { - enum Errors: Error { + enum Errors: LocalizedError { case utf8EncodingFailed + case invalidSignature(message: String) + case invalidAddress(message: String) + case invalidMessageHash(message: String) + case verificationFailed(message: String) + case custom(message: String) + + var errorDescription: String? { + switch self { + case .utf8EncodingFailed: + return "Failed to encode string using UTF-8." + case .invalidSignature(let message): + return "Invalid signature: \(message)" + case .invalidAddress(let message): + return "Invalid address: \(message)" + case .invalidMessageHash(let message): + return "Invalid message hash: \(message)" + case .verificationFailed(let message): + return "Verification failed: \(message)" + case .custom(let message): + return message + } + } } private let eip191Verifier: EIP191Verifier private let eip1271Verifier: EIP1271Verifier - private let crypto: CryptoProvider init( @@ -39,35 +60,7 @@ public struct MessageVerifier { address: String, chainId: String ) async throws { - - guard let messageData = message.data(using: .utf8) else { - throw Errors.utf8EncodingFailed - } - - let signatureData = Data(hex: signature.s) - - let erc6492Client = Erc6492Client("".intoRustString()) - - let messageHash = crypto.keccak256(messageData) - - returns false if sig is not valid - _ = try await erc6492Client.verify_signature(signature.s.intoRustString(), address.intoRustString(), messageHash.toHexString().intoRustString()) - -// switch signature.t { -// case .eip191: -// return try await eip191Verifier.verify( -// signature: signatureData, -// message: messageData.prefixed, -// address: address -// ) -// case .eip1271: -// return try await eip1271Verifier.verify( -// signature: signatureData, -// message: messageData.prefixed, -// address: address, -// chainId: chainId -// ) -// } + try await verifySignature(signature.s, message: message, address: address, chainId: chainId) } public func verify(signature: String, @@ -75,28 +68,63 @@ public struct MessageVerifier { address: String, chainId: String ) async throws { + try await verifySignature(signature, message: message, address: address, chainId: chainId) + } + // Private helper method containing the common logic + private func verifySignature(_ signatureString: String, + message: String, + address: String, + chainId: String + ) async throws { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } - let signatureData = Data(hex: signature) + + let signatureData = Data(hex: signatureString) + + let projectId = Networking.projectId + + let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + + let erc6492Client = Erc6492Client(rpcUrl.intoRustString()) let prefixedMessage = messageData.prefixed + let messageHash = crypto.keccak256(prefixedMessage) + do { - try await eip191Verifier.verify( - signature: signatureData, - message: prefixedMessage, - address: address + let result = try await erc6492Client.verify_signature( + signatureString.intoRustString(), + address.intoRustString(), + messageHash.toHexString().intoRustString() ) + + if result == true { + return + } else { + throw Errors.verificationFailed(message: "Signature verification failed.") + } + } catch let ffiError as Erc6492Error { + switch ffiError { + case .InvalidSignature(let x): + let errorMessage = x.toString() + throw Errors.invalidSignature(message: errorMessage) + case .InvalidAddress(let x): + let errorMessage = x.toString() + throw Errors.invalidAddress(message: errorMessage) + case .InvalidMessageHash(let x): + let errorMessage = x.toString() + throw Errors.invalidMessageHash(message: errorMessage) + case .Verification(let x): + let errorMessage = x.toString() + throw Errors.verificationFailed(message: errorMessage) + default: + let errorMessage = "An unknown error occurred." + throw Errors.custom(message: errorMessage) + } } catch { - // If eip191 verification fails, try eip1271 verification - try await eip1271Verifier.verify( - signature: signatureData, - message: prefixedMessage, - address: address, - chainId: chainId - ) + throw error } } } From 6e10cbcbb2fa21dddd303ad24eae96f4845e9641 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 12 Nov 2024 11:21:01 +0100 Subject: [PATCH 19/73] savepoint --- Sources/WalletConnectSigner/Verifier/MessageVerifier.swift | 7 ++++--- .../Verifier/MessageVerifierFactory.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index 5aaa81705..442fff7ae 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -32,15 +32,18 @@ public struct MessageVerifier { private let eip191Verifier: EIP191Verifier private let eip1271Verifier: EIP1271Verifier private let crypto: CryptoProvider + private let projectId: String init( eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier, - crypto: CryptoProvider + crypto: CryptoProvider, + projectId: String ) { self.eip191Verifier = eip191Verifier self.eip1271Verifier = eip1271Verifier self.crypto = crypto + self.projectId = projectId } public func verify(signature: CacaoSignature, @@ -83,8 +86,6 @@ public struct MessageVerifier { let signatureData = Data(hex: signatureString) - let projectId = Networking.projectId - let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" let erc6492Client = Erc6492Client(rpcUrl.intoRustString()) diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift index 5bea00997..e67953cbb 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift @@ -14,6 +14,6 @@ public struct MessageVerifierFactory { public func create(projectId: String) -> MessageVerifier { - return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto), crypto: crypto) + return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto), crypto: crypto, projectId: projectId) } } From 894b2e246e372e1a4232d77e39eefa6096594dbe Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 12 Nov 2024 13:21:47 +0100 Subject: [PATCH 20/73] add 191 verifier --- .../Verifier/MessageVerifier.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index 442fff7ae..1fce4d6e7 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -85,13 +85,23 @@ public struct MessageVerifier { } let signatureData = Data(hex: signatureString) + let prefixedMessage = messageData.prefixed - let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + // Try eip191 verification first for better performance + do { + try await eip191Verifier.verify( + signature: signatureData, + message: prefixedMessage, + address: address + ) + return // If 6492 verification succeeds, we’re done + } catch { + // If eip191 verification fails, we’ll attempt 6492 verification + } + // Fallback to 6492 verification + let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" let erc6492Client = Erc6492Client(rpcUrl.intoRustString()) - - let prefixedMessage = messageData.prefixed - let messageHash = crypto.keccak256(prefixedMessage) do { From 588cb8443b2599912291d9d8be160613a40dfead Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 13 Nov 2024 07:55:56 +0100 Subject: [PATCH 21/73] savepoint --- Sources/WalletConnectSigner/Verifier/MessageVerifier.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index 1fce4d6e7..bc60b39e0 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -100,6 +100,7 @@ public struct MessageVerifier { } // Fallback to 6492 verification + print("i was called only once") let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" let erc6492Client = Erc6492Client(rpcUrl.intoRustString()) let messageHash = crypto.keccak256(prefixedMessage) From e5c167785a08d1b241690db8106b39c20f9ad47c Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 13 Nov 2024 10:16:48 +0100 Subject: [PATCH 22/73] expose 3 step signing on walletkit --- Sources/ReownWalletKit/WalletKitClient.swift | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 7fc1b0a40..ab54941aa 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -305,6 +305,32 @@ public class WalletKitClient { let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) return try await client.waitForUserOperationReceipt(userOperationHash: userOperationHash) } + + public func prepareSignMessage(_ messageHash: String, ownerAccount: Account) async throws -> PreparedSignMessage { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + return try await client.prepareSignMessage(messageHash) + } + + public func doSignMessage(_ signatures: [String], ownerAccount: Account) async throws -> String { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + let signature = try await client.doSignMessage(signatures) + return signature + } + + public func finalizeSignMessage(_ signatures: [String], signStep3Params: String, ownerAccount: Account) async throws -> String { + guard let smartAccountsManager = smartAccountsManager else { + throw Errors.smartAccountNotEnabled + } + let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) + let signature = try await client.finalizeSignMessage(signatures, signStep3Params: signStep3Params) + return signature + } } From fb9f6f205fce09459a003e6fa1059355f567b475 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 13 Nov 2024 11:39:15 +0100 Subject: [PATCH 23/73] fix appkit on the simulator --- Sources/ReownAppKit/Core/AppKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ReownAppKit/Core/AppKit.swift b/Sources/ReownAppKit/Core/AppKit.swift index 60836ed09..55addf589 100644 --- a/Sources/ReownAppKit/Core/AppKit.swift +++ b/Sources/ReownAppKit/Core/AppKit.swift @@ -137,7 +137,7 @@ public class AppKit { supportsAuthenticatedSession: (config.authRequestParams != nil) ) - Task { + Task(priority: .background) { try? await w3mApiInteractor.fetchWalletImages(for: store.recentWallets + store.customWallets) try? await w3mApiInteractor.fetchAllWalletMetadata() try? await w3mApiInteractor.fetchFeaturedWallets() From c69e782971505284c7254cda8fdedc3fad996651 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 14 Nov 2024 13:36:49 +0100 Subject: [PATCH 24/73] update 3 step signing --- Example/Shared/Signer/Signer.swift | 65 ++++++++++++++++++-- Sources/ReownWalletKit/WalletKitClient.swift | 2 +- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 54a2f4e97..125a48255 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -2,6 +2,7 @@ import Foundation import Commons import WalletConnectSign import ReownWalletKit +import Web3 struct SendCallsParams: Codable { let version: String @@ -32,7 +33,7 @@ final class Signer { return try signWithEOA(request: request, importAccount: importAccount) } let smartAccount = try await WalletKit.instance.getSmartAccount(ownerAccount: importAccount.account) - if smartAccount.address == requestedAddress { + if smartAccount.address.lowercased() == requestedAddress.lowercased() { return try await signWithSmartAccount(request: request, importAccount: importAccount) } throw Errors.accountForRequestNotFound @@ -95,10 +96,40 @@ final class Signer { switch request.method { case "personal_sign": 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) + let requestedMessage = params[0] + + let messageToSign = Self.prepareMessageToSign(requestedMessage) + + let messageHash = messageToSign.sha3(.keccak256).toHexString() + + // Step 1 + let prepareSignMessage = try await WalletKit.instance.prepareSignMessage(messageHash, ownerAccount: ownerAccount) + + // 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 = try await WalletKit.instance.doSignMessage([result], ownerAccount: ownerAccount) + + 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 = try await WalletKit.instance.finalizeSignMessage([result], signStep3Params: preparedSignStep3.signStep3Params, ownerAccount: ownerAccount) + + return AnyCodable(signature) + } + case "eth_signTypedData": let params = try request.params.get([String].self) @@ -160,6 +191,30 @@ final class Signer { throw Errors.notImplemented } } + + private static func prepareMessageToSign(_ messageToSign: String) -> Bytes { + let dataToSign: Bytes + 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) -> Bytes { + 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: LocalizedError { diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index ab54941aa..c67ffb41c 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -314,7 +314,7 @@ public class WalletKitClient { return try await client.prepareSignMessage(messageHash) } - public func doSignMessage(_ signatures: [String], ownerAccount: Account) async throws -> String { + public func doSignMessage(_ signatures: [String], ownerAccount: Account) async throws -> PreparedSign { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled } From ecbfe26a34108920ba2f14bfa576b1e4df88766a Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 15 Nov 2024 08:24:36 +0100 Subject: [PATCH 25/73] complate 3step signing --- Example/Shared/Signer/Signer.swift | 27 ++++++++++++++----- .../SmartAccount/SafesManager.swift | 1 + .../Verifier/MessageVerifier.swift | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 125a48255..91a7cb2fc 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -103,7 +103,13 @@ final class Signer { let messageHash = messageToSign.sha3(.keccak256).toHexString() // Step 1 - let prepareSignMessage = try await WalletKit.instance.prepareSignMessage(messageHash, ownerAccount: ownerAccount) + 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)! @@ -112,25 +118,34 @@ final class Signer { let result: String = "0x" + r.toHexString() + s.toHexString() + String(v + 27, radix: 16) // Step 2 - let prepareSign = try await WalletKit.instance.doSignMessage([result], ownerAccount: ownerAccount) + 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 = try await WalletKit.instance.finalizeSignMessage([result], signStep3Params: preparedSignStep3.signStep3Params, ownerAccount: ownerAccount) - + 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] diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index fa3f97ab7..14129ec7a 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -42,6 +42,7 @@ class SafesManager { safe: true ) // TODO remove registration + x.register(privateKey: "ff89825a799afce0d5deaa079cdde227072ec3f62973951683ac8cc033092156") return x } diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index bc60b39e0..3d3ba9196 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -94,7 +94,7 @@ public struct MessageVerifier { message: prefixedMessage, address: address ) - return // If 6492 verification succeeds, we’re done + return // If 191 verification succeeds, we’re done } catch { // If eip191 verification fails, we’ll attempt 6492 verification } From 2c35e8f92d224b2e4aa75d3f2529fe99af783563 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 15 Nov 2024 13:21:53 +0100 Subject: [PATCH 26/73] add ca module --- Example/ExampleApp.xcodeproj/project.pbxproj | 20 +++ .../CATransactionModule.swift | 16 ++ .../CATransactionPresenter.swift | 29 ++++ .../CATransactionView.swift | 148 ++++++++++++++++++ .../Wallet/Wallet/WalletPresenter.swift | 4 + .../Wallet/Wallet/WalletRouter.swift | 6 + .../Wallet/Wallet/WalletView.swift | 12 +- 7 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift create mode 100644 Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift create mode 100644 Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 40532900b..7e7624800 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -78,6 +78,9 @@ 84CE642827981DF000142511 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642727981DF000142511 /* Assets.xcassets */; }; 84CE642B27981DF000142511 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84CE642927981DF000142511 /* LaunchScreen.storyboard */; }; 84D093EB2B4EA6CB005B1925 /* ActivityIndicatorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D093EA2B4EA6CB005B1925 /* ActivityIndicatorManager.swift */; }; + 84D88C7F2CE751E7003A6C16 /* CATransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */; }; + 84D88C822CE7525F003A6C16 /* CATransactionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */; }; + 84D88C842CE754CC003A6C16 /* CATransactionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */; }; 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */; }; 84E6B84A29787A8000428BAF /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B84929787A8000428BAF /* NotificationService.swift */; }; 84E6B84E29787A8000428BAF /* PNDecryptionService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 84E6B84729787A8000428BAF /* PNDecryptionService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -381,6 +384,9 @@ 84CE6453279FFE1100142511 /* Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wallet.entitlements; sourceTree = ""; }; 84D093EA2B4EA6CB005B1925 /* ActivityIndicatorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorManager.swift; sourceTree = ""; }; 84D72FC62B4692770057EAF3 /* DApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DApp.entitlements; sourceTree = ""; }; + 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionView.swift; sourceTree = ""; }; + 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionPresenter.swift; sourceTree = ""; }; + 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionModule.swift; sourceTree = ""; }; 84DB38F029828A7C00BFEE37 /* WalletApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletApp.entitlements; sourceTree = ""; }; 84DB38F129828A7F00BFEE37 /* PNDecryptionService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionService.entitlements; sourceTree = ""; }; 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegisterer.swift; sourceTree = ""; }; @@ -875,6 +881,16 @@ path = Auth; sourceTree = ""; }; + 84D88C802CE751EE003A6C16 /* CATransactionModal */ = { + isa = PBXGroup; + children = ( + 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */, + 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */, + 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */, + ); + path = CATransactionModal; + sourceTree = ""; + }; 84E6B84829787A8000428BAF /* PNDecryptionService */ = { isa = PBXGroup; children = ( @@ -1152,6 +1168,7 @@ C56EE22A293F5668004840D1 /* Wallet */, 847BD1E9298A807000076C90 /* Notifications */, 84B815592991217F00FAD54E /* PushMessages */, + 84D88C802CE751EE003A6C16 /* CATransactionModal */, ); path = Wallet; sourceTree = ""; @@ -2017,6 +2034,7 @@ A5D610CA2AB3249100C20083 /* ListingViewModel.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, A5D610CE2AB3594100C20083 /* ListingsAPI.swift in Sources */, + 84D88C7F2CE751E7003A6C16 /* CATransactionView.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, A5D610D42AB35BED00C20083 /* FailableDecodable.swift in Sources */, @@ -2028,8 +2046,10 @@ C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, C5B2F6FC297055B0000DBA0E /* SOLSigner.swift in Sources */, A518119F2A52E83100A52B15 /* SettingsModule.swift in Sources */, + 84D88C842CE754CC003A6C16 /* CATransactionModule.swift in Sources */, C5FFEA7C2ADD897C007282A2 /* BrowserInteractor.swift in Sources */, 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */, + 84D88C822CE7525F003A6C16 /* CATransactionPresenter.swift in Sources */, C55D348D295DD8CA0004314A /* PasteUriView.swift in Sources */, C5F32A2C2954814200A6476E /* ConnectionDetailsModule.swift in Sources */, C56EE249293F566D004840D1 /* ScanInteractor.swift in Sources */, diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift new file mode 100644 index 000000000..635938f1a --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift @@ -0,0 +1,16 @@ +import Foundation +import UIKit + +final class CATransactionModule { + @discardableResult + static func create( + app: Application + ) -> UIViewController { + let presenter = CATransactionPresenter() + let view = CATransactionView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + + return viewController + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift new file mode 100644 index 000000000..5e1b499ca --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -0,0 +1,29 @@ +import UIKit +import Combine + +final class CATransactionPresenter: ObservableObject { + + private var disposeBag = Set() + + init( + ) { + defer { setupInitialState() } + } + + func dismiss() { + + } +} + + +// MARK: - Private functions +private extension CATransactionPresenter { + func setupInitialState() { + + } +} + +// MARK: - SceneViewModel +extension CATransactionPresenter: SceneViewModel { + +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift new file mode 100644 index 000000000..e0fa387c3 --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -0,0 +1,148 @@ +import SwiftUI + +struct CATransactionView: View { + @EnvironmentObject var presenter: CATransactionPresenter + + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 24) { + // Header + Text("Review Transaction") + .font(.headline) + .padding(.top) + + VStack(spacing: 20) { + // Paying Section + VStack(alignment: .leading, spacing: 4) { + Text("Paying") + .foregroundColor(.gray) + Text("10.00 USDC") + .font(.system(.body, design: .monospaced)) + } + .frame(maxWidth: .infinity, alignment: .leading) + + // Source of funds + VStack(alignment: .leading, spacing: 12) { + Text("Source of funds") + .foregroundColor(.gray) + + // Balance Row + HStack { + Image(systemName: "creditcard.circle.fill") + .foregroundColor(.blue) + Text("Balance") + Spacer() + Text("5.00 USDC") + .font(.system(.body, design: .monospaced)) + } + + // Bridging Row + HStack { + Image(systemName: "arrow.left.arrow.right.circle.fill") + .foregroundColor(.gray) + Text("Bridging") + Spacer() + VStack(alignment: .trailing) { + Text("5.00 USDC") + .font(.system(.body, design: .monospaced)) + Text("from Optimism") + .font(.footnote) + .foregroundColor(.gray) + } + } + } + + // App and Network + VStack(spacing: 12) { + HStack { + Text("App") + .foregroundColor(.gray) + Spacer() + Text("https://sampleapp.com") + .foregroundColor(.blue) + } + + HStack { + Text("Network") + .foregroundColor(.gray) + Spacer() + HStack(spacing: 4) { + Image(systemName: "network") + .foregroundColor(.blue) + Text("Arbitrum") + } + } + } + + // Estimated Fees Section + VStack(spacing: 12) { + HStack { + Text("Estimated Fees") + .foregroundColor(.gray) + Spacer() + Text("$4.34") + .font(.system(.body, design: .monospaced)) + } + + VStack(spacing: 8) { + HStack { + Text("Bridge") + .foregroundColor(.gray) + Spacer() + Text("$3.00") + .font(.system(.body, design: .monospaced)) + } + + HStack { + Text("Purchase") + .foregroundColor(.gray) + Spacer() + Text("$1.34") + .font(.system(.body, design: .monospaced)) + } + + HStack { + Text("Execution") + .foregroundColor(.gray) + Spacer() + Text("Fast (~20 sec)") + .font(.system(.body, design: .monospaced)) + } + } + .padding(.leading) + } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) + } + .padding(.horizontal) + + Spacer() + + // Action Buttons + VStack(spacing: 12) { + Button(action: { + // Buy action + }) { + Text("Buy") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .cornerRadius(12) + } + + Button(action: { + dismiss() + }) { + Text("Cancel") + .foregroundColor(.blue) + } + } + .padding() + } + } +} + diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index fcbd1ab39..ae033343b 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -90,6 +90,10 @@ final class WalletPresenter: ObservableObject { } } + func onTest() { + router.presentTest() + } + func removeSession(at indexSet: IndexSet) async { if let index = indexSet.first { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 8109c488f..2252de09a 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 presentTest() { + CATransactionModule.create(app: app) + .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 60bfc75b4..fb38dca78 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -72,7 +72,17 @@ struct WalletView: View { HStack(spacing: 20) { Spacer() - + + Button { + presenter.onTest() + } label: { + Label("Test Transaction", systemImage: "creditcard") + .labelStyle(.iconOnly) // Only show the icon + .frame(width: 56, height: 56) + .background(Circle().fill(Color(.systemBlue))) + } + .shadow(color: .black.opacity(0.25), radius: 8, y: 4) + Button { presenter.onPasteUri() } label: { From a31af90f9df7998fb6aa55ad6775acc0e291e2ab Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 15 Nov 2024 13:43:12 +0100 Subject: [PATCH 27/73] savepoint --- .../CATransactionPresenter.swift | 23 ++++++++----- .../CATransactionView.swift | 34 +++++++++++-------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 5e1b499ca..fc6e59244 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -2,28 +2,35 @@ import UIKit import Combine final class CATransactionPresenter: ObservableObject { + // Published properties to be used in the view + @Published var payingAmount: Double = 10.00 + @Published var balanceAmount: Double = 5.00 + @Published var bridgingAmount: Double = 5.00 + @Published var bridgingSource: String = "Optimism" + @Published var appURL: String = "https://sampleapp.com" + @Published var networkName: String = "Arbitrum" + @Published var estimatedFees: Double = 4.34 + @Published var bridgeFee: Double = 3.00 + @Published var purchaseFee: Double = 1.34 + @Published var executionSpeed: String = "Fast (~20 sec)" private var disposeBag = Set() - init( - ) { + init() { defer { setupInitialState() } } func dismiss() { - + // Implement dismissal logic if needed } } - // MARK: - Private functions private extension CATransactionPresenter { func setupInitialState() { - + // Initialize state if necessary } } // MARK: - SceneViewModel -extension CATransactionPresenter: SceneViewModel { - -} +extension CATransactionPresenter: SceneViewModel {} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index e0fa387c3..b9f988b37 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -2,7 +2,6 @@ import SwiftUI struct CATransactionView: View { @EnvironmentObject var presenter: CATransactionPresenter - @Environment(\.dismiss) private var dismiss var body: some View { @@ -17,7 +16,7 @@ struct CATransactionView: View { VStack(alignment: .leading, spacing: 4) { Text("Paying") .foregroundColor(.gray) - Text("10.00 USDC") + Text("\(presenter.payingAmount, specifier: "%.2f") USDC") .font(.system(.body, design: .monospaced)) } .frame(maxWidth: .infinity, alignment: .leading) @@ -33,7 +32,7 @@ struct CATransactionView: View { .foregroundColor(.blue) Text("Balance") Spacer() - Text("5.00 USDC") + Text("\(presenter.balanceAmount, specifier: "%.2f") USDC") .font(.system(.body, design: .monospaced)) } @@ -44,9 +43,9 @@ struct CATransactionView: View { Text("Bridging") Spacer() VStack(alignment: .trailing) { - Text("5.00 USDC") + Text("\(presenter.bridgingAmount, specifier: "%.2f") USDC") .font(.system(.body, design: .monospaced)) - Text("from Optimism") + Text("from \(presenter.bridgingSource)") .font(.footnote) .foregroundColor(.gray) } @@ -59,7 +58,7 @@ struct CATransactionView: View { Text("App") .foregroundColor(.gray) Spacer() - Text("https://sampleapp.com") + Text(presenter.appURL) .foregroundColor(.blue) } @@ -70,7 +69,7 @@ struct CATransactionView: View { HStack(spacing: 4) { Image(systemName: "network") .foregroundColor(.blue) - Text("Arbitrum") + Text(presenter.networkName) } } } @@ -81,7 +80,7 @@ struct CATransactionView: View { Text("Estimated Fees") .foregroundColor(.gray) Spacer() - Text("$4.34") + Text("$\(presenter.estimatedFees, specifier: "%.2f")") .font(.system(.body, design: .monospaced)) } @@ -90,7 +89,7 @@ struct CATransactionView: View { Text("Bridge") .foregroundColor(.gray) Spacer() - Text("$3.00") + Text("$\(presenter.bridgeFee, specifier: "%.2f")") .font(.system(.body, design: .monospaced)) } @@ -98,7 +97,7 @@ struct CATransactionView: View { Text("Purchase") .foregroundColor(.gray) Spacer() - Text("$1.34") + Text("$\(presenter.purchaseFee, specifier: "%.2f")") .font(.system(.body, design: .monospaced)) } @@ -106,7 +105,7 @@ struct CATransactionView: View { Text("Execution") .foregroundColor(.gray) Spacer() - Text("Fast (~20 sec)") + Text(presenter.executionSpeed) .font(.system(.body, design: .monospaced)) } } @@ -125,19 +124,25 @@ struct CATransactionView: View { Button(action: { // Buy action }) { - Text("Buy") + Text("Confirm") .fontWeight(.semibold) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding() - .background(Color.blue) + .background( + LinearGradient( + gradient: Gradient(colors: [Color.blue, Color.purple]), + startPoint: .leading, + endPoint: .trailing + ) + ) .cornerRadius(12) } Button(action: { dismiss() }) { - Text("Cancel") + Text("Reject") .foregroundColor(.blue) } } @@ -145,4 +150,3 @@ struct CATransactionView: View { } } } - From a572c79602ef837f3ff1dea75253b41329aa5b53 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 18 Nov 2024 08:29:37 +0100 Subject: [PATCH 28/73] savepoint --- .../CATransactionPresenter.swift | 83 +++++++++++++++++++ .../CATransactionView.swift | 6 +- 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index fc6e59244..72d6394cc 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -1,5 +1,8 @@ import UIKit import Combine +import Web3 +import ReownWalletKit +import final class CATransactionPresenter: ObservableObject { // Published properties to be used in the view @@ -14,15 +17,74 @@ final class CATransactionPresenter: ObservableObject { @Published var purchaseFee: Double = 1.34 @Published var executionSpeed: String = "Fast (~20 sec)" + var transactions: [(transaction: Transaction, foundingFrom: FundingFrom)] = [] + + private var disposeBag = Set() init() { defer { setupInitialState() } + } func dismiss() { // Implement dismissal logic if needed } + + func approveTransactions() { + let projectId = Networking.projectId + + for transactionItem in transactions { + let transaction = transactionItem.transaction + let fundingFrom = transactionItem.foundingFrom + + let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(transaction.chainId)&projectId=\(projectId)" + + let web3 = Web3(rpcURL: rpcUrl) + + let contractAddress = try! EthereumAddress(fundingFrom.tokenContract) + + // Fetch nonce + firstly { + web3.eth.getTransactionCount(address: EthereumAddress(transaction.from)!, block: .latest) + }.then { nonce in + // Create the contract object + let contract = web3.contract(Web3.Utils.erc20ABI, at: contractAddress, abiVersion: 2)! + + // Define method parameters (e.g., transfer to and value) + let parameters = [EthereumAddress(transaction.to)!, BigUInt(fundingFrom.amount)!] as [AnyObject] + + // Prepare transaction + return try contract.method( + "transfer", + parameters: parameters, + extraData: Data(), + transactionOptions: nil + )!.createTransaction( + nonce: nonce, + gasPrice: EthereumQuantity(quantity: BigUInt(21.gwei)), + maxFeePerGas: nil, + maxPriorityFeePerGas: nil, + gasLimit: BigUInt(100000), + from: EthereumAddress(transaction.from)!, + value: EthereumQuantity(quantity: BigUInt(0)), + accessList: [:], + transactionType: .eip1559 // Adjust to match your use case + )!.sign(with: EthereumPrivateKey(transaction.from)) // Replace with your actual private key + }.then { tx in + // Send the raw transaction + web3.eth.sendRawTransaction(transaction: tx) + }.done { txHash in + print("Transaction sent successfully with hash: \(txHash.hex())") + }.catch { error in + print("Error during transaction approval: \(error.localizedDescription)") + } + } + } + + func rejectTransactions() { + + } } // MARK: - Private functions @@ -34,3 +96,24 @@ private extension CATransactionPresenter { // MARK: - SceneViewModel extension CATransactionPresenter: SceneViewModel {} + + +struct Transaction: Codable { + let chainId: String + let from: String + let to: String + let value: String + let gas: String + let gasPrice: String + let data: String + let nonce: String + let maxFeePerGas: String + let maxPriorityFeePerGas: String +} + +struct FundingFrom: Codable { + let symbol: String // e.g., "USDC" + let tokenContract: String // Token contract address + let chainId: String // Blockchain ID + let amount: String // Amount to fund +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index b9f988b37..fbd0ff622 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -122,9 +122,9 @@ struct CATransactionView: View { // Action Buttons VStack(spacing: 12) { Button(action: { - // Buy action + presenter.approveTransactions() }) { - Text("Confirm") + Text("Approve") .fontWeight(.semibold) .foregroundColor(.white) .frame(maxWidth: .infinity) @@ -140,7 +140,7 @@ struct CATransactionView: View { } Button(action: { - dismiss() + presenter.rejectTransactions() }) { Text("Reject") .foregroundColor(.blue) From 65001fdc4deba7849c5051af63b5d62d3fc84c3d Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 18 Nov 2024 15:41:39 +0100 Subject: [PATCH 29/73] update walletkit client --- .../CATransactionPresenter.swift | 137 +++++++++--------- Sources/ReownWalletKit/WalletKitClient.swift | 22 ++- .../WalletKitClientFactory.swift | 5 +- 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 72d6394cc..a25a2634c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -2,7 +2,6 @@ import UIKit import Combine import Web3 import ReownWalletKit -import final class CATransactionPresenter: ObservableObject { // Published properties to be used in the view @@ -17,7 +16,7 @@ final class CATransactionPresenter: ObservableObject { @Published var purchaseFee: Double = 1.34 @Published var executionSpeed: String = "Fast (~20 sec)" - var transactions: [(transaction: Transaction, foundingFrom: FundingFrom)] = [] +// var transactions: [(transaction: Transaction, foundingFrom: FundingFrom)] = [] private var disposeBag = Set() @@ -32,54 +31,54 @@ final class CATransactionPresenter: ObservableObject { } func approveTransactions() { - let projectId = Networking.projectId - - for transactionItem in transactions { - let transaction = transactionItem.transaction - let fundingFrom = transactionItem.foundingFrom - - let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(transaction.chainId)&projectId=\(projectId)" - - let web3 = Web3(rpcURL: rpcUrl) - - let contractAddress = try! EthereumAddress(fundingFrom.tokenContract) - - // Fetch nonce - firstly { - web3.eth.getTransactionCount(address: EthereumAddress(transaction.from)!, block: .latest) - }.then { nonce in - // Create the contract object - let contract = web3.contract(Web3.Utils.erc20ABI, at: contractAddress, abiVersion: 2)! - - // Define method parameters (e.g., transfer to and value) - let parameters = [EthereumAddress(transaction.to)!, BigUInt(fundingFrom.amount)!] as [AnyObject] - - // Prepare transaction - return try contract.method( - "transfer", - parameters: parameters, - extraData: Data(), - transactionOptions: nil - )!.createTransaction( - nonce: nonce, - gasPrice: EthereumQuantity(quantity: BigUInt(21.gwei)), - maxFeePerGas: nil, - maxPriorityFeePerGas: nil, - gasLimit: BigUInt(100000), - from: EthereumAddress(transaction.from)!, - value: EthereumQuantity(quantity: BigUInt(0)), - accessList: [:], - transactionType: .eip1559 // Adjust to match your use case - )!.sign(with: EthereumPrivateKey(transaction.from)) // Replace with your actual private key - }.then { tx in - // Send the raw transaction - web3.eth.sendRawTransaction(transaction: tx) - }.done { txHash in - print("Transaction sent successfully with hash: \(txHash.hex())") - }.catch { error in - print("Error during transaction approval: \(error.localizedDescription)") - } - } +// let projectId = Networking.projectId +// +// for transactionItem in transactions { +// let transaction = transactionItem.transaction +// let fundingFrom = transactionItem.foundingFrom +// +// let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(transaction.chainId)&projectId=\(projectId)" +// +// let web3 = Web3(rpcURL: rpcUrl) +// +// let contractAddress = try! EthereumAddress(fundingFrom.tokenContract) +// +// // Fetch nonce +// firstly { +// web3.eth.getTransactionCount(address: EthereumAddress(transaction.from)!, block: .latest) +// }.then { nonce in +// // Create the contract object +// let contract = web3.contract(Web3.Utils.erc20ABI, at: contractAddress, abiVersion: 2)! +// +// // Define method parameters (e.g., transfer to and value) +// let parameters = [EthereumAddress(transaction.to)!, BigUInt(fundingFrom.amount)!] as [AnyObject] +// +// // Prepare transaction +// return try contract.method( +// "transfer", +// parameters: parameters, +// extraData: Data(), +// transactionOptions: nil +// )!.createTransaction( +// nonce: nonce, +// gasPrice: EthereumQuantity(quantity: BigUInt(21.gwei)), +// maxFeePerGas: nil, +// maxPriorityFeePerGas: nil, +// gasLimit: BigUInt(100000), +// from: EthereumAddress(transaction.from)!, +// value: EthereumQuantity(quantity: BigUInt(0)), +// accessList: [:], +// transactionType: .eip1559 // Adjust to match your use case +// )!.sign(with: EthereumPrivateKey(transaction.from)) // Replace with your actual private key +// }.then { tx in +// // Send the raw transaction +// web3.eth.sendRawTransaction(transaction: tx) +// }.done { txHash in +// print("Transaction sent successfully with hash: \(txHash.hex())") +// }.catch { error in +// print("Error during transaction approval: \(error.localizedDescription)") +// } +// } } func rejectTransactions() { @@ -98,22 +97,22 @@ private extension CATransactionPresenter { extension CATransactionPresenter: SceneViewModel {} -struct Transaction: Codable { - let chainId: String - let from: String - let to: String - let value: String - let gas: String - let gasPrice: String - let data: String - let nonce: String - let maxFeePerGas: String - let maxPriorityFeePerGas: String -} - -struct FundingFrom: Codable { - let symbol: String // e.g., "USDC" - let tokenContract: String // Token contract address - let chainId: String // Blockchain ID - let amount: String // Amount to fund -} +//struct Transaction: Codable { +// let chainId: String +// let from: String +// let to: String +// let value: String +// let gas: String +// let gasPrice: String +// let data: String +// let nonce: String +// let maxFeePerGas: String +// let maxPriorityFeePerGas: String +//} +// +//struct FundingFrom: Codable { +// let symbol: String // e.g., "USDC" +// let tokenContract: String // Token contract address +// let chainId: String // Blockchain ID +// let amount: String // Amount to fund +//} diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index c67ffb41c..57b81a57d 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -10,6 +10,7 @@ import YttriumWrapper public class WalletKitClient { enum Errors: Error { case smartAccountNotEnabled + case chainAbstractionNotEnabled } // MARK: - Public Properties @@ -103,6 +104,7 @@ public class WalletKitClient { private let pairingClient: PairingClientProtocol private let pushClient: PushClientProtocol private let smartAccountsManager: SafesManager? + private let chainAbstractionClient: ChainAbstractionClient? private var account: Account? @@ -110,12 +112,14 @@ public class WalletKitClient { signClient: SignClientProtocol, pairingClient: PairingClientProtocol, pushClient: PushClientProtocol, - smartAccountsManager: SafesManager? + smartAccountsManager: SafesManager?, + chainAbstractionClient: ChainAbstractionClient? ) { self.signClient = signClient self.pairingClient = pairingClient self.pushClient = pushClient self.smartAccountsManager = smartAccountsManager + self.chainAbstractionClient = chainAbstractionClient } /// For a wallet to approve a session proposal. @@ -331,6 +335,22 @@ public class WalletKitClient { let signature = try await client.finalizeSignMessage(signatures, signStep3Params: signStep3Params) return signature } + + public func status(orchestrationId: String) async throws -> StatusResponseSuccess { + guard let chainAbstractionClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + + return try await chainAbstractionClient.status(orchestrationId: orchestrationId) + } + + public func route(transaction: EthTransaction) async throws -> RouteResponseSuccess { + guard let chainAbstractionClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + + return try await chainAbstractionClient.route(transaction: transaction) + } } diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index 693a70111..a4fa0c797 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -1,4 +1,5 @@ import Foundation +import YttriumWrapper struct WalletKitClientFactory { static func create( @@ -11,11 +12,13 @@ struct WalletKitClientFactory { if let pimlicoApiKey = config.pimlicoApiKey { safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey) } + let chainAbstractionClient = ChainAbstractionClient(projectId: Networking.projectId) return WalletKitClient( signClient: signClient, pairingClient: pairingClient, pushClient: pushClient, - smartAccountsManager: safesManager + smartAccountsManager: safesManager, + chainAbstractionClient: chainAbstractionClient ) } } From f9106c8fd0038db6df4867e8e612cc84ccf5a06e Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 19 Nov 2024 08:21:05 +0100 Subject: [PATCH 30/73] savepoint --- .../SessionRequestPresenter.swift | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index b10792bc4..96993402c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -50,16 +50,39 @@ final class SessionRequestPresenter: ObservableObject { @MainActor func onApprove() async throws { - do { - ActivityIndicatorManager.shared.start() - let showConnected = try await interactor.respondSessionRequest(sessionRequest: sessionRequest, importAccount: importAccount) - showConnected ? showSignedSheet.toggle() : router.dismiss() - ActivityIndicatorManager.shared.stop() - } catch { - ActivityIndicatorManager.shared.stop() - errorMessage = error.localizedDescription - showError.toggle() + + struct Tx: Codable { + let data: String + let from: String + let to: String + } + + //test CA + + if sessionRequest.method == "eth_sendTransaction" { + do { + let tx = try sessionRequest.params.get([Tx].self)[0] + let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "1000", gasPrice: "31000000000", data: tx.data, nonce: "0", maxFeePerGas: "", maxPriorityFeePerGas: "", chainId: sessionRequest.chainId.absoluteString) + let x = try await WalletKit.instance.route(transaction: transaction) + print(tx) + } catch { + print(error) + } } + print(sessionRequest.params) + + + +// do { +// ActivityIndicatorManager.shared.start() +// let showConnected = try await interactor.respondSessionRequest(sessionRequest: sessionRequest, importAccount: importAccount) +// showConnected ? showSignedSheet.toggle() : router.dismiss() +// ActivityIndicatorManager.shared.stop() +// } catch { +// ActivityIndicatorManager.shared.stop() +// errorMessage = error.localizedDescription +// showError.toggle() +// } } @MainActor From dd591fd5b67d137dff73783874eb3dcbc61a420c Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 19 Nov 2024 10:43:58 +0100 Subject: [PATCH 31/73] savepoint --- .../Wallet/SessionRequest/SessionRequestPresenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 96993402c..91c1eb7ea 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -62,7 +62,7 @@ final class SessionRequestPresenter: ObservableObject { if sessionRequest.method == "eth_sendTransaction" { do { let tx = try sessionRequest.params.get([Tx].self)[0] - let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "1000", gasPrice: "31000000000", data: tx.data, nonce: "0", maxFeePerGas: "", maxPriorityFeePerGas: "", chainId: sessionRequest.chainId.absoluteString) + let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "0", gasPrice: "0", data: tx.data, nonce: "0", maxFeePerGas: "0", maxPriorityFeePerGas: "0", chainId: sessionRequest.chainId.absoluteString) let x = try await WalletKit.instance.route(transaction: transaction) print(tx) } catch { From 214af44a88cb738d7a35adb543800c1149f71855 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 19 Nov 2024 14:03:29 +0100 Subject: [PATCH 32/73] expose estimate fees --- Example/ExampleApp.xcodeproj/project.pbxproj | 4 +++ .../ChainAbstractionService.swift | 28 +++++++++++++++++++ .../SessionRequestPresenter.swift | 21 ++------------ Sources/ReownWalletKit/WalletKitClient.swift | 8 ++++++ 4 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 Example/WalletApp/BusinessLayer/ChainAbstractionService.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 7e7624800..14f30ca05 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 84D88C7F2CE751E7003A6C16 /* CATransactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */; }; 84D88C822CE7525F003A6C16 /* CATransactionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */; }; 84D88C842CE754CC003A6C16 /* CATransactionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */; }; + 84D88C862CECC2C5003A6C16 /* ChainAbstractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */; }; 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */; }; 84E6B84A29787A8000428BAF /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B84929787A8000428BAF /* NotificationService.swift */; }; 84E6B84E29787A8000428BAF /* PNDecryptionService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 84E6B84729787A8000428BAF /* PNDecryptionService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -387,6 +388,7 @@ 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionView.swift; sourceTree = ""; }; 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionPresenter.swift; sourceTree = ""; }; 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionModule.swift; sourceTree = ""; }; + 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAbstractionService.swift; sourceTree = ""; }; 84DB38F029828A7C00BFEE37 /* WalletApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletApp.entitlements; sourceTree = ""; }; 84DB38F129828A7F00BFEE37 /* PNDecryptionService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionService.entitlements; sourceTree = ""; }; 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegisterer.swift; sourceTree = ""; }; @@ -1027,6 +1029,7 @@ children = ( A5D610CC2AB3592F00C20083 /* ListingsSertice */, 8406AEDB2CCFB424007B758A /* SmartAccountEnabler.swift */, + 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */, ); path = BusinessLayer; sourceTree = ""; @@ -2027,6 +2030,7 @@ C5B2F6F829705293000DBA0E /* SessionRequestView.swift in Sources */, C56EE28C293F5757004840D1 /* Configurator.swift in Sources */, C5FFEA782ADD896E007282A2 /* BrowserPresenter.swift in Sources */, + 84D88C862CECC2C5003A6C16 /* ChainAbstractionService.swift in Sources */, C55D3489295DD8CA0004314A /* PasteUriModule.swift in Sources */, C55D3494295DFA750004314A /* WelcomePresenter.swift in Sources */, C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift new file mode 100644 index 000000000..09d46bcbf --- /dev/null +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -0,0 +1,28 @@ +import ReownWalletKit +import Foundation + +class ChainAbstractionService { + + func handle(request: Request) async throws { + struct Tx: Codable { + let data: String + let from: String + let to: String + } + + + guard request.method == "eth_sendTransaction" else { + return + } + do { + let tx = try request.params.get([Tx].self)[0] + + let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "0", gasPrice: "0", data: tx.data, nonce: "0", maxFeePerGas: "0", maxPriorityFeePerGas: "0", chainId: request.chainId.absoluteString) + + let x = try await WalletKit.instance.route(transaction: transaction) + print(tx) + } catch { + print(error) + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 91c1eb7ea..643a866bb 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -11,7 +11,8 @@ final class SessionRequestPresenter: ObservableObject { let sessionRequest: Request let session: Session? let validationStatus: VerifyContext.ValidationStatus? - + let chainAbstractionService = ChainAbstractionService() + var message: String { guard let messages = try? sessionRequest.params.get([String].self), let firstMessage = messages.first else { @@ -51,25 +52,9 @@ final class SessionRequestPresenter: ObservableObject { @MainActor func onApprove() async throws { - struct Tx: Codable { - let data: String - let from: String - let to: String - } - //test CA - if sessionRequest.method == "eth_sendTransaction" { - do { - let tx = try sessionRequest.params.get([Tx].self)[0] - let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "0", gasPrice: "0", data: tx.data, nonce: "0", maxFeePerGas: "0", maxPriorityFeePerGas: "0", chainId: sessionRequest.chainId.absoluteString) - let x = try await WalletKit.instance.route(transaction: transaction) - print(tx) - } catch { - print(error) - } - } - print(sessionRequest.params) + chainAbstractionService.handle(request: sessionRequest) diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 57b81a57d..677fb4350 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -351,6 +351,14 @@ public class WalletKitClient { return try await chainAbstractionClient.route(transaction: transaction) } + + public func estimateFees(chainId: String) async throws -> Eip1559Estimation { + guard let chainAbstractionClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + + return try await chainAbstractionClient.estimateFees(chainId: chainId) + } } From 09c0412b170b58e443b7d64f64b151783d15b5aa Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 20 Nov 2024 10:11:36 +0100 Subject: [PATCH 33/73] savepoint --- Example/ExampleApp.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 100 +++++++++++++++++- .../ChainAbstractionService.swift | 75 ++++++++++++- 3 files changed, 171 insertions(+), 10 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 14f30ca05..ed0edab8f 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -2827,10 +2827,10 @@ }; A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/WalletConnect/Web3.swift"; + repositoryURL = "https://github.com/Boilertalk/Web3.swift"; requirement = { - kind = exactVersion; - version = 1.0.2; + kind = upToNextMajorVersion; + minimumVersion = 0.8.0; }; }; A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */ = { diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 421868a5e..e44740603 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/attaswift/BigInt.git", "state": { "branch": null, - "revision": "793a7fac0bfc318e85994bf6900652e827aef33e", - "version": "5.4.1" + "revision": "a7ee11486233ba45f5ceee0b8cb3d6629ed450ef", + "version": "5.5.0" } }, { @@ -91,6 +91,24 @@ "version": "3.1.2" } }, + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "cd142fd2f64be2100422d658e7411e39489da985", + "version": "1.2.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "671108c96644956dddcd89dd59c203dcdb36cec7", + "version": "1.1.4" + } + }, { "package": "SwiftDocCPlugin", "repositoryURL": "https://github.com/apple/swift-docc-plugin", @@ -109,6 +127,60 @@ "version": "1.0.0" } }, + { + "package": "swift-http-types", + "repositoryURL": "https://github.com/apple/swift-http-types", + "state": { + "branch": null, + "revision": "ef18d829e8b92d731ad27bb81583edd2094d1ce3", + "version": "1.3.1" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "914081701062b11e3bb9e21accc379822621995e", + "version": "2.76.1" + } + }, + { + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", + "state": { + "branch": null, + "revision": "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version": "1.24.1" + } + }, + { + "package": "swift-nio-http2", + "repositoryURL": "https://github.com/apple/swift-nio-http2.git", + "state": { + "branch": null, + "revision": "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca", + "version": "1.34.1" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "c7e95421334b1068490b5d41314a50e70bab23d1", + "version": "2.29.0" + } + }, + { + "package": "swift-nio-transport-services", + "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", + "state": { + "branch": null, + "revision": "bbd5e63cf949b7db0c9edaf7a21e141c52afe214", + "version": "1.23.0" + } + }, { "package": "swift-qrcode-generator", "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator", @@ -118,6 +190,15 @@ "version": "1.0.3" } }, + { + "package": "swift-system", + "repositoryURL": "https://github.com/apple/swift-system.git", + "state": { + "branch": null, + "revision": "c8a44d836fe7913603e246acab7c528c2e780168", + "version": "1.4.0" + } + }, { "package": "SwiftImageReadWrite", "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", @@ -174,11 +255,20 @@ }, { "package": "Web3", - "repositoryURL": "https://github.com/WalletConnect/Web3.swift", + "repositoryURL": "https://github.com/Boilertalk/Web3.swift", + "state": { + "branch": null, + "revision": "b85187ddf230a10b04fa8574c44980f3369ddff5", + "version": "0.8.8" + } + }, + { + "package": "websocket-kit", + "repositoryURL": "https://github.com/vapor/websocket-kit", "state": { "branch": null, - "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df", - "version": "1.0.2" + "revision": "4232d34efa49f633ba61afde365d3896fc7f8740", + "version": "2.15.0" } } ] diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 09d46bcbf..71b8ee4c4 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -1,8 +1,11 @@ import ReownWalletKit import Foundation +import Web3 class ChainAbstractionService { + let privateKey: EthereumPrivateKey! + func handle(request: Request) async throws { struct Tx: Codable { let data: String @@ -17,12 +20,80 @@ class ChainAbstractionService { do { let tx = try request.params.get([Tx].self)[0] - let transaction = EthTransaction(from: tx.from, to: tx.to, value: "0", gas: "0", gasPrice: "0", data: tx.data, nonce: "0", maxFeePerGas: "0", maxPriorityFeePerGas: "0", chainId: request.chainId.absoluteString) + let transaction = EthTransaction( + from: tx.from, + to: tx.to, + value: "0", + gas: "0", + gasPrice: "0", + data: tx.data, + nonce: "0", + maxFeePerGas: "0", + maxPriorityFeePerGas: "0", + chainId: request.chainId.absoluteString + ) + + let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) + + switch routeResponseSuccess { + + case .available(let routeResponseAvailable): + + + var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] + routeResponseAvailable.transactions.forEach { tx in + + let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) + let maxPriorityFeePerGas = EthereumQuantity(quantity: try! BigUInt(estimates.maxPriorityFeePerGas)) + let maxFeePerGas = EthereumQuantity(quantity: try! BigUInt(estimates.maxFeePerGas)) + + let transaction = try! EthereumTransaction( + routingTransaction: tx, + maxPriorityFeePerGas: maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas + ) + + let chain = Blockchain(tx.chainId)! + let chainId = EthereumQuantity(quantity: BigUInt(chain.reference)) + + let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) - let x = try await WalletKit.instance.route(transaction: transaction) + transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) + } + + + case .notRequired(let routeResponseNotRequired): + print(("routing not required")) + } print(tx) } catch { print(error) } } } + + + +extension EthereumTransaction { + init(routingTransaction: RoutingTransaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { + + self.init( + nonce: try EthereumQuantity(routingTransaction.nonce), + gasPrice: nil, // Not needed for EIP1559 + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, + gasLimit: try EthereumQuantity(routingTransaction.gas), + from: try EthereumAddress(hex: routingTransaction.from, eip55: false), + to: try EthereumAddress(hex: routingTransaction.to, eip55: false), + value: try EthereumQuantity(routingTransaction.value), + data: EthereumData(Array(hex: routingTransaction.data)), + accessList: [:], // Empty access list for basic transactions + transactionType: .eip1559 // Specify EIP1559 transaction type + ) + } + + enum InitializationError: Error { + case invalidAddress(String) + case invalidValue(String) + } +} From c17ab96e7f20ff653f2fe907601d78ae9f6992df Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 20 Nov 2024 10:31:55 +0100 Subject: [PATCH 34/73] update ChainAbstractionService --- .../ChainAbstractionService.swift | 83 +++++++++++++++---- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 71b8ee4c4..ad9da22d9 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -3,7 +3,11 @@ import Foundation import Web3 class ChainAbstractionService { - + enum NetworkError: Error { + case invalidURL + case invalidResponse + case invalidData + } let privateKey: EthereumPrivateKey! func handle(request: Request) async throws { @@ -39,28 +43,33 @@ class ChainAbstractionService { case .available(let routeResponseAvailable): + var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] - var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] - routeResponseAvailable.transactions.forEach { tx in - - let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) - let maxPriorityFeePerGas = EthereumQuantity(quantity: try! BigUInt(estimates.maxPriorityFeePerGas)) - let maxFeePerGas = EthereumQuantity(quantity: try! BigUInt(estimates.maxFeePerGas)) + for tx in routeResponseAvailable.transactions { + do { + let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) + let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas)) + let maxFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxFeePerGas)) - let transaction = try! EthereumTransaction( - routingTransaction: tx, - maxPriorityFeePerGas: maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas - ) + let transaction = try EthereumTransaction( + routingTransaction: tx, + maxPriorityFeePerGas: maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas + ) - let chain = Blockchain(tx.chainId)! - let chainId = EthereumQuantity(quantity: BigUInt(chain.reference)) + let chain = Blockchain(tx.chainId)! + let chainId = EthereumQuantity(quantity: try BigUInt(chain.reference)) - let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) + let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) - transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) + transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) + } catch { + print("Error processing transaction: \(error)") + } } + try await broadcastTransactions(transactions: transactions) + case .notRequired(let routeResponseNotRequired): print(("routing not required")) @@ -70,6 +79,48 @@ class ChainAbstractionService { print(error) } } + + private func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws { + for transaction in transactions { + let chainId = transaction.chainId + let projectId = Networking.projectId + let rpcUrl = "rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + + let rawTransaction = try transaction.transaction.rawTransaction() + let rpcRequest = RPCRequest(method: "eth_sendRawTransaction", params: [rawTransaction]) + + // Create URL and request + guard let url = URL(string: "https://" + rpcUrl) else { + throw NetworkError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Convert RPC request to JSON data + let jsonData = try JSONEncoder().encode(rpcRequest) + request.httpBody = jsonData + + do { + // Use async/await URLSession + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + + // Parse response + let responseJSON = try JSONSerialization.jsonObject(with: data) + print("Transaction broadcast success: \(responseJSON)") + + } catch { + print("Error broadcasting transaction: \(error)") + throw error + } + } + } } From 464eb41fae0cfc82051c3e5167c95223cbebc260 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 20 Nov 2024 13:43:29 +0100 Subject: [PATCH 35/73] update ca --- Example/Shared/Signer/ETHSigner.swift | 2 +- .../ConfigurationService.swift | 8 +++---- .../ChainAbstractionService.swift | 23 +++++++++++++++---- .../Extensions/EthereumTransaction.swift | 1 - .../SessionRequestPresenter.swift | 7 ++++-- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Example/Shared/Signer/ETHSigner.swift b/Example/Shared/Signer/ETHSigner.swift index 0d940c821..a3dbf71b1 100644 --- a/Example/Shared/Signer/ETHSigner.swift +++ b/Example/Shared/Signer/ETHSigner.swift @@ -65,7 +65,7 @@ struct ETHSigner { func sendTransaction(_ params: AnyCodable) throws -> AnyCodable { let params = try params.get([EthereumTransaction].self) var transaction = params[0] - transaction.gas = EthereumQuantity(quantity: BigUInt("1234")) +// transaction.gas = EthereumQuantity(quantity: BigUInt("1234")) transaction.nonce = EthereumQuantity(quantity: BigUInt("0")) transaction.gasPrice = EthereumQuantity(quantity: BigUInt(0)) print(transaction.description) diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index 5772e3e0b..cb222703f 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -14,7 +14,7 @@ final class ConfigurationService { projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory() ) - Networking.instance.setLogging(level: .debug) + Networking.instance.setLogging(level: .off) let metadata = AppMetadata( name: "Example Wallet", @@ -31,9 +31,9 @@ final class ConfigurationService { crypto: DefaultCryptoProvider() ) - Notify.instance.setLogging(level: .debug) - Sign.instance.setLogging(level: .debug) - Events.instance.setLogging(level: .debug) + Notify.instance.setLogging(level: .off) + Sign.instance.setLogging(level: .off) + Events.instance.setLogging(level: .off) if let clientId = try? Networking.interactor.getClientId() { LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index ad9da22d9..f0c062ce6 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -8,7 +8,12 @@ class ChainAbstractionService { case invalidResponse case invalidData } - let privateKey: EthereumPrivateKey! + + let privateKey: EthereumPrivateKey + + init(privateKey: EthereumPrivateKey) { + self.privateKey = privateKey + } func handle(request: Request) async throws { struct Tx: Codable { @@ -58,10 +63,12 @@ class ChainAbstractionService { ) let chain = Blockchain(tx.chainId)! - let chainId = EthereumQuantity(quantity: try BigUInt(chain.reference)) + let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) + print(chainId.quantity) let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) + print(signedTransaction.value) transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) } catch { print("Error processing transaction: \(error)") @@ -129,14 +136,14 @@ extension EthereumTransaction { init(routingTransaction: RoutingTransaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { self.init( - nonce: try EthereumQuantity(routingTransaction.nonce), + nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 16)!), gasPrice: nil, // Not needed for EIP1559 maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: try EthereumQuantity(routingTransaction.gas), + gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 16)!), from: try EthereumAddress(hex: routingTransaction.from, eip55: false), to: try EthereumAddress(hex: routingTransaction.to, eip55: false), - value: try EthereumQuantity(routingTransaction.value), + value: EthereumQuantity(quantity: 0.gwei), data: EthereumData(Array(hex: routingTransaction.data)), accessList: [:], // Empty access list for basic transactions transactionType: .eip1559 // Specify EIP1559 transaction type @@ -148,3 +155,9 @@ extension EthereumTransaction { case invalidValue(String) } } + +fileprivate extension String { + func stripHexPrefix() -> String { + return hasPrefix("0x") ? String(dropFirst(2)) : self + } +} diff --git a/Example/WalletApp/Common/Extensions/EthereumTransaction.swift b/Example/WalletApp/Common/Extensions/EthereumTransaction.swift index ac167fc9f..fe980847d 100644 --- a/Example/WalletApp/Common/Extensions/EthereumTransaction.swift +++ b/Example/WalletApp/Common/Extensions/EthereumTransaction.swift @@ -8,7 +8,6 @@ extension EthereumTransaction { to: \(String(describing: to!.hex(eip55: true))), value: \(String(describing: value!.hex())), gasPrice: \(String(describing: gasPrice?.hex())), - gas: \(String(describing: gas?.hex())), data: \(data.hex()), nonce: \(String(describing: nonce?.hex())) """ diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 643a866bb..96e96cece 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -1,5 +1,6 @@ import UIKit import Combine +import Web3 import ReownWalletKit @@ -11,7 +12,7 @@ final class SessionRequestPresenter: ObservableObject { let sessionRequest: Request let session: Session? let validationStatus: VerifyContext.ValidationStatus? - let chainAbstractionService = ChainAbstractionService() + let chainAbstractionService: ChainAbstractionService! var message: String { guard let messages = try? sessionRequest.params.get([String].self), @@ -47,6 +48,8 @@ final class SessionRequestPresenter: ObservableObject { self.session = interactor.getSession(topic: sessionRequest.topic) self.importAccount = importAccount self.validationStatus = context?.validation + let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) + self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey) } @MainActor @@ -54,7 +57,7 @@ final class SessionRequestPresenter: ObservableObject { //test CA - chainAbstractionService.handle(request: sessionRequest) + try await chainAbstractionService.handle(request: sessionRequest) From 5fde3b4af3bb645ffdceb4e1d98bc3fafdecf03e Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 20 Nov 2024 14:57:17 +0100 Subject: [PATCH 36/73] fix fees calculation --- .../WalletApp/BusinessLayer/ChainAbstractionService.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index f0c062ce6..706152197 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -53,9 +53,11 @@ class ChainAbstractionService { for tx in routeResponseAvailable.transactions { do { let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) - let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas)) - let maxFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxFeePerGas)) + let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + print(maxFeePerGas) + print(maxPriorityFeePerGas) let transaction = try EthereumTransaction( routingTransaction: tx, maxPriorityFeePerGas: maxPriorityFeePerGas, From 4405eb4df8037f8bc9939b82fe2dc0a332bfc20f Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 21 Nov 2024 09:09:11 +0100 Subject: [PATCH 37/73] add ca button in settings --- .../Wallet/Settings/SettingsPresenter.swift | 4 ++++ .../Wallet/Settings/SettingsView.swift | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 0ce87f708..1ec422b31 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -41,6 +41,10 @@ final class SettingsPresenter: ObservableObject { SmartAccountEnabler.shared.isSmartAccountEnabled = enable } + func enableChainAbstraction(_ enable: Bool) { + + } + private func getSmartAccountSafe() async throws -> String { try await WalletKit.instance.getSmartAccount(ownerAccount: importAccount.account).absoluteString } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 8ddbaa68e..50751a079 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -6,7 +6,9 @@ import ReownAppKitUI struct SettingsView: View { @EnvironmentObject var viewModel: SettingsPresenter @State private var copyAlert: Bool = false - @State private var isSmartAccountEnabled: Bool = false // State for the toggle switch + @State private var isSmartAccountEnabled: Bool = false + @State private var isChainAbstractionEnabled: Bool = false + var body: some View { ScrollView { @@ -19,7 +21,6 @@ struct SettingsView: View { row(title: "Smart Account Safe", subtitle: viewModel.smartAccountSafe) row(title: "Private key", subtitle: viewModel.privateKey) - // New Smart Account Toggle Row HStack { Text("Smart Account") .foregroundColor(.Foreground100) @@ -36,6 +37,23 @@ struct SettingsView: View { .padding(.horizontal, 12) .padding(.vertical, 16) .background(Color.Foreground100.opacity(0.05).cornerRadius(12)) + + HStack { + Text("Chain Abstraction") + .foregroundColor(.Foreground100) + .font(.paragraph700) + + Spacer() + + Toggle("", isOn: $isChainAbstractionEnabled) + .onChange(of: isChainAbstractionEnabled) { newValue in + viewModel.enableChainAbstraction(newValue) + } + .labelsHidden() + } + .padding(.horizontal, 12) + .padding(.vertical, 16) + .background(Color.Foreground100.opacity(0.05).cornerRadius(12)) } .padding(.horizontal, 20) From 7d26cd09ef155be6fdbfb6f41f00a344bd68734e Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 21 Nov 2024 09:11:35 +0100 Subject: [PATCH 38/73] add walletkit enabler --- Example/ExampleApp.xcodeproj/project.pbxproj | 8 +++---- ...ntEnabler.swift => WalletKitEnabler.swift} | 23 +++++++++++++++---- .../SessionProposalInteractor.swift | 4 ++-- .../Wallet/Settings/SettingsPresenter.swift | 4 ++-- .../Wallet/Settings/SettingsView.swift | 2 +- 5 files changed, 28 insertions(+), 13 deletions(-) rename Example/WalletApp/BusinessLayer/{SmartAccountEnabler.swift => WalletKitEnabler.swift} (64%) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index ed0edab8f..3b8ac6dbe 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767DC83428997F8E00080FA9 /* EthSendTransaction.swift */; }; 840507E32C8BAC7C00148A9B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 840507E22C8BAC7C00148A9B /* Preview Assets.xcassets */; }; - 8406AEDC2CCFB424007B758A /* SmartAccountEnabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8406AEDB2CCFB424007B758A /* SmartAccountEnabler.swift */; }; + 8406AEDC2CCFB424007B758A /* WalletKitEnabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8406AEDB2CCFB424007B758A /* WalletKitEnabler.swift */; }; 8421447B2C80A2B8004FF494 /* ReownAppKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8421447A2C80A2B8004FF494 /* ReownAppKit */; }; 8421447D2C80A544004FF494 /* ReownAppKitUI in Frameworks */ = {isa = PBXBuildFile; productRef = 8421447C2C80A544004FF494 /* ReownAppKitUI */; }; 8421447F2C81863A004FF494 /* ReownWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8421447E2C81863A004FF494 /* ReownWalletKit */; }; @@ -328,7 +328,7 @@ 764E1D5626F8DB6000A1FB15 /* WalletConnectSwiftV2 */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WalletConnectSwiftV2; path = ..; sourceTree = ""; }; 767DC83428997F8E00080FA9 /* EthSendTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthSendTransaction.swift; sourceTree = ""; }; 840507E22C8BAC7C00148A9B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 8406AEDB2CCFB424007B758A /* SmartAccountEnabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartAccountEnabler.swift; sourceTree = ""; }; + 8406AEDB2CCFB424007B758A /* WalletKitEnabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletKitEnabler.swift; sourceTree = ""; }; 84310D04298BC980000C15B6 /* MainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainInteractor.swift; sourceTree = ""; }; 8439CB88293F658E00F2F2E2 /* PushMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessage.swift; sourceTree = ""; }; 8445118F2C8B689D00A6A86C /* AppKitLab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppKitLab.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1028,7 +1028,7 @@ isa = PBXGroup; children = ( A5D610CC2AB3592F00C20083 /* ListingsSertice */, - 8406AEDB2CCFB424007B758A /* SmartAccountEnabler.swift */, + 8406AEDB2CCFB424007B758A /* WalletKitEnabler.swift */, 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */, ); path = BusinessLayer; @@ -1971,7 +1971,7 @@ 847BD1D92989492500076C90 /* MainPresenter.swift in Sources */, C5FFEA842ADDAD6D007282A2 /* SafariViewController.swift in Sources */, C55D3497295DFA750004314A /* WelcomeView.swift in Sources */, - 8406AEDC2CCFB424007B758A /* SmartAccountEnabler.swift in Sources */, + 8406AEDC2CCFB424007B758A /* WalletKitEnabler.swift in Sources */, A57879722A4F225E00F8D10B /* ImportAccount.swift in Sources */, C5B2F71029705827000DBA0E /* EthereumTransaction.swift in Sources */, A50D53C32ABA055700A4FD8B /* NotifyPreferencesRouter.swift in Sources */, diff --git a/Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift similarity index 64% rename from Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift rename to Example/WalletApp/BusinessLayer/WalletKitEnabler.swift index 8bc1495f6..e64c7c027 100644 --- a/Example/WalletApp/BusinessLayer/SmartAccountEnabler.swift +++ b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift @@ -1,19 +1,20 @@ import Foundation import ReownWalletKit -class SmartAccountEnabler { +class WalletKitEnabler { enum Errors: Error { case smartAccountNotEnabled } // Singleton instance - static let shared = SmartAccountEnabler() + static let shared = WalletKitEnabler() - // Use a private queue for thread-safe access to the isSmartAccountEnabled property + // 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 property + // A private backing variable for the thread-safe properties private var _isSmartAccountEnabled: Bool = false + private var _isChainAbstractionEnabled: Bool = false // Thread-safe access for setting and getting isSmartAccountEnabled var isSmartAccountEnabled: Bool { @@ -29,6 +30,20 @@ class SmartAccountEnabler { } } + // Thread-safe access for setting and getting isChainAbstractionEnabled + var isChainAbstractionEnabled: Bool { + get { + return queue.sync { + _isChainAbstractionEnabled + } + } + set { + queue.async(flags: .barrier) { + self._isChainAbstractionEnabled = newValue + } + } + } + // Private initializer to ensure it cannot be instantiated externally private init() {} diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift index ac1581dcf..2dc731a95 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift @@ -16,10 +16,10 @@ final class SessionProposalInteractor { var supportedAccounts: [Account] var sessionProperties = [String: String]() - if SmartAccountEnabler.shared.isSmartAccountEnabled { + if WalletKitEnabler.shared.isSmartAccountEnabled { let sepolia = Blockchain("eip155:11155111")! let sepoliaOwnerAccount = Account(blockchain: sepolia, address: EOAAccount.address)! - let smartAccountAddresses = try await SmartAccountEnabler.shared.getSmartAccountsAddresses(ownerAccount: sepoliaOwnerAccount) + let smartAccountAddresses = try await WalletKitEnabler.shared.getSmartAccountsAddresses(ownerAccount: sepoliaOwnerAccount) supportedAccounts = smartAccountAddresses.map { Account(blockchain: sepolia, address: $0)! } sessionProperties = getSessionProperties(addresses: smartAccountAddresses) } else { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift index 1ec422b31..aa04620b3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsPresenter.swift @@ -38,11 +38,11 @@ final class SettingsPresenter: ObservableObject { } func enableSmartAccount(_ enable: Bool) { - SmartAccountEnabler.shared.isSmartAccountEnabled = enable + WalletKitEnabler.shared.isSmartAccountEnabled = enable } func enableChainAbstraction(_ enable: Bool) { - + WalletKitEnabler.shared.isChainAbstractionEnabled = enable } private func getSmartAccountSafe() async throws -> String { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 50751a079..6555eccf2 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -113,7 +113,7 @@ struct SettingsView: View { } .onAppear { viewModel.objectWillChange.send() - isSmartAccountEnabled = SmartAccountEnabler.shared.isSmartAccountEnabled + isSmartAccountEnabled = WalletKitEnabler.shared.isSmartAccountEnabled } } From 756166e057a7656a86b9e5c7d584ae672f86f4f4 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 21 Nov 2024 09:43:50 +0100 Subject: [PATCH 39/73] update wallet routing --- .../PresentationLayer/Wallet/Main/MainPresenter.swift | 6 +++++- .../PresentationLayer/Wallet/Main/MainRouter.swift | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index dfc484842..32ab6ef11 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -71,7 +71,11 @@ extension MainPresenter { AlertPresenter.present(message: "No common chains", type: .error) return } - router.present(request: result.request, importAccount: importAccount, context: result.context) + if WalletKitEnabler.shared.isChainAbstractionEnabled { + router.presentCATransaction() + } else { + router.present(request: result.request, importAccount: importAccount, context: result.context) + } } .store(in: &disposeBag) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index f1ea36065..bed6e1e7a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -42,6 +42,12 @@ final class MainRouter { .presentFullScreen(from: viewController, transparentBackground: true) } + func presentCATransaction(request: AuthenticationRequest, importAccount: ImportAccount, context: VerifyContext?) { + CATransactionModule.create(app: app) + .wrapToNavigationController() + .present(from: viewController) + } + func dismiss() { viewController.dismiss() } From 8bac85c2b95677f99427d8e4d0e9630ef3c0a56a Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 21 Nov 2024 14:52:53 +0100 Subject: [PATCH 40/73] update app flow to present ca screen --- .../ChainAbstractionService.swift | 182 +++++++++++------- .../CATransactionModule.swift | 10 +- .../CATransactionPresenter.swift | 94 +++------ .../Wallet/Main/MainPresenter.swift | 69 ++++++- .../Wallet/Main/MainRouter.swift | 4 +- .../SessionRequestPresenter.swift | 30 +-- .../Wallet/Wallet/WalletPresenter.swift | 5 - .../Wallet/Wallet/WalletRouter.swift | 6 - .../Wallet/Wallet/WalletView.swift | 10 - 9 files changed, 212 insertions(+), 198 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 706152197..8c1df6672 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -10,86 +10,122 @@ class ChainAbstractionService { } let privateKey: EthereumPrivateKey + private let routeResponseAvailable: RouteResponseAvailable - init(privateKey: EthereumPrivateKey) { + init(privateKey: EthereumPrivateKey, routeResponseAvailable: RouteResponseAvailable) { self.privateKey = privateKey + self.routeResponseAvailable = routeResponseAvailable } - func handle(request: Request) async throws { - struct Tx: Codable { - let data: String - let from: String - let to: String - } - - - guard request.method == "eth_sendTransaction" else { - return - } - do { - let tx = try request.params.get([Tx].self)[0] - - let transaction = EthTransaction( - from: tx.from, - to: tx.to, - value: "0", - gas: "0", - gasPrice: "0", - data: tx.data, - nonce: "0", - maxFeePerGas: "0", - maxPriorityFeePerGas: "0", - chainId: request.chainId.absoluteString - ) - - let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) - - switch routeResponseSuccess { - - case .available(let routeResponseAvailable): - - var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] - - for tx in routeResponseAvailable.transactions { - do { - let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) - let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) - let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) - - print(maxFeePerGas) - print(maxPriorityFeePerGas) - let transaction = try EthereumTransaction( - routingTransaction: tx, - maxPriorityFeePerGas: maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas - ) - - let chain = Blockchain(tx.chainId)! - let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) - - print(chainId.quantity) - let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) - - print(signedTransaction.value) - transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) - } catch { - print("Error processing transaction: \(error)") - } - } - - try await broadcastTransactions(transactions: transactions) - - - case .notRequired(let routeResponseNotRequired): - print(("routing not required")) +// func handle(request: Request) async throws { +// struct Tx: Codable { +// let data: String +// let from: String +// let to: String +// } +// +// +// guard request.method == "eth_sendTransaction" else { +// return +// } +// do { +// let tx = try request.params.get([Tx].self)[0] +// +// let transaction = EthTransaction( +// from: tx.from, +// to: tx.to, +// value: "0", +// gas: "0", +// gasPrice: "0", +// data: tx.data, +// nonce: "0", +// maxFeePerGas: "0", +// maxPriorityFeePerGas: "0", +// chainId: request.chainId.absoluteString +// ) +// +// let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) +// +// switch routeResponseSuccess { +// +// case .available(let routeResponseAvailable): +// +// var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] +// +// for tx in routeResponseAvailable.transactions { +// do { +// let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) +// let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) +// let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) +// +// print(maxFeePerGas) +// print(maxPriorityFeePerGas) +// let transaction = try EthereumTransaction( +// routingTransaction: tx, +// maxPriorityFeePerGas: maxPriorityFeePerGas, +// maxFeePerGas: maxFeePerGas +// ) +// +// let chain = Blockchain(tx.chainId)! +// let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) +// +// print(chainId.quantity) +// let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) +// +// print(signedTransaction.value) +// transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) +// } catch { +// print("Error processing transaction: \(error)") +// } +// } +// +// try await broadcastTransactions(transactions: transactions) +// +// +// case .notRequired(let routeResponseNotRequired): +// print(("routing not required")) +// } +// print(tx) +// } catch { +// print(error) +// } +// } + + + + func signTransactions() async throws -> [(transaction: EthereumSignedTransaction, chainId: String)] { + var signedTransactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] + + for tx in routeResponseAvailable.transactions { + do { + let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + + print(maxFeePerGas) + print(maxPriorityFeePerGas) + let transaction = try EthereumTransaction( + routingTransaction: tx, + maxPriorityFeePerGas: maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas + ) + + let chain = Blockchain(tx.chainId)! + let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) + + print(chainId.quantity) + let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) + + print(signedTransaction.value) + signedTransactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) + } catch { + print("Error processing transaction: \(error)") } - print(tx) - } catch { - print(error) } + return signedTransactions } - private func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws { + func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws { for transaction in transactions { let chainId = transaction.chainId let projectId = Networking.projectId @@ -130,6 +166,8 @@ class ChainAbstractionService { } } } + + } diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift index 635938f1a..c17754fcb 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift @@ -1,16 +1,18 @@ import Foundation import UIKit +import ReownWalletKit final class CATransactionModule { @discardableResult static func create( - app: Application + app: Application, + sessionRequest: Request, + importAccount: ImportAccount, + routeResponseAvailable: RouteResponseAvailable ) -> UIViewController { - let presenter = CATransactionPresenter() + let presenter = CATransactionPresenter(sessionRequest: sessionRequest, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable) let view = CATransactionView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) - - return viewController } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index a25a2634c..c47c902ce 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -16,14 +16,26 @@ final class CATransactionPresenter: ObservableObject { @Published var purchaseFee: Double = 1.34 @Published var executionSpeed: String = "Fast (~20 sec)" -// var transactions: [(transaction: Transaction, foundingFrom: FundingFrom)] = [] + private let sessionRequest: Request + private let routeResponseAvailable: RouteResponseAvailable + let chainAbstractionService: ChainAbstractionService! private var disposeBag = Set() - init() { - defer { setupInitialState() } + init( + sessionRequest: Request, + importAccount: ImportAccount, + routeResponseAvailable: RouteResponseAvailable + ) { + self.sessionRequest = sessionRequest + self.routeResponseAvailable = routeResponseAvailable + let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) + self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) + + // Any additional setup for the parameters + setupInitialState() } func dismiss() { @@ -31,54 +43,14 @@ final class CATransactionPresenter: ObservableObject { } func approveTransactions() { -// let projectId = Networking.projectId -// -// for transactionItem in transactions { -// let transaction = transactionItem.transaction -// let fundingFrom = transactionItem.foundingFrom -// -// let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(transaction.chainId)&projectId=\(projectId)" -// -// let web3 = Web3(rpcURL: rpcUrl) -// -// let contractAddress = try! EthereumAddress(fundingFrom.tokenContract) -// -// // Fetch nonce -// firstly { -// web3.eth.getTransactionCount(address: EthereumAddress(transaction.from)!, block: .latest) -// }.then { nonce in -// // Create the contract object -// let contract = web3.contract(Web3.Utils.erc20ABI, at: contractAddress, abiVersion: 2)! -// -// // Define method parameters (e.g., transfer to and value) -// let parameters = [EthereumAddress(transaction.to)!, BigUInt(fundingFrom.amount)!] as [AnyObject] -// -// // Prepare transaction -// return try contract.method( -// "transfer", -// parameters: parameters, -// extraData: Data(), -// transactionOptions: nil -// )!.createTransaction( -// nonce: nonce, -// gasPrice: EthereumQuantity(quantity: BigUInt(21.gwei)), -// maxFeePerGas: nil, -// maxPriorityFeePerGas: nil, -// gasLimit: BigUInt(100000), -// from: EthereumAddress(transaction.from)!, -// value: EthereumQuantity(quantity: BigUInt(0)), -// accessList: [:], -// transactionType: .eip1559 // Adjust to match your use case -// )!.sign(with: EthereumPrivateKey(transaction.from)) // Replace with your actual private key -// }.then { tx in -// // Send the raw transaction -// web3.eth.sendRawTransaction(transaction: tx) -// }.done { txHash in -// print("Transaction sent successfully with hash: \(txHash.hex())") -// }.catch { error in -// print("Error during transaction approval: \(error.localizedDescription)") -// } -// } + Task { + do { + let signedTransactions = try await chainAbstractionService.signTransactions() + try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) + } catch { + AlertPresenter.present(message: error.localizedDescription, type: .error) + } + } } func rejectTransactions() { @@ -96,23 +68,3 @@ private extension CATransactionPresenter { // MARK: - SceneViewModel extension CATransactionPresenter: SceneViewModel {} - -//struct Transaction: Codable { -// let chainId: String -// let from: String -// let to: String -// let value: String -// let gas: String -// let gasPrice: String -// let data: String -// let nonce: String -// let maxFeePerGas: String -// let maxPriorityFeePerGas: String -//} -// -//struct FundingFrom: Codable { -// let symbol: String // e.g., "USDC" -// let tokenContract: String // Token contract address -// let chainId: String // Blockchain ID -// let amount: String // Amount to fund -//} diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 32ab6ef11..05227c0af 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -2,6 +2,7 @@ import UIKit import Combine import SwiftUI import WalletConnectUtils +import ReownWalletKit final class MainPresenter { private let interactor: MainInteractor @@ -47,7 +48,7 @@ extension MainPresenter { router.present(proposal: session.proposal, importAccount: importAccount, context: session.context) } .store(in: &disposeBag) - + interactor.sessionRequestPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] (request, context) in @@ -56,10 +57,16 @@ extension MainPresenter { return } router.dismiss() - router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + if WalletKitEnabler.shared.isChainAbstractionEnabled && request.method == "eth_sendTransaction" { + Task(priority: .background) { + try await tryRoutCATransaction(request: request, context: context) + } + } else { + router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + } }.store(in: &disposeBag) - + interactor.authenticateRequestPublisher .receive(on: DispatchQueue.main) .sink { [unowned self] result in @@ -71,12 +78,58 @@ extension MainPresenter { AlertPresenter.present(message: "No common chains", type: .error) return } - if WalletKitEnabler.shared.isChainAbstractionEnabled { - router.presentCATransaction() - } else { - router.present(request: result.request, importAccount: importAccount, context: result.context) - } + + router.present(request: result.request, importAccount: importAccount, context: result.context) } .store(in: &disposeBag) } + + private func tryRoutCATransaction(request: Request, context: VerifyContext?) async throws { + struct Tx: Codable { + let data: String + let from: String + let to: String + } + + guard request.method == "eth_sendTransaction" else { + return + } + do { + let tx = try request.params.get([Tx].self)[0] + + let transaction = EthTransaction( + from: tx.from, + to: tx.to, + value: "0", + gas: "0", + gasPrice: "0", + data: tx.data, + nonce: "0", + maxFeePerGas: "0", + maxPriorityFeePerGas: "0", + chainId: request.chainId.absoluteString + ) + + + + let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) + + await MainActor.run { + switch routeResponseSuccess { + case .available(let routeResponseAvailable): + + router.presentCATransaction(sessionRequest: request, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable, context: context) + + case .notRequired(let routeResponseNotRequired): + AlertPresenter.present(message: "Routing not required", type: .success) + router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + } + } + } catch { + await MainActor.run { + AlertPresenter.present(message: "CA error: \(error.localizedDescription)", type: .error) + router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + } + } + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index bed6e1e7a..1045c2f98 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -42,8 +42,8 @@ final class MainRouter { .presentFullScreen(from: viewController, transparentBackground: true) } - func presentCATransaction(request: AuthenticationRequest, importAccount: ImportAccount, context: VerifyContext?) { - CATransactionModule.create(app: app) + func presentCATransaction(sessionRequest: Request, importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable, context: VerifyContext?) { + CATransactionModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable) .wrapToNavigationController() .present(from: viewController) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift index 96e96cece..950e842ee 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionRequest/SessionRequestPresenter.swift @@ -12,7 +12,6 @@ final class SessionRequestPresenter: ObservableObject { let sessionRequest: Request let session: Session? let validationStatus: VerifyContext.ValidationStatus? - let chainAbstractionService: ChainAbstractionService! var message: String { guard let messages = try? sessionRequest.params.get([String].self), @@ -48,29 +47,20 @@ final class SessionRequestPresenter: ObservableObject { self.session = interactor.getSession(topic: sessionRequest.topic) self.importAccount = importAccount self.validationStatus = context?.validation - let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) - self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey) } @MainActor func onApprove() async throws { - - //test CA - - try await chainAbstractionService.handle(request: sessionRequest) - - - -// do { -// ActivityIndicatorManager.shared.start() -// let showConnected = try await interactor.respondSessionRequest(sessionRequest: sessionRequest, importAccount: importAccount) -// showConnected ? showSignedSheet.toggle() : router.dismiss() -// ActivityIndicatorManager.shared.stop() -// } catch { -// ActivityIndicatorManager.shared.stop() -// errorMessage = error.localizedDescription -// showError.toggle() -// } + do { + ActivityIndicatorManager.shared.start() + let showConnected = try await interactor.respondSessionRequest(sessionRequest: sessionRequest, importAccount: importAccount) + showConnected ? showSignedSheet.toggle() : router.dismiss() + ActivityIndicatorManager.shared.stop() + } catch { + ActivityIndicatorManager.shared.stop() + errorMessage = error.localizedDescription + showError.toggle() + } } @MainActor diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index ae033343b..13575c232 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -89,11 +89,6 @@ final class WalletPresenter: ObservableObject { self?.router.dismiss() } } - - func onTest() { - router.presentTest() - } - func removeSession(at indexSet: IndexSet) async { if let index = indexSet.first { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 2252de09a..8109c488f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -37,12 +37,6 @@ final class WalletRouter { .present(from: viewController) } - func presentTest() { - CATransactionModule.create(app: app) - .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 fb38dca78..96e151f0c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -73,16 +73,6 @@ struct WalletView: View { HStack(spacing: 20) { Spacer() - Button { - presenter.onTest() - } label: { - Label("Test Transaction", systemImage: "creditcard") - .labelStyle(.iconOnly) // Only show the icon - .frame(width: 56, height: 56) - .background(Circle().fill(Color(.systemBlue))) - } - .shadow(color: .black.opacity(0.25), radius: 8, y: 4) - Button { presenter.onPasteUri() } label: { From 1c629d7e7fe58015310d4417db7b829c6deb0bd0 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 22 Nov 2024 09:29:05 +0100 Subject: [PATCH 41/73] update ca view --- Example/ExampleApp.xcodeproj/project.pbxproj | 4 ++ .../BusinessLayer/WalletKitEnabler.swift | 2 +- .../CATransactionModule.swift | 6 ++- .../CATransactionPresenter.swift | 39 +++++++++++++++++-- .../CATransactionRouter.swift | 18 +++++++++ .../CATransactionView.swift | 37 +++++++----------- .../Wallet/Main/MainRouter.swift | 1 - .../Wallet/Settings/SettingsView.swift | 4 +- 8 files changed, 80 insertions(+), 31 deletions(-) create mode 100644 Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionRouter.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 3b8ac6dbe..a51bbbe9b 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 84D88C822CE7525F003A6C16 /* CATransactionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */; }; 84D88C842CE754CC003A6C16 /* CATransactionModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */; }; 84D88C862CECC2C5003A6C16 /* ChainAbstractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */; }; + 84D88C8A2CF075E9003A6C16 /* CATransactionRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D88C892CF075E9003A6C16 /* CATransactionRouter.swift */; }; 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */; }; 84E6B84A29787A8000428BAF /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B84929787A8000428BAF /* NotificationService.swift */; }; 84E6B84E29787A8000428BAF /* PNDecryptionService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 84E6B84729787A8000428BAF /* PNDecryptionService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -389,6 +390,7 @@ 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionPresenter.swift; sourceTree = ""; }; 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionModule.swift; sourceTree = ""; }; 84D88C852CECC2C5003A6C16 /* ChainAbstractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAbstractionService.swift; sourceTree = ""; }; + 84D88C892CF075E9003A6C16 /* CATransactionRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransactionRouter.swift; sourceTree = ""; }; 84DB38F029828A7C00BFEE37 /* WalletApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletApp.entitlements; sourceTree = ""; }; 84DB38F129828A7F00BFEE37 /* PNDecryptionService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionService.entitlements; sourceTree = ""; }; 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRegisterer.swift; sourceTree = ""; }; @@ -889,6 +891,7 @@ 84D88C7E2CE751E7003A6C16 /* CATransactionView.swift */, 84D88C812CE7525F003A6C16 /* CATransactionPresenter.swift */, 84D88C832CE754CC003A6C16 /* CATransactionModule.swift */, + 84D88C892CF075E9003A6C16 /* CATransactionRouter.swift */, ); path = CATransactionModal; sourceTree = ""; @@ -2068,6 +2071,7 @@ A5D610D22AB35B1100C20083 /* Listings.swift in Sources */, A5B4F7C82ABB21190099AF7C /* CacheAsyncImage.swift in Sources */, 847BD1E8298A806800076C90 /* NotificationsView.swift in Sources */, + 84D88C8A2CF075E9003A6C16 /* CATransactionRouter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift index e64c7c027..b90e591bb 100644 --- a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift +++ b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift @@ -14,7 +14,7 @@ class WalletKitEnabler { // A private backing variable for the thread-safe properties private var _isSmartAccountEnabled: Bool = false - private var _isChainAbstractionEnabled: Bool = false + private var _isChainAbstractionEnabled: Bool = true // Thread-safe access for setting and getting isSmartAccountEnabled var isSmartAccountEnabled: Bool { diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift index c17754fcb..9a49dc59a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionModule.swift @@ -10,9 +10,13 @@ final class CATransactionModule { importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable ) -> UIViewController { - let presenter = CATransactionPresenter(sessionRequest: sessionRequest, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable) + let router = CATransactionRouter(app: app) + let presenter = CATransactionPresenter(sessionRequest: sessionRequest, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable, router: router) let view = CATransactionView().environmentObject(presenter) let viewController = SceneViewController(viewModel: presenter, content: view) + router.viewController = viewController return viewController } } + + diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index c47c902ce..b06b67573 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -19,19 +19,24 @@ final class CATransactionPresenter: ObservableObject { private let sessionRequest: Request private let routeResponseAvailable: RouteResponseAvailable let chainAbstractionService: ChainAbstractionService! - + var fundingFrom: [FundingMetadata] { + return routeResponseAvailable.metadata.fundingFrom + } + let router: CATransactionRouter private var disposeBag = Set() init( sessionRequest: Request, importAccount: ImportAccount, - routeResponseAvailable: RouteResponseAvailable + routeResponseAvailable: RouteResponseAvailable, + router: CATransactionRouter ) { self.sessionRequest = sessionRequest self.routeResponseAvailable = routeResponseAvailable let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) + self.router = router // Any additional setup for the parameters @@ -47,14 +52,41 @@ final class CATransactionPresenter: ObservableObject { do { let signedTransactions = try await chainAbstractionService.signTransactions() try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) + + // wait routing is completed + // broadcast initial transaction } catch { AlertPresenter.present(message: error.localizedDescription, type: .error) } } } - func rejectTransactions() { + @MainActor + func rejectTransactions() async throws { + do { + ActivityIndicatorManager.shared.start() + try await WalletKit.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .error(.init(code: 0, message: "")) + ) + ActivityIndicatorManager.shared.stop() + router.dismiss() + } catch { + ActivityIndicatorManager.shared.stop() + AlertPresenter.present(message: error.localizedDescription, type: .error) +// errorMessage = error.localizedDescription +// showError.toggle() + } + } + func network(for chainId: String) -> String { + let chainIdToNetwork = [ + "eip155:10": "Optimism", + "eip155:42161": "Arbitrium", + "eip155:8453": "Base" + ] + return chainIdToNetwork[chainId]! } } @@ -67,4 +99,3 @@ private extension CATransactionPresenter { // MARK: - SceneViewModel extension CATransactionPresenter: SceneViewModel {} - diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionRouter.swift new file mode 100644 index 000000000..cb7a9839c --- /dev/null +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionRouter.swift @@ -0,0 +1,18 @@ +import Foundation +import UIKit + +final class CATransactionRouter { + 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() + } + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index fbd0ff622..ef920636c 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -26,28 +26,19 @@ struct CATransactionView: View { Text("Source of funds") .foregroundColor(.gray) - // Balance Row - HStack { - Image(systemName: "creditcard.circle.fill") - .foregroundColor(.blue) - Text("Balance") - Spacer() - Text("\(presenter.balanceAmount, specifier: "%.2f") USDC") - .font(.system(.body, design: .monospaced)) - } - - // Bridging Row - HStack { - Image(systemName: "arrow.left.arrow.right.circle.fill") - .foregroundColor(.gray) - Text("Bridging") - Spacer() - VStack(alignment: .trailing) { - Text("\(presenter.bridgingAmount, specifier: "%.2f") USDC") - .font(.system(.body, design: .monospaced)) - Text("from \(presenter.bridgingSource)") - .font(.footnote) + // Iterate over funding sources + ForEach(presenter.fundingFrom, id: \.chainId) { funding in + HStack { + Image(systemName: "arrow.left.arrow.right.circle.fill") .foregroundColor(.gray) + VStack(alignment: .leading) { + Text("\(funding.amount) \(funding.symbol)") + .font(.system(.body, design: .monospaced)) + Text("from \(presenter.network(for: funding.chainId))") + .font(.footnote) + .foregroundColor(.gray) + } + Spacer() } } } @@ -140,7 +131,9 @@ struct CATransactionView: View { } Button(action: { - presenter.rejectTransactions() + Task(priority: .userInitiated) { + try await presenter.rejectTransactions() + } }) { Text("Reject") .foregroundColor(.blue) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index 1045c2f98..8a6b19786 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -44,7 +44,6 @@ final class MainRouter { func presentCATransaction(sessionRequest: Request, importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable, context: VerifyContext?) { CATransactionModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable) - .wrapToNavigationController() .present(from: viewController) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift index 6555eccf2..4b3f07dad 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Settings/SettingsView.swift @@ -6,8 +6,8 @@ import ReownAppKitUI struct SettingsView: View { @EnvironmentObject var viewModel: SettingsPresenter @State private var copyAlert: Bool = false - @State private var isSmartAccountEnabled: Bool = false - @State private var isChainAbstractionEnabled: Bool = false + @State private var isSmartAccountEnabled: Bool = WalletKitEnabler.shared.isSmartAccountEnabled + @State private var isChainAbstractionEnabled: Bool = WalletKitEnabler.shared.isChainAbstractionEnabled var body: some View { From 3a3a4a70ad6b107147706b2c21ec9e90d2543df6 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 22 Nov 2024 13:18:43 +0100 Subject: [PATCH 42/73] update ui --- .../CATransactionPresenter.swift | 27 ++++++++++++++----- .../CATransactionView.swift | 19 ++++++++----- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index b06b67573..78c0fd713 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -8,9 +8,8 @@ final class CATransactionPresenter: ObservableObject { @Published var payingAmount: Double = 10.00 @Published var balanceAmount: Double = 5.00 @Published var bridgingAmount: Double = 5.00 - @Published var bridgingSource: String = "Optimism" - @Published var appURL: String = "https://sampleapp.com" - @Published var networkName: String = "Arbitrum" + @Published var appURL: String = "" + @Published var networkName: String = "" @Published var estimatedFees: Double = 4.34 @Published var bridgeFee: Double = 3.00 @Published var purchaseFee: Double = 1.34 @@ -88,12 +87,26 @@ final class CATransactionPresenter: ObservableObject { ] return chainIdToNetwork[chainId]! } -} -// MARK: - Private functions -private extension CATransactionPresenter { + func hexAmountToDenominatedUSDC(_ hexAmount: String) -> String { + guard let indecValue = hexToDecimal(hexAmount) else { + return "Invalid amount" + } + let usdcValue = Double(indecValue) / 1_000_000 + return String(format: "%.2f", usdcValue) + } + + func hexToDecimal(_ hex: String) -> Int? { + let cleanHex = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex + + return Int(cleanHex, radix: 16) + } + func setupInitialState() { - // Initialize state if necessary + if let session = WalletKit.instance.getSessions().first(where: { $0.topic == sessionRequest.topic }) { + self.appURL = session.peer.url + } + networkName = network(for: sessionRequest.chainId.absoluteString) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index ef920636c..f091df495 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -16,7 +16,8 @@ struct CATransactionView: View { VStack(alignment: .leading, spacing: 4) { Text("Paying") .foregroundColor(.gray) - Text("\(presenter.payingAmount, specifier: "%.2f") USDC") + Text("$TODO") +// Text("\(presenter.payingAmount, specifier: "%.2f") USDC") .font(.system(.body, design: .monospaced)) } .frame(maxWidth: .infinity, alignment: .leading) @@ -26,20 +27,21 @@ struct CATransactionView: View { Text("Source of funds") .foregroundColor(.gray) - // Iterate over funding sources ForEach(presenter.fundingFrom, id: \.chainId) { funding in HStack { + Spacer() // Push content to the right Image(systemName: "arrow.left.arrow.right.circle.fill") .foregroundColor(.gray) VStack(alignment: .leading) { - Text("\(funding.amount) \(funding.symbol)") + // Use the presenter for conversion + Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") .font(.system(.body, design: .monospaced)) Text("from \(presenter.network(for: funding.chainId))") .font(.footnote) .foregroundColor(.gray) } - Spacer() } + .frame(maxWidth: .infinity, alignment: .trailing) // Ensure the entire HStack aligns to the trailing edge } } @@ -71,7 +73,8 @@ struct CATransactionView: View { Text("Estimated Fees") .foregroundColor(.gray) Spacer() - Text("$\(presenter.estimatedFees, specifier: "%.2f")") + Text("$TODO") +// Text("$\(presenter.estimatedFees, specifier: "%.2f")") .font(.system(.body, design: .monospaced)) } @@ -80,7 +83,8 @@ struct CATransactionView: View { Text("Bridge") .foregroundColor(.gray) Spacer() - Text("$\(presenter.bridgeFee, specifier: "%.2f")") + Text("$TODO") +// Text("$\(presenter.bridgeFee, specifier: "%.2f")") .font(.system(.body, design: .monospaced)) } @@ -88,7 +92,8 @@ struct CATransactionView: View { Text("Purchase") .foregroundColor(.gray) Spacer() - Text("$\(presenter.purchaseFee, specifier: "%.2f")") +// Text("$\(presenter.purchaseFee, specifier: "%.2f")") + Text("$TODO") .font(.system(.body, design: .monospaced)) } From e117c07ed2fbdfac714efe7c4c260272aa1fea4d Mon Sep 17 00:00:00 2001 From: llbartekll Date: Sat, 23 Nov 2024 10:55:06 +0100 Subject: [PATCH 43/73] savepoint --- Sources/ReownWalletKit/WalletKitClient.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 677fb4350..497f45952 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -336,7 +336,7 @@ public class WalletKitClient { return signature } - public func status(orchestrationId: String) async throws -> StatusResponseSuccess { + public func status(orchestrationId: String) async throws -> StatusResponse { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } @@ -359,6 +359,14 @@ public class WalletKitClient { return try await chainAbstractionClient.estimateFees(chainId: chainId) } + + public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { + guard let chainClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + + return try await chainClient.waitForSuccess(orchestrationId: orchestrationId, checkIn: checkIn) + } } From e1385373a4da960d80a22ca6a43bd66c6ee33967 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Sat, 23 Nov 2024 11:41:04 +0100 Subject: [PATCH 44/73] implement query status --- .../CATransactionPresenter.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 78c0fd713..1b5784738 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -49,8 +49,35 @@ final class CATransactionPresenter: ObservableObject { func approveTransactions() { Task { do { + ActivityIndicatorManager.shared.start() let signedTransactions = try await chainAbstractionService.signTransactions() try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) + let orchestrationId = routeResponseAvailable.orchestrationId +// let statusResponseCompleted = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: routeResponseAvailable.metadata.checkIn) + + + + + var status: StatusResponse = try await WalletKit.instance.status(orchestrationId: orchestrationId) + while true { + switch status { + case .pending(let pending): + let delay = try UInt64(pending.checkIn) * 1_000_000 + try await Task.sleep(nanoseconds: delay) + // Fetch the status again after the delay + status = try await WalletKit.instance.status(orchestrationId: orchestrationId) + case .completed(let completed): + // Handle the completed status + print("Transaction completed: \(completed)") + ActivityIndicatorManager.shared.stop() + return + case .error(let error): + // Handle the error + print("Transaction failed: \(error)") + ActivityIndicatorManager.shared.stop() + return + } + } // wait routing is completed // broadcast initial transaction From eec72fbe73652d90f0bc422075dada05c22b3dde Mon Sep 17 00:00:00 2001 From: llbartekll Date: Sat, 23 Nov 2024 12:03:44 +0100 Subject: [PATCH 45/73] savepoint --- .../ChainAbstractionService.swift | 76 ------------------- .../CATransactionPresenter.swift | 30 ++++++-- 2 files changed, 23 insertions(+), 83 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 8c1df6672..3d05ec5e6 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -17,82 +17,6 @@ class ChainAbstractionService { self.routeResponseAvailable = routeResponseAvailable } -// func handle(request: Request) async throws { -// struct Tx: Codable { -// let data: String -// let from: String -// let to: String -// } -// -// -// guard request.method == "eth_sendTransaction" else { -// return -// } -// do { -// let tx = try request.params.get([Tx].self)[0] -// -// let transaction = EthTransaction( -// from: tx.from, -// to: tx.to, -// value: "0", -// gas: "0", -// gasPrice: "0", -// data: tx.data, -// nonce: "0", -// maxFeePerGas: "0", -// maxPriorityFeePerGas: "0", -// chainId: request.chainId.absoluteString -// ) -// -// let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) -// -// switch routeResponseSuccess { -// -// case .available(let routeResponseAvailable): -// -// var transactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] -// -// for tx in routeResponseAvailable.transactions { -// do { -// let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) -// let maxPriorityFeePerGas = EthereumQuantity(quantity: try BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) -// let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) -// -// print(maxFeePerGas) -// print(maxPriorityFeePerGas) -// let transaction = try EthereumTransaction( -// routingTransaction: tx, -// maxPriorityFeePerGas: maxPriorityFeePerGas, -// maxFeePerGas: maxFeePerGas -// ) -// -// let chain = Blockchain(tx.chainId)! -// let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) -// -// print(chainId.quantity) -// let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) -// -// print(signedTransaction.value) -// transactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) -// } catch { -// print("Error processing transaction: \(error)") -// } -// } -// -// try await broadcastTransactions(transactions: transactions) -// -// -// case .notRequired(let routeResponseNotRequired): -// print(("routing not required")) -// } -// print(tx) -// } catch { -// print(error) -// } -// } - - - func signTransactions() async throws -> [(transaction: EthereumSignedTransaction, chainId: String)] { var signedTransactions: [(transaction: EthereumSignedTransaction, chainId: String)] = [] diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 1b5784738..6aa7bde33 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -22,6 +22,7 @@ final class CATransactionPresenter: ObservableObject { return routeResponseAvailable.metadata.fundingFrom } let router: CATransactionRouter + let initialTransaction: TransactionData private var disposeBag = Set() @@ -29,14 +30,15 @@ final class CATransactionPresenter: ObservableObject { sessionRequest: Request, importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable, - router: CATransactionRouter + router: CATransactionRouter, + initialTransaction: TransactionData ) { self.sessionRequest = sessionRequest self.routeResponseAvailable = routeResponseAvailable let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) self.router = router - + self.initialTransaction = initialTransaction // Any additional setup for the parameters setupInitialState() @@ -51,13 +53,13 @@ final class CATransactionPresenter: ObservableObject { do { ActivityIndicatorManager.shared.start() let signedTransactions = try await chainAbstractionService.signTransactions() + + print(signedTransactions[0]) try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) let orchestrationId = routeResponseAvailable.orchestrationId // let statusResponseCompleted = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: routeResponseAvailable.metadata.checkIn) - - var status: StatusResponse = try await WalletKit.instance.status(orchestrationId: orchestrationId) while true { switch status { @@ -69,17 +71,19 @@ final class CATransactionPresenter: ObservableObject { case .completed(let completed): // Handle the completed status print("Transaction completed: \(completed)") - ActivityIndicatorManager.shared.stop() - return + AlertPresenter.present(message: "routing transactions completed", type: .success) case .error(let error): // Handle the error print("Transaction failed: \(error)") + AlertPresenter.present(message: "routing failed with error: \(error)", type: .error) ActivityIndicatorManager.shared.stop() return } } - // wait routing is completed + try await sendInitialTransaction() + ActivityIndicatorManager.shared.stop() + // broadcast initial transaction } catch { AlertPresenter.present(message: error.localizedDescription, type: .error) @@ -87,6 +91,18 @@ final class CATransactionPresenter: ObservableObject { } } + private func sendInitialTransaction() async throws { + + + + let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + + print(maxFeePerGas) + print(maxPriorityFeePerGas) + } + @MainActor func rejectTransactions() async throws { do { From e60eaa2cf8326665a9d1edae7b60fb275960dffb Mon Sep 17 00:00:00 2001 From: llbartekll Date: Sat, 23 Nov 2024 12:16:44 +0100 Subject: [PATCH 46/73] savepoint --- .../CATransactionPresenter.swift | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 6aa7bde33..246fc3184 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -22,7 +22,7 @@ final class CATransactionPresenter: ObservableObject { return routeResponseAvailable.metadata.fundingFrom } let router: CATransactionRouter - let initialTransaction: TransactionData + let importAccount: ImportAccount private var disposeBag = Set() @@ -31,13 +31,13 @@ final class CATransactionPresenter: ObservableObject { importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable, router: CATransactionRouter, - initialTransaction: TransactionData ) { self.sessionRequest = sessionRequest self.routeResponseAvailable = routeResponseAvailable let prvKey = try! EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) self.router = router + self.importAccount = importAccount self.initialTransaction = initialTransaction // Any additional setup for the parameters @@ -92,15 +92,45 @@ final class CATransactionPresenter: ObservableObject { } private func sendInitialTransaction() async throws { + struct Tx: Codable { + let data: String + let from: String + let to: String + } + + + let tx = try! sessionRequest.params.get([Tx].self)[0] - - let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) + + let estimates = try await WalletKit.instance.estimateFees(chainId: sessionRequest.chainId.absoluteString) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) - print(maxFeePerGas) - print(maxPriorityFeePerGas) + + let ethTransaction = EthereumTransaction( + nonce: 0, + gasPrice: nil, + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, + gasLimit: EthereumQuantity(quantity: 1023618), + from: try EthereumAddress(hex: tx.from, eip55: false), + to: try EthereumAddress(hex: tx.to, eip55: false), + value: EthereumQuantity(quantity: 0.gwei), + data: EthereumData(Array(hex: tx.data)), + accessList: [:], + transactionType: .eip1559) + + let chain = sessionRequest.chainId + let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) + + let privateKey = try EthereumPrivateKey(hexPrivateKey: importAccount.privateKey) + + let signedTransaction = try ethTransaction.sign(with: privateKey, chainId: chainId) + + try await chainAbstractionService.broadcastTransactions(transactions: [(signedTransaction, chain.absoluteString)]) + } @MainActor From 85c6d014d376bf0241795a0375640ec89f605d3a Mon Sep 17 00:00:00 2001 From: llbartekll Date: Sat, 23 Nov 2024 12:17:38 +0100 Subject: [PATCH 47/73] fix build --- .../Wallet/CATransactionModal/CATransactionPresenter.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 246fc3184..96abb2ccf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -30,7 +30,7 @@ final class CATransactionPresenter: ObservableObject { sessionRequest: Request, importAccount: ImportAccount, routeResponseAvailable: RouteResponseAvailable, - router: CATransactionRouter, + router: CATransactionRouter ) { self.sessionRequest = sessionRequest self.routeResponseAvailable = routeResponseAvailable @@ -38,8 +38,6 @@ final class CATransactionPresenter: ObservableObject { self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) self.router = router self.importAccount = importAccount - self.initialTransaction = initialTransaction - // Any additional setup for the parameters setupInitialState() } From d931092913732799df328605df683460096fc351 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 25 Nov 2024 09:05:26 +0100 Subject: [PATCH 48/73] get nonce --- .../CATransactionPresenter.swift | 76 ++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 96abb2ccf..0d3b92dff 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -4,6 +4,11 @@ import Web3 import ReownWalletKit final class CATransactionPresenter: ObservableObject { + enum Errors: Error { + case invalidURL + case invalidResponse + case invalidData + } // Published properties to be used in the view @Published var payingAmount: Double = 10.00 @Published var balanceAmount: Double = 5.00 @@ -55,11 +60,11 @@ final class CATransactionPresenter: ObservableObject { print(signedTransactions[0]) try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) let orchestrationId = routeResponseAvailable.orchestrationId -// let statusResponseCompleted = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: routeResponseAvailable.metadata.checkIn) + // let statusResponseCompleted = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: routeResponseAvailable.metadata.checkIn) var status: StatusResponse = try await WalletKit.instance.status(orchestrationId: orchestrationId) - while true { + loop: while true { switch status { case .pending(let pending): let delay = try UInt64(pending.checkIn) * 1_000_000 @@ -70,12 +75,13 @@ final class CATransactionPresenter: ObservableObject { // Handle the completed status print("Transaction completed: \(completed)") AlertPresenter.present(message: "routing transactions completed", type: .success) + break loop // Exit the labeled loop but continue with the function case .error(let error): // Handle the error print("Transaction failed: \(error)") AlertPresenter.present(message: "routing failed with error: \(error)", type: .error) ActivityIndicatorManager.shared.stop() - return + return // Exit the function due to an error } } @@ -96,7 +102,7 @@ final class CATransactionPresenter: ObservableObject { let to: String } - + print("will send initial transaction") let tx = try! sessionRequest.params.get([Tx].self)[0] @@ -105,15 +111,17 @@ final class CATransactionPresenter: ObservableObject { let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + let from = try EthereumAddress(hex: tx.from, eip55: false) + let nonce = try await getNonce(for: from, chainId: sessionRequest.chainId.absoluteString) let ethTransaction = EthereumTransaction( - nonce: 0, + nonce: nonce, gasPrice: nil, maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, gasLimit: EthereumQuantity(quantity: 1023618), - from: try EthereumAddress(hex: tx.from, eip55: false), + from: from, to: try EthereumAddress(hex: tx.to, eip55: false), value: EthereumQuantity(quantity: 0.gwei), data: EthereumData(Array(hex: tx.data)), @@ -131,6 +139,56 @@ final class CATransactionPresenter: ObservableObject { } + func getNonce(for address: EthereumAddress, chainId: String) async throws -> EthereumQuantity { + // Create a Web3 provider + + print("Getting nonce for initial transaction...") + let projectId = Networking.projectId + let rpcUrl = "rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + + let params = [address.hex(eip55: true), "latest"] + + let rpcRequest = RPCRequest(method: "eth_getTransactionCount", params: params) + + guard let url = URL(string: "https://" + rpcUrl) else { + throw Errors.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Convert RPC request to JSON data + let jsonData = try JSONEncoder().encode(rpcRequest) + request.httpBody = jsonData + + do { + // Perform the URL session request + let (data, response) = try await URLSession.shared.data(for: request) + + // Validate the response + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw Errors.invalidResponse + } + + let rpcResponse = try JSONDecoder().decode(RPCResponse.self, from: data) + + let responseJSON = try JSONSerialization.jsonObject(with: data) + print(responseJSON) + + let stringResult = try rpcResponse.result!.get(String.self) + guard let nonceValue = BigUInt(stringResult.stripHexPrefix(), radix: 16) else { + throw Errors.invalidData + } + + return EthereumQuantity(quantity: nonceValue) + } catch { + print("Error fetching nonce: \(error)") + throw error + } + } + @MainActor func rejectTransactions() async throws { do { @@ -145,8 +203,8 @@ final class CATransactionPresenter: ObservableObject { } catch { ActivityIndicatorManager.shared.stop() AlertPresenter.present(message: error.localizedDescription, type: .error) -// errorMessage = error.localizedDescription -// showError.toggle() + // errorMessage = error.localizedDescription + // showError.toggle() } } @@ -166,7 +224,7 @@ final class CATransactionPresenter: ObservableObject { let usdcValue = Double(indecValue) / 1_000_000 return String(format: "%.2f", usdcValue) } - + func hexToDecimal(_ hex: String) -> Int? { let cleanHex = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex From 86f95c33b0e401d7922edfb65528816fde0247ea Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 25 Nov 2024 12:14:01 +0100 Subject: [PATCH 49/73] update ui --- .../tada.imageset/Contents.json | 21 + .../Assets.xcassets/tada.imageset/tada.png | Bin 0 -> 3068 bytes .../CATransactionPresenter.swift | 181 ++++++--- .../CATransactionView.swift | 380 +++++++++++++----- 4 files changed, 417 insertions(+), 165 deletions(-) create mode 100644 Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json create mode 100644 Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png diff --git a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json new file mode 100644 index 000000000..e1308ded3 --- /dev/null +++ b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "tada.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png new file mode 100644 index 0000000000000000000000000000000000000000..c885e0e2a9b6bc2acddce46330b715a451c58b10 GIT binary patch literal 3068 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 EthereumQuantity { - // Create a Web3 provider + print("🔢 Getting nonce for address: \(address.hex(eip55: true))") + print("⛓️ Chain ID: \(chainId)") - print("Getting nonce for initial transaction...") let projectId = Networking.projectId let rpcUrl = "rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + print("🌐 Using RPC URL: \(rpcUrl)") let params = [address.hex(eip55: true), "latest"] - let rpcRequest = RPCRequest(method: "eth_getTransactionCount", params: params) + print("📝 Created RPC request for nonce") guard let url = URL(string: "https://" + rpcUrl) else { + print("❌ Failed to create URL from RPC URL string") throw Errors.invalidURL } @@ -163,28 +191,38 @@ final class CATransactionPresenter: ObservableObject { request.httpBody = jsonData do { - // Perform the URL session request + print("📡 Sending request to get nonce...") let (data, response) = try await URLSession.shared.data(for: request) - // Validate the response - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { + guard let httpResponse = response as? HTTPURLResponse else { + print("❌ Invalid response type received") throw Errors.invalidResponse } - let rpcResponse = try JSONDecoder().decode(RPCResponse.self, from: data) + print("📊 Response status code: \(httpResponse.statusCode)") + + guard (200...299).contains(httpResponse.statusCode) else { + print("❌ Received error status code: \(httpResponse.statusCode)") + throw Errors.invalidResponse + } + let rpcResponse = try JSONDecoder().decode(RPCResponse.self, from: data) let responseJSON = try JSONSerialization.jsonObject(with: data) - print(responseJSON) + print("📥 Raw response: \(responseJSON)") let stringResult = try rpcResponse.result!.get(String.self) + print("🔢 Nonce hex string: \(stringResult)") + guard let nonceValue = BigUInt(stringResult.stripHexPrefix(), radix: 16) else { + print("❌ Failed to parse nonce value from hex string") throw Errors.invalidData } + print("✅ Successfully retrieved nonce: \(nonceValue)") return EthereumQuantity(quantity: nonceValue) } catch { - print("Error fetching nonce: \(error)") + print("❌ Error while fetching nonce:") + print("💥 Error details: \(error)") throw error } } @@ -241,3 +279,38 @@ final class CATransactionPresenter: ObservableObject { // MARK: - SceneViewModel extension CATransactionPresenter: SceneViewModel {} + + + + +extension CATransactionPresenter { + // Test function that succeeds after delay + func testAsyncSuccess() async throws { + print("Starting test async operation...") + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay + print("Test async operation completed successfully") + DispatchQueue.main.async { [weak self] in + self?.transactionCompleted = true + } + } + + // Test function that throws after delay + func testAsyncError() async throws { + print("Starting test async operation that will fail...") + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay + + enum TestError: Error, LocalizedError { + case sampleError + + var errorDescription: String? { + return "This is a test error" + } + + var failureReason: String? { + return "The operation failed because this is a test of error handling" + } + } + + throw TestError.sampleError + } +} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index f091df495..5eab32bb6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -1,150 +1,308 @@ import SwiftUI +import AsyncButton struct CATransactionView: View { @EnvironmentObject var presenter: CATransactionPresenter @Environment(\.dismiss) private var dismiss + @State private var viewScale: CGFloat = 1.0 var body: some View { - VStack(spacing: 24) { - // Header - Text("Review Transaction") - .font(.headline) - .padding(.top) - - VStack(spacing: 20) { - // Paying Section - VStack(alignment: .leading, spacing: 4) { - Text("Paying") - .foregroundColor(.gray) - Text("$TODO") -// Text("\(presenter.payingAmount, specifier: "%.2f") USDC") - .font(.system(.body, design: .monospaced)) - } - .frame(maxWidth: .infinity, alignment: .leading) + ZStack { + if presenter.transactionCompleted { + TransactionCompletedView() + .scaleEffect(viewScale) // Apply the renamed property here + .onAppear { + withAnimation(.easeInOut(duration: 0.4)) { + viewScale = 1.0 // Reset scaling for the new view + } + } + } else { + VStack(spacing: 24) { + // Header + Text("Review Transaction") + .font(.headline) + .padding(.top) - // Source of funds - VStack(alignment: .leading, spacing: 12) { - Text("Source of funds") - .foregroundColor(.gray) + VStack(spacing: 20) { + // Paying Section + VStack(alignment: .leading, spacing: 4) { + Text("Paying") + .foregroundColor(.gray) + Text("$TODO") + // Text("\(presenter.payingAmount, specifier: "%.2f") USDC") + .font(.system(.body, design: .monospaced)) + } + .frame(maxWidth: .infinity, alignment: .leading) - ForEach(presenter.fundingFrom, id: \.chainId) { funding in - HStack { - Spacer() // Push content to the right - Image(systemName: "arrow.left.arrow.right.circle.fill") + // Source of funds + VStack(alignment: .leading, spacing: 12) { + Text("Source of funds") .foregroundColor(.gray) - VStack(alignment: .leading) { - // Use the presenter for conversion - Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") - .font(.system(.body, design: .monospaced)) - Text("from \(presenter.network(for: funding.chainId))") - .font(.footnote) + + ForEach(presenter.fundingFrom, id: \.chainId) { funding in + HStack { + Spacer() // Push content to the right + Image(systemName: "arrow.left.arrow.right.circle.fill") + .foregroundColor(.gray) + VStack(alignment: .leading) { + // Use the presenter for conversion + Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") + .font(.system(.body, design: .monospaced)) + Text("from \(presenter.network(for: funding.chainId))") + .font(.footnote) + .foregroundColor(.gray) + } + } + .frame(maxWidth: .infinity, alignment: .trailing) // Ensure the entire HStack aligns to the trailing edge + } + } + + // App and Network + VStack(spacing: 12) { + HStack { + Text("App") .foregroundColor(.gray) + Spacer() + Text(presenter.appURL) + .foregroundColor(.blue) + } + + HStack { + Text("Network") + .foregroundColor(.gray) + Spacer() + HStack(spacing: 4) { + Image(systemName: "network") + .foregroundColor(.blue) + Text(presenter.networkName) + } } } - .frame(maxWidth: .infinity, alignment: .trailing) // Ensure the entire HStack aligns to the trailing edge - } - } - // App and Network - VStack(spacing: 12) { - HStack { - Text("App") - .foregroundColor(.gray) - Spacer() - Text(presenter.appURL) - .foregroundColor(.blue) - } + // Estimated Fees Section + VStack(spacing: 12) { + HStack { + Text("Estimated Fees") + .foregroundColor(.gray) + Spacer() + Text("$TODO") + // Text("$\(presenter.estimatedFees, specifier: "%.2f")") + .font(.system(.body, design: .monospaced)) + } - HStack { - Text("Network") - .foregroundColor(.gray) - Spacer() - HStack(spacing: 4) { - Image(systemName: "network") - .foregroundColor(.blue) - Text(presenter.networkName) + VStack(spacing: 8) { + HStack { + Text("Bridge") + .foregroundColor(.gray) + Spacer() + Text("$TODO") + // Text("$\(presenter.bridgeFee, specifier: "%.2f")") + .font(.system(.body, design: .monospaced)) + } + + HStack { + Text("Purchase") + .foregroundColor(.gray) + Spacer() + // Text("$\(presenter.purchaseFee, specifier: "%.2f")") + Text("$TODO") + .font(.system(.body, design: .monospaced)) + } + + HStack { + Text("Execution") + .foregroundColor(.gray) + Spacer() + Text(presenter.executionSpeed) + .font(.system(.body, design: .monospaced)) + } + } + .padding(.leading) } + .padding() + .background(Color(.systemGray6)) + .cornerRadius(12) } - } + .padding(.horizontal) - // Estimated Fees Section - VStack(spacing: 12) { - HStack { - Text("Estimated Fees") - .foregroundColor(.gray) - Spacer() - Text("$TODO") -// Text("$\(presenter.estimatedFees, specifier: "%.2f")") - .font(.system(.body, design: .monospaced)) - } + Spacer() - VStack(spacing: 8) { - HStack { - Text("Bridge") - .foregroundColor(.gray) - Spacer() - Text("$TODO") -// Text("$\(presenter.bridgeFee, specifier: "%.2f")") - .font(.system(.body, design: .monospaced)) + // Action Buttons + VStack(spacing: 12) { + AsyncButton( + options: [ + .showProgressViewOnLoading, + .disableButtonOnLoading, + .showAlertOnError, + .enableNotificationFeedback + ] + ) { + try await presenter.testAsyncSuccess() + } label: { + Text("Test Success") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.green) + .cornerRadius(12) } - HStack { - Text("Purchase") - .foregroundColor(.gray) - Spacer() -// Text("$\(presenter.purchaseFee, specifier: "%.2f")") - Text("$TODO") - .font(.system(.body, design: .monospaced)) + // Error test button + AsyncButton( + options: [ + .showProgressViewOnLoading, + .disableButtonOnLoading, + .showAlertOnError, + .enableNotificationFeedback + ] + ) { + try await presenter.testAsyncError() + } label: { + Text("Test Error") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.red) + .cornerRadius(12) } - HStack { - Text("Execution") - .foregroundColor(.gray) - Spacer() - Text(presenter.executionSpeed) - .font(.system(.body, design: .monospaced)) + // Original Approve button + AsyncButton( + options: [ + .showProgressViewOnLoading, + .disableButtonOnLoading, + .showAlertOnError, + .enableNotificationFeedback + ] + ) { + try await presenter.approveTransactions() + } label: { + Text("Approve") + .fontWeight(.semibold) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background( + LinearGradient( + gradient: Gradient(colors: [Color.blue, Color.purple]), + startPoint: .leading, + endPoint: .trailing + ) + ) + .cornerRadius(12) + } + + Button(action: { + Task(priority: .userInitiated) { + try await presenter.rejectTransactions() + } + }) { + Text("Reject") + .foregroundColor(.blue) } } - .padding(.leading) + .padding() } - .padding() - .background(Color(.systemGray6)) - .cornerRadius(12) } - .padding(.horizontal) + } + } +} - Spacer() - // Action Buttons - VStack(spacing: 12) { - Button(action: { - presenter.approveTransactions() - }) { - Text("Approve") - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background( - LinearGradient( - gradient: Gradient(colors: [Color.blue, Color.purple]), - startPoint: .leading, - endPoint: .trailing - ) +struct TransactionCompletedView: View { + @EnvironmentObject var presenter: CATransactionPresenter + + var body: some View { + VStack(spacing: 32) { + Text("Transaction Completed") + .font(.title2) + .fontWeight(.semibold) + + // Original tada image in blue-purple gradient circle + ZStack { + Circle() + .fill( + LinearGradient( + gradient: Gradient(colors: [Color.blue, Color.purple]), + startPoint: .leading, + endPoint: .trailing ) - .cornerRadius(12) + ) + .frame(width: 80, height: 80) + + Image("tada") + .resizable() + .scaledToFit() + .frame(width: 40, height: 40) + } + + Text("You successfully sent USDC!") + .font(.title3) + .foregroundColor(.gray) + + // Transaction details + VStack(spacing: 24) { + HStack { + Text("Payed") + .foregroundColor(.gray) + Spacer() + Text("X USDC") + .font(.system(.body, design: .monospaced)) + Text("on ") + Text("\(presenter.networkName)") + .font(.system(.body, design: .monospaced)) } - Button(action: { - Task(priority: .userInitiated) { - try await presenter.rejectTransactions() + HStack { + Text("Bridged") + .foregroundColor(.gray) + Spacer() + ForEach(presenter.fundingFrom, id: \.chainId) { funding in + Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") + .font(.system(.body, design: .monospaced)) + Text("from ") + Text("\(presenter.network(for: funding.chainId))") + .font(.system(.body, design: .monospaced)) } - }) { - Text("Reject") - .foregroundColor(.blue) } } .padding() + .background(Color(.systemGray6)) + .cornerRadius(16) + + // View on Explorer button + Button(action: { + // presenter.onViewOnExplorer() + }) { + HStack { + Text("View on Explorer") + Image(systemName: "arrow.up.forward") + .font(.footnote) + } + .foregroundColor(.gray) + } + + Spacer() + + // Back to App button + Button(action: { + presenter.dismiss() + }) { + Text("Back to App") + .fontWeight(.semibold) + .frame(maxWidth: .infinity) + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color(.separator), lineWidth: 1) + ) + } } + .padding(24) + .background(Color(.systemBackground)) } } From 750d5620a84f6ccb703d09dbcdb98f85ed956527 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 25 Nov 2024 12:21:49 +0100 Subject: [PATCH 50/73] open explorer --- .../CATransactionPresenter.swift | 23 +++++++++++++++++++ .../CATransactionView.swift | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 973305d23..17a5b6ad1 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -275,6 +275,29 @@ final class CATransactionPresenter: ObservableObject { } networkName = network(for: sessionRequest.chainId.absoluteString) } + + func onViewOnExplorer() { + // Force unwrap the address from the import account + let address = importAccount.account.address + + // Mapping of network names to Blockscout URLs + let networkBaseURLMap: [String: String] = [ + "Optimism": "optimism.blockscout.com", + "Arbitrium": "arbitrum.blockscout.com", + "Base": "base.blockscout.com" + ] + + // Force unwrap the base URL for the current network + let baseURL = networkBaseURLMap[networkName]! + + // Construct the explorer URL + let explorerURL = URL(string: "https://\(baseURL)/address/\(address)")! + + // Open the URL in Safari + UIApplication.shared.open(explorerURL, options: [:], completionHandler: nil) + + print("🌐 Opened explorer URL: \(explorerURL)") + } } // MARK: - SceneViewModel diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index 5eab32bb6..c6be00d08 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -274,7 +274,7 @@ struct TransactionCompletedView: View { // View on Explorer button Button(action: { - // presenter.onViewOnExplorer() + presenter.onViewOnExplorer() }) { HStack { Text("View on Explorer") From ece90512231771d786fa908ae8d05300745028f9 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 25 Nov 2024 13:00:23 +0100 Subject: [PATCH 51/73] savepoint --- .../CATransactionPresenter.swift | 9 ++- .../CATransactionView.swift | 76 +++++++++---------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 17a5b6ad1..24ab45487 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -95,7 +95,6 @@ final class CATransactionPresenter: ObservableObject { } print("🚀 Initiating initial transaction...") - fatalError() try await sendInitialTransaction() ActivityIndicatorManager.shared.stop() print("✅ Initial transaction process completed successfully") @@ -163,6 +162,14 @@ final class CATransactionPresenter: ObservableObject { print("📡 Broadcasting initial transaction...") try await chainAbstractionService.broadcastTransactions(transactions: [(signedTransaction, chain.absoluteString)]) print("✅ Initial transaction broadcast complete") + + let result = signedTransaction.r.hex() + signedTransaction.s.hex().dropFirst(2) + String(signedTransaction.v.quantity, radix: 16) + + try await WalletKit.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(result))) + + DispatchQueue.main.async { [weak self] in + self?.transactionCompleted = true + } } func getNonce(for address: EthereumAddress, chainId: String) async throws -> EthereumQuantity { diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index c6be00d08..c7daeee19 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -129,44 +129,44 @@ struct CATransactionView: View { // Action Buttons VStack(spacing: 12) { - AsyncButton( - options: [ - .showProgressViewOnLoading, - .disableButtonOnLoading, - .showAlertOnError, - .enableNotificationFeedback - ] - ) { - try await presenter.testAsyncSuccess() - } label: { - Text("Test Success") - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background(Color.green) - .cornerRadius(12) - } - - // Error test button - AsyncButton( - options: [ - .showProgressViewOnLoading, - .disableButtonOnLoading, - .showAlertOnError, - .enableNotificationFeedback - ] - ) { - try await presenter.testAsyncError() - } label: { - Text("Test Error") - .fontWeight(.semibold) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background(Color.red) - .cornerRadius(12) - } +// AsyncButton( +// options: [ +// .showProgressViewOnLoading, +// .disableButtonOnLoading, +// .showAlertOnError, +// .enableNotificationFeedback +// ] +// ) { +// try await presenter.testAsyncSuccess() +// } label: { +// Text("Test Success") +// .fontWeight(.semibold) +// .foregroundColor(.white) +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.green) +// .cornerRadius(12) +// } +// +// // Error test button +// AsyncButton( +// options: [ +// .showProgressViewOnLoading, +// .disableButtonOnLoading, +// .showAlertOnError, +// .enableNotificationFeedback +// ] +// ) { +// try await presenter.testAsyncError() +// } label: { +// Text("Test Error") +// .fontWeight(.semibold) +// .foregroundColor(.white) +// .frame(maxWidth: .infinity) +// .padding() +// .background(Color.red) +// .cornerRadius(12) +// } // Original Approve button AsyncButton( From d0c893392d99b6fc830c0ac7c85120feff1a2ad9 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 26 Nov 2024 08:13:50 +0100 Subject: [PATCH 52/73] savepoint --- .../PresentationLayer/Wallet/Main/MainPresenter.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 05227c0af..8c6860790 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -112,21 +112,22 @@ extension MainPresenter { + ActivityIndicatorManager.shared.start() let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) await MainActor.run { switch routeResponseSuccess { case .available(let routeResponseAvailable): - router.presentCATransaction(sessionRequest: request, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable, context: context) - case .notRequired(let routeResponseNotRequired): AlertPresenter.present(message: "Routing not required", type: .success) router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) } } + ActivityIndicatorManager.shared.stop() } catch { await MainActor.run { + ActivityIndicatorManager.shared.stop() AlertPresenter.present(message: "CA error: \(error.localizedDescription)", type: .error) router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) } From 164204a1d91fe8573124307e6c48300485197147 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 26 Nov 2024 09:28:46 +0100 Subject: [PATCH 53/73] prepare release --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 4a8f5606c..dd859917d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = true +let yttriumDebug = false // Define dependencies array @@ -30,7 +30,7 @@ func buildYttriumWrapperTarget() -> Target { swiftSettings: yttriumSwiftSettings ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.7"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.13"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From 15ae6855b8665fd89c63cb6607c31db4918d9d4e Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 28 Nov 2024 07:52:24 +0100 Subject: [PATCH 54/73] integrate uniffi client --- .../AuthRequest/AuthRequestPresenter.swift | 2 +- .../Wallet/Wallet/WalletPresenter.swift | 4 +- Package.swift | 6 +- .../SmartAccount/SafesManager.swift | 39 +++++----- Sources/ReownWalletKit/WalletKitClient.swift | 78 +++++++++---------- .../WalletKitDecryptionService.swift | 2 +- Sources/YttriumWrapper/YttriumWrapper.swift | 4 - 7 files changed, 66 insertions(+), 69 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift index ecac1c074..b8b9877b3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -5,7 +5,7 @@ import ReownWalletKit import ReownRouter final class AuthRequestPresenter: ObservableObject { - enum Errors: Error { + enum Errors: LocalizedError { case noCommonChains } private let router: AuthRequestRouter diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 13575c232..573e8d722 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -4,7 +4,7 @@ import Combine import ReownWalletKit final class WalletPresenter: ObservableObject { - enum Errors: Error { + enum Errors: LocalizedError { case invalidUri(uri: String) } @@ -169,7 +169,7 @@ extension WalletPresenter: SceneViewModel { } // MARK: - LocalizedError -extension WalletPresenter.Errors: LocalizedError { +extension WalletPresenter.Errors { var errorDescription: String? { switch self { case .invalidUri(let uri): return "URI invalid format\n\(uri)" diff --git a/Package.swift b/Package.swift index dd859917d..f90fdb142 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true // Define dependencies array @@ -21,11 +21,11 @@ func buildYttriumWrapperTarget() -> Target { // Conditionally add Yttrium dependency if yttriumDebug { var yttriumSwiftSettings: [SwiftSetting] = [] - dependencies.append(.package(path: "../yttrium/crates/ffi/YttriumCore")) + dependencies.append(.package(path: "../yttrium")) yttriumSwiftSettings.append(.define("YTTRIUM_DEBUG")) return .target( name: "YttriumWrapper", - dependencies: [.product(name: "YttriumCore", package: "YttriumCore")], + dependencies: [.product(name: "Yttrium", package: "yttrium")], path: "Sources/YttriumWrapper", swiftSettings: yttriumSwiftSettings ) diff --git a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift index 14129ec7a..7f2d5d7c1 100644 --- a/Sources/ReownWalletKit/SmartAccount/SafesManager.swift +++ b/Sources/ReownWalletKit/SmartAccount/SafesManager.swift @@ -3,14 +3,14 @@ import Foundation import YttriumWrapper class SafesManager { - var ownerToClient: [Account: AccountClient] = [:] + var ownerToClient: [Account: FfiAccountClient] = [:] let apiKey: String init(pimlicoApiKey: String) { self.apiKey = pimlicoApiKey } - func getOrCreateSafe(for owner: Account) -> AccountClient { + func getOrCreateSafe(for owner: Account) -> FfiAccountClient { if let client = ownerToClient[owner] { return client } else { @@ -21,29 +21,30 @@ class SafesManager { } } - private func createSafe(ownerAccount: Account) -> AccountClient { + private func createSafe(ownerAccount: Account) -> FfiAccountClient { let chainId = ownerAccount.reference let projectId = Networking.projectId let pimlicoBundlerUrl = "https://api.pimlico.io/v2/\(chainId)/rpc?apikey=\(apiKey)" let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(ownerAccount.blockchainIdentifier)&projectId=\(projectId)" - let pimlicoSepolia = YttriumWrapper.Config( - endpoints: .init( - rpc: .init(baseURL: rpcUrl), - bundler: .init(baseURL: pimlicoBundlerUrl), - paymaster: .init(baseURL: pimlicoBundlerUrl) - ) - ) + + let pimlicoSepolia = Config(endpoints: .init( + rpc: .init(baseUrl: rpcUrl, apiKey: ""), + bundler: .init(baseUrl: pimlicoBundlerUrl, apiKey: ""), + paymaster: .init(baseUrl: pimlicoBundlerUrl, apiKey: "") + )) // use YttriumWrapper.Config.local() for local foundry node - let x = AccountClient( + + let FfiAccountClientConfig = FfiAccountClientConfig( ownerAddress: ownerAccount.address, - entryPoint: "", // remove the entrypoint - chainId: Int(ownerAccount.blockchain.reference)!, + chainId: UInt64(ownerAccount.blockchain.reference)!, config: pimlicoSepolia, - safe: true - ) - // TODO remove registration - - x.register(privateKey: "ff89825a799afce0d5deaa079cdde227072ec3f62973951683ac8cc033092156") - return x + signerType: "PrivateKey", + safe: true, + privateKey: "ff89825a799afce0d5deaa079cdde227072ec3f62973951683ac8cc033092156") + + let client = FfiAccountClient(config: FfiAccountClientConfig) + + + return client } } diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 497f45952..31921c10f 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -8,7 +8,7 @@ import YttriumWrapper /// /// Access via `WalletKit.instance` public class WalletKitClient { - enum Errors: Error { + enum Errors: LocalizedError { case smartAccountNotEnabled case chainAbstractionNotEnabled } @@ -275,12 +275,12 @@ public class WalletKitClient { // MARK: Yttrium - public func prepareSendTransactions(_ transactions: [Transaction], ownerAccount: Account) async throws -> PreparedSendTransaction { + public func prepareSendTransactions(_ transactions: [FfiTransaction], ownerAccount: Account) async throws -> PreparedSendTransaction { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled } let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - return try await client.prepareSendTransactions(transactions) + return try await client.prepareSendTransactions(transactions: transactions) } public func doSendTransaction(signatures: [OwnerSignature], doSendTransactionParams: String, ownerAccount: Account) async throws -> String { @@ -288,7 +288,7 @@ public class WalletKitClient { throw Errors.smartAccountNotEnabled } let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - return try await client.doSendTransaction(signatures: signatures, params: doSendTransactionParams) + return try await client.doSendTransactions(signatures: signatures, doSendTransactionParams: doSendTransactionParams) } public func getSmartAccount(ownerAccount: Account) async throws -> Account { @@ -302,39 +302,39 @@ public class WalletKitClient { return Account(blockchain: ownerAccount.blockchain, address: address)! } - public func waitForUserOperationReceipt(userOperationHash: String, ownerAccount: Account) async throws -> UserOperationReceipt { + public func waitForUserOperationReceipt(userOperationHash: String, ownerAccount: Account) async throws -> String { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled } let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) return try await client.waitForUserOperationReceipt(userOperationHash: userOperationHash) } - - public func prepareSignMessage(_ messageHash: String, ownerAccount: Account) async throws -> PreparedSignMessage { - guard let smartAccountsManager = smartAccountsManager else { - throw Errors.smartAccountNotEnabled - } - let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - return try await client.prepareSignMessage(messageHash) - } - - public func doSignMessage(_ signatures: [String], ownerAccount: Account) async throws -> PreparedSign { - guard let smartAccountsManager = smartAccountsManager else { - throw Errors.smartAccountNotEnabled - } - let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - let signature = try await client.doSignMessage(signatures) - return signature - } - - public func finalizeSignMessage(_ signatures: [String], signStep3Params: String, ownerAccount: Account) async throws -> String { - guard let smartAccountsManager = smartAccountsManager else { - throw Errors.smartAccountNotEnabled - } - let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) - let signature = try await client.finalizeSignMessage(signatures, signStep3Params: signStep3Params) - return signature - } +// +// public func prepareSignMessage(_ messageHash: String, ownerAccount: Account) async throws -> PreparedSignMessage { +// guard let smartAccountsManager = smartAccountsManager else { +// throw Errors.smartAccountNotEnabled +// } +// let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) +// return try await client.prepareSignMessage(messageHash) +// } +// +// public func doSignMessage(_ signatures: [String], ownerAccount: Account) async throws -> PreparedSign { +// guard let smartAccountsManager = smartAccountsManager else { +// throw Errors.smartAccountNotEnabled +// } +// let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) +// let signature = try await client.doSignMessage(signatures) +// return signature +// } +// +// public func finalizeSignMessage(_ signatures: [String], signStep3Params: String, ownerAccount: Account) async throws -> String { +// guard let smartAccountsManager = smartAccountsManager else { +// throw Errors.smartAccountNotEnabled +// } +// let client = smartAccountsManager.getOrCreateSafe(for: ownerAccount) +// let signature = try await client.finalizeSignMessage(signatures, signStep3Params: signStep3Params) +// return signature +// } public func status(orchestrationId: String) async throws -> StatusResponse { guard let chainAbstractionClient = chainAbstractionClient else { @@ -344,7 +344,7 @@ public class WalletKitClient { return try await chainAbstractionClient.status(orchestrationId: orchestrationId) } - public func route(transaction: EthTransaction) async throws -> RouteResponseSuccess { + public func route(transaction: InitTransaction) async throws -> RouteResponse { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } @@ -360,13 +360,13 @@ public class WalletKitClient { return try await chainAbstractionClient.estimateFees(chainId: chainId) } - public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { - guard let chainClient = chainAbstractionClient else { - throw Errors.chainAbstractionNotEnabled - } - - return try await chainClient.waitForSuccess(orchestrationId: orchestrationId, checkIn: checkIn) - } +// public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { +// guard let chainClient = chainAbstractionClient else { +// throw Errors.chainAbstractionNotEnabled +// } +// +// return try await chainClient.waitForSuccess(orchestrationId: orchestrationId, checkIn: checkIn) +// } } diff --git a/Sources/ReownWalletKit/WalletKitDecryptionService.swift b/Sources/ReownWalletKit/WalletKitDecryptionService.swift index 3bba0510c..18873040a 100644 --- a/Sources/ReownWalletKit/WalletKitDecryptionService.swift +++ b/Sources/ReownWalletKit/WalletKitDecryptionService.swift @@ -1,7 +1,7 @@ import Foundation public final class WalletKitDecryptionService { - enum Errors: Error { + enum Errors: LocalizedError { case unknownTag } public enum RequestMethod: UInt { diff --git a/Sources/YttriumWrapper/YttriumWrapper.swift b/Sources/YttriumWrapper/YttriumWrapper.swift index b37426a43..d7d761cd4 100644 --- a/Sources/YttriumWrapper/YttriumWrapper.swift +++ b/Sources/YttriumWrapper/YttriumWrapper.swift @@ -1,7 +1,3 @@ import Foundation -#if YTTRIUM_DEBUG -@_exported import YttriumCore -#else @_exported import Yttrium -#endif From 8035448d14c41a57e1e15f510fb158c1e5e8b70f Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 28 Nov 2024 08:52:46 +0100 Subject: [PATCH 55/73] savepoint --- Example/Shared/Signer/Signer.swift | 124 ++++++++++-------- .../ChainAbstractionService.swift | 4 +- .../BusinessLayer/WalletKitEnabler.swift | 2 +- .../CATransactionPresenter.swift | 4 +- .../Wallet/Main/MainPresenter.swift | 17 ++- Sources/ReownWalletKit/WalletKitClient.swift | 1 + 6 files changed, 83 insertions(+), 69 deletions(-) diff --git a/Example/Shared/Signer/Signer.swift b/Example/Shared/Signer/Signer.swift index 91a7cb2fc..8cc2eb627 100644 --- a/Example/Shared/Signer/Signer.swift +++ b/Example/Shared/Signer/Signer.swift @@ -19,7 +19,7 @@ struct SendCallsParams: Codable { final class Signer { - enum Errors: Error { + enum Errors: LocalizedError { case notImplemented case accountForRequestNotFound case cantFindRequestedAddress @@ -95,56 +95,57 @@ final class Signer { switch request.method { case "personal_sign": - 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) - } + 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) @@ -154,7 +155,12 @@ final class Signer { // return AnyCodable(signedMessage) case "eth_sendTransaction": - let params = try request.params.get([YttriumWrapper.Transaction].self) + struct Tx: Codable { + var to: String + var value: String + var data: String + } + let params = try request.params.get([Tx].self).map { FfiTransaction(to: $0.to, value: $0.value, data: $0.data)} let prepareSendTransactions = try await WalletKit.instance.prepareSendTransactions(params, ownerAccount: ownerAccount) let signer = ETHSigner(importAccount: importAccount) @@ -173,7 +179,7 @@ final class Signer { } let transactions = calls.map { - YttriumWrapper.Transaction( + FfiTransaction( to: $0.to!, value: $0.value ?? "0", data: $0.data ?? "" @@ -193,7 +199,7 @@ final class Signer { Task { do { let receipt = try await WalletKit.instance.waitForUserOperationReceipt(userOperationHash: userOpHash, ownerAccount: ownerAccount) - let message = "User Op receipt received, transaction hash: \(receipt.receipt.transactionHash)" + let message = "User Op receipt received" AlertPresenter.present(message: message, type: .success) } catch { AlertPresenter.present(message: error.localizedDescription, type: .error) @@ -207,8 +213,8 @@ final class Signer { } } - private static func prepareMessageToSign(_ messageToSign: String) -> Bytes { - let dataToSign: Bytes + 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)) @@ -223,7 +229,7 @@ final class Signer { return dataToSign } - private static func dataToHash(_ data: Data) -> Bytes { + 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 @@ -232,7 +238,7 @@ final class Signer { } -extension Signer.Errors: LocalizedError { +extension Signer.Errors { var errorDescription: String? { switch self { case .notImplemented: return "Requested method is not implemented" @@ -241,3 +247,5 @@ extension Signer.Errors: LocalizedError { } } } + + diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 3d05ec5e6..84e7b24b7 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -3,7 +3,7 @@ import Foundation import Web3 class ChainAbstractionService { - enum NetworkError: Error { + enum NetworkError: LocalizedError { case invalidURL case invalidResponse case invalidData @@ -97,7 +97,7 @@ class ChainAbstractionService { extension EthereumTransaction { - init(routingTransaction: RoutingTransaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { + init(routingTransaction: Transaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { self.init( nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 16)!), diff --git a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift index b90e591bb..46c5d60f4 100644 --- a/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift +++ b/Example/WalletApp/BusinessLayer/WalletKitEnabler.swift @@ -2,7 +2,7 @@ import Foundation import ReownWalletKit class WalletKitEnabler { - enum Errors: Error { + enum Errors: LocalizedError { case smartAccountNotEnabled } diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 24ab45487..e0087aeaf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -4,7 +4,7 @@ import Web3 import ReownWalletKit final class CATransactionPresenter: ObservableObject { - enum Errors: Error { + enum Errors: LocalizedError { case invalidURL case invalidResponse case invalidData @@ -329,7 +329,7 @@ extension CATransactionPresenter { print("Starting test async operation that will fail...") try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay - enum TestError: Error, LocalizedError { + enum TestError: LocalizedError { case sampleError var errorDescription: String? { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 8c6860790..98ed20e7a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -97,7 +97,7 @@ extension MainPresenter { do { let tx = try request.params.get([Tx].self)[0] - let transaction = EthTransaction( + let transaction = InitTransaction( from: tx.from, to: tx.to, value: "0", @@ -117,11 +117,16 @@ extension MainPresenter { await MainActor.run { switch routeResponseSuccess { - case .available(let routeResponseAvailable): - router.presentCATransaction(sessionRequest: request, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable, context: context) - case .notRequired(let routeResponseNotRequired): - AlertPresenter.present(message: "Routing not required", type: .success) - router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + case .success(let routeResponseSuccess): + switch routeResponseSuccess { + case .available(let routeResponseAvailable): + router.presentCATransaction(sessionRequest: request, importAccount: importAccount, routeResponseAvailable: routeResponseAvailable, context: context) + case .notRequired(let routeResponseNotRequired): + AlertPresenter.present(message: "Routing not required", type: .success) + router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) + } + case .error(let routeResponseError): + AlertPresenter.present(message: "Rout response error", type: .success) } } ActivityIndicatorManager.shared.stop() diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 31921c10f..2db218334 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -377,3 +377,4 @@ extension WalletKitClient { } } #endif + From 9355447ba2a39cc08e9945e22939dedbb1882267 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 29 Nov 2024 09:58:19 +0100 Subject: [PATCH 56/73] update package version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index f90fdb142..642076b45 100644 --- a/Package.swift +++ b/Package.swift @@ -30,7 +30,7 @@ func buildYttriumWrapperTarget() -> Target { swiftSettings: yttriumSwiftSettings ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.13"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.16"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From 791ab13bf677905777bc5280a2d34afe268382e0 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Mon, 2 Dec 2024 15:42:52 +0100 Subject: [PATCH 57/73] get transaction recepits --- .../xcshareddata/swiftpm/Package.resolved | 9 + .../ChainAbstractionService.swift | 154 ++++++++++++++++-- .../CATransactionPresenter.swift | 19 ++- Package.swift | 10 +- .../Client/Wallet/NotifyClient.swift | 2 +- 5 files changed, 171 insertions(+), 23 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e44740603..5414cfeff 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -270,6 +270,15 @@ "revision": "4232d34efa49f633ba61afde365d3896fc7f8740", "version": "2.15.0" } + }, + { + "package": "Yttrium", + "repositoryURL": "https://github.com/reown-com/yttrium", + "state": { + "branch": null, + "revision": "f6b33fe4ddbea6215cd8bc67cb42b105c3d12881", + "version": "0.2.22" + } } ] }, diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 84e7b24b7..fe1ef31af 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -3,10 +3,27 @@ import Foundation import Web3 class ChainAbstractionService { - enum NetworkError: LocalizedError { + enum Errors: LocalizedError { case invalidURL case invalidResponse case invalidData + case transactionFailed(String) // Includes additional context about the transaction failure + case receiptUnavailable(String) // Includes additional context when a receipt is not available + + var errorDescription: String? { + switch self { + case .invalidURL: + return "The provided URL is invalid." + case .invalidResponse: + return "The server responded with an invalid response." + case .invalidData: + return "The data received from the server is invalid." + case .transactionFailed(let reason): + return "Transaction failed: \(reason)" + case .receiptUnavailable(let transactionHash): + return "Transaction receipt is unavailable for transaction hash: \(transactionHash)." + } + } } let privateKey: EthereumPrivateKey @@ -23,8 +40,8 @@ class ChainAbstractionService { for tx in routeResponseAvailable.transactions { do { let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) - let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) - let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)! * 2) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)! * 2) print(maxFeePerGas) print(maxPriorityFeePerGas) @@ -33,6 +50,7 @@ class ChainAbstractionService { maxPriorityFeePerGas: maxPriorityFeePerGas, maxFeePerGas: maxFeePerGas ) + prettyPrintTransaction(transaction) let chain = Blockchain(tx.chainId)! let chainId = EthereumQuantity(quantity: BigUInt(chain.reference, radix: 10)!) @@ -49,18 +67,39 @@ class ChainAbstractionService { return signedTransactions } - func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws { + private func getRpcUrl(chainId: String) -> String { + let projectId = Networking.projectId + + return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + +// switch chainId { +// case "eip155:10": +// return "https://mainnet.optimism.io" +// case "eip155:8453": +// return "https://mainnet.base.org" +// case "eip155:42161": +// return "https://arbitrum.llamarpc.com" +// default: +// let projectId = Networking.projectId +// return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" +// } + } + + + func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws -> [(txHash: String, chainId: String)] { + var transactionResults: [(txHash: String, chainId: String)] = [] + for transaction in transactions { let chainId = transaction.chainId - let projectId = Networking.projectId - let rpcUrl = "rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + let rpcUrl = getRpcUrl(chainId: chainId) + // Build the raw transaction let rawTransaction = try transaction.transaction.rawTransaction() let rpcRequest = RPCRequest(method: "eth_sendRawTransaction", params: [rawTransaction]) // Create URL and request - guard let url = URL(string: "https://" + rpcUrl) else { - throw NetworkError.invalidURL + guard let url = URL(string: rpcUrl) else { + throw Errors.invalidURL } var request = URLRequest(url: url) @@ -77,21 +116,108 @@ class ChainAbstractionService { guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { - throw NetworkError.invalidResponse + throw Errors.invalidResponse } - // Parse response - let responseJSON = try JSONSerialization.jsonObject(with: data) - print("Transaction broadcast success: \(responseJSON)") + // Parse JSON response to extract the transaction hash + // Parse JSON response to extract the transaction hash + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + print("🔍 Full JSON response: \(json)") // Print the full JSON response + if let transactionHash = json["result"] as? String { + transactionResults.append((txHash: transactionHash, chainId: chainId)) + print("Transaction broadcast success: \(transactionHash) on chain \(chainId)") + } else { + throw Errors.invalidData + } + } else { + throw Errors.invalidData + } } catch { - print("Error broadcasting transaction: \(error)") + print("Error broadcasting transaction on chain \(chainId): \(error)") throw error } } + + return transactionResults } + func getTransactionReceipt(transactionHash: String, chainId: String, retries: Int = 10, delay: UInt64 = 2_000_000_000) async throws -> [String: Any] { + let rpcUrl = getRpcUrl(chainId: chainId) + + // Build the RPC request + let rpcRequest = RPCRequest(method: "eth_getTransactionReceipt", params: [transactionHash]) + guard let url = URL(string: rpcUrl) else { + throw Errors.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Convert RPC request to JSON data + let jsonData = try JSONEncoder().encode(rpcRequest) + request.httpBody = jsonData + + for attempt in 1...retries { + do { + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw Errors.invalidResponse + } + + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + print("🔍 Full JSON response: \(json)") // Print the full JSON response + if let result = json["result"] as? [String: Any] { + // Receipt is available + return result + } else { + // Receipt is still pending + print("⏳ Receipt not available for \(transactionHash) on chain \(chainId). Attempt \(attempt) of \(retries). Retrying...") + } + } else { + throw Errors.invalidData + } + } catch { + print("❌ Error fetching transaction receipt for \(transactionHash) on chain \(chainId): \(error)") + throw error + } + + if attempt < retries { + try await Task.sleep(nanoseconds: delay) + } + } + + // If no receipt is returned after retries + throw Errors.invalidData + } + + private func prettyPrintTransaction(_ transaction: EthereumTransaction) { + func formatQuantity(_ quantity: EthereumQuantity?) -> String { + guard let quantity = quantity else { return "nil" } + return String(quantity.quantity) + } + + print(""" + Ethereum Transaction: + ---------------------- + Nonce: \(formatQuantity(transaction.nonce)) + Gas Price: \(formatQuantity(transaction.gasPrice)) + Max Fee Per Gas: \(formatQuantity(transaction.maxFeePerGas)) + Max Priority Fee Per Gas: \(formatQuantity(transaction.maxPriorityFeePerGas)) + Gas Limit: \(formatQuantity(transaction.gasLimit)) + From: \(transaction.from?.hex(eip55: true) ?? "nil") + To: \(transaction.to?.hex(eip55: true) ?? "nil") + Value: \(formatQuantity(transaction.value)) + Data: \(transaction.data.bytes.map { String(format: "%02x", $0) }.joined()) + Access List: \(transaction.accessList) + Transaction Type: \(transaction.transactionType) + ---------------------- + """) + } } @@ -125,3 +251,5 @@ fileprivate extension String { return hasPrefix("0x") ? String(dropFirst(2)) : self } } + + diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index e0087aeaf..b49ddd93a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -63,7 +63,20 @@ final class CATransactionPresenter: ObservableObject { print("✅ Successfully signed transactions. First transaction: \(signedTransactions[0])") print("📡 Broadcasting signed transactions...") - try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) + let txResults = try await chainAbstractionService.broadcastTransactions(transactions: signedTransactions) + + print("🧾 Fetching transaction receipts...") + for (txHash, chainId) in txResults { + do { + let receipt = try await chainAbstractionService.getTransactionReceipt(transactionHash: txHash, chainId: chainId) + print("✅ Transaction receipt for \(txHash) on chain \(chainId): \(receipt)") + + } catch { + print("❌ Failed to fetch receipt for \(txHash) on chain \(chainId): \(error)") + throw error + } + } + let orchestrationId = routeResponseAvailable.orchestrationId print("📋 Orchestration ID: \(orchestrationId)") @@ -82,13 +95,13 @@ final class CATransactionPresenter: ObservableObject { case .completed(let completed): print("✅ Transaction completed successfully!") print("📊 Completion details: \(completed)") - AlertPresenter.present(message: "routing transactions completed", type: .success) + AlertPresenter.present(message: "Routing transactions completed", type: .success) break loop case .error(let error): print("❌ Transaction failed with error!") print("💥 Error details: \(error)") - AlertPresenter.present(message: "routing failed with error: \(error)", type: .error) + AlertPresenter.present(message: "Routing failed with error: \(error)", type: .error) ActivityIndicatorManager.shared.stop() return } diff --git a/Package.swift b/Package.swift index 642076b45..3f6b91d72 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,8 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = true +let yttriumDebug = false + // Define dependencies array @@ -20,17 +21,14 @@ let yttriumTarget = buildYttriumWrapperTarget() func buildYttriumWrapperTarget() -> Target { // Conditionally add Yttrium dependency if yttriumDebug { - var yttriumSwiftSettings: [SwiftSetting] = [] dependencies.append(.package(path: "../yttrium")) - yttriumSwiftSettings.append(.define("YTTRIUM_DEBUG")) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], - path: "Sources/YttriumWrapper", - swiftSettings: yttriumSwiftSettings + path: "Sources/YttriumWrapper" ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.16"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.22"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index 7ff218e84..c1a3c612d 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -192,7 +192,7 @@ extension NotifyClient { } public func register(deviceToken: String) async throws { - try await pushClient.register(deviceToken: deviceToken) +// try await pushClient.register(deviceToken: deviceToken) } } #endif From 1405952fd6b987e575b9873eae4e31f66d0ac4d2 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 3 Dec 2024 09:05:52 +0100 Subject: [PATCH 58/73] revert 6492 --- .../Verifier/MessageVerifier.swift | 105 +++++------------- .../Verifier/MessageVerifierFactory.swift | 3 +- 2 files changed, 31 insertions(+), 77 deletions(-) diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift index 3d3ba9196..586077972 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifier.swift @@ -1,49 +1,17 @@ import Foundation -import YttriumWrapper public struct MessageVerifier { - enum Errors: LocalizedError { + enum Errors: Error { case utf8EncodingFailed - case invalidSignature(message: String) - case invalidAddress(message: String) - case invalidMessageHash(message: String) - case verificationFailed(message: String) - case custom(message: String) - - var errorDescription: String? { - switch self { - case .utf8EncodingFailed: - return "Failed to encode string using UTF-8." - case .invalidSignature(let message): - return "Invalid signature: \(message)" - case .invalidAddress(let message): - return "Invalid address: \(message)" - case .invalidMessageHash(let message): - return "Invalid message hash: \(message)" - case .verificationFailed(let message): - return "Verification failed: \(message)" - case .custom(let message): - return message - } - } } private let eip191Verifier: EIP191Verifier private let eip1271Verifier: EIP1271Verifier - private let crypto: CryptoProvider - private let projectId: String - init( - eip191Verifier: EIP191Verifier, - eip1271Verifier: EIP1271Verifier, - crypto: CryptoProvider, - projectId: String - ) { + init(eip191Verifier: EIP191Verifier, eip1271Verifier: EIP1271Verifier) { self.eip191Verifier = eip191Verifier self.eip1271Verifier = eip1271Verifier - self.crypto = crypto - self.projectId = projectId } public func verify(signature: CacaoSignature, @@ -63,7 +31,28 @@ public struct MessageVerifier { address: String, chainId: String ) async throws { - try await verifySignature(signature.s, message: message, address: address, chainId: chainId) + + guard let messageData = message.data(using: .utf8) else { + throw Errors.utf8EncodingFailed + } + + let signatureData = Data(hex: signature.s) + + switch signature.t { + case .eip191: + return try await eip191Verifier.verify( + signature: signatureData, + message: messageData.prefixed, + address: address + ) + case .eip1271: + return try await eip1271Verifier.verify( + signature: signatureData, + message: messageData.prefixed, + address: address, + chainId: chainId + ) + } } public func verify(signature: String, @@ -96,47 +85,13 @@ public struct MessageVerifier { ) return // If 191 verification succeeds, we’re done } catch { - // If eip191 verification fails, we’ll attempt 6492 verification - } - - // Fallback to 6492 verification - print("i was called only once") - let rpcUrl = "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" - let erc6492Client = Erc6492Client(rpcUrl.intoRustString()) - let messageHash = crypto.keccak256(prefixedMessage) - - do { - let result = try await erc6492Client.verify_signature( - signatureString.intoRustString(), - address.intoRustString(), - messageHash.toHexString().intoRustString() + // If eip191 verification fails, try eip1271 verification + try await eip1271Verifier.verify( + signature: signatureData, + message: prefixedMessage, + address: address, + chainId: chainId ) - - if result == true { - return - } else { - throw Errors.verificationFailed(message: "Signature verification failed.") - } - } catch let ffiError as Erc6492Error { - switch ffiError { - case .InvalidSignature(let x): - let errorMessage = x.toString() - throw Errors.invalidSignature(message: errorMessage) - case .InvalidAddress(let x): - let errorMessage = x.toString() - throw Errors.invalidAddress(message: errorMessage) - case .InvalidMessageHash(let x): - let errorMessage = x.toString() - throw Errors.invalidMessageHash(message: errorMessage) - case .Verification(let x): - let errorMessage = x.toString() - throw Errors.verificationFailed(message: errorMessage) - default: - let errorMessage = "An unknown error occurred." - throw Errors.custom(message: errorMessage) - } - } catch { - throw error } } } diff --git a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift index e67953cbb..2dcc4255c 100644 --- a/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift +++ b/Sources/WalletConnectSigner/Verifier/MessageVerifierFactory.swift @@ -13,7 +13,6 @@ public struct MessageVerifierFactory { } public func create(projectId: String) -> MessageVerifier { - - return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto), crypto: crypto, projectId: projectId) + return MessageVerifier(eip191Verifier: EIP191Verifier(crypto: crypto), eip1271Verifier: EIP1271Verifier(projectId: projectId, httpClient: HTTPNetworkClient(host: "rpc.walletconnect.com"), crypto: crypto)) } } From 6acf9ce38e33aef78fdcd1fa8ac04c0a5c4a0323 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 3 Dec 2024 14:35:20 +0100 Subject: [PATCH 59/73] savepoint --- .../ChainAbstractionService.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index fe1ef31af..2cc22e06f 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -68,21 +68,21 @@ class ChainAbstractionService { } private func getRpcUrl(chainId: String) -> String { - let projectId = Networking.projectId - - return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" - -// switch chainId { -// case "eip155:10": -// return "https://mainnet.optimism.io" -// case "eip155:8453": -// return "https://mainnet.base.org" -// case "eip155:42161": -// return "https://arbitrum.llamarpc.com" -// default: -// let projectId = Networking.projectId -// return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" -// } +// let projectId = Networking.projectId +// +// return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + + switch chainId { + case "eip155:10": + return "https://mainnet.optimism.io" + case "eip155:8453": + return "https://mainnet.base.org" + case "eip155:42161": + return "https://arbitrum.llamarpc.com" + default: + let projectId = Networking.projectId + return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + } } From b2c02789ebddc692b75ba919a57c0914fff590f7 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 5 Dec 2024 08:36:48 +0100 Subject: [PATCH 60/73] fix nonce bug --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- .../WalletApp/BusinessLayer/ChainAbstractionService.swift | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5414cfeff..bdedb04a3 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/attaswift/BigInt.git", "state": { "branch": null, - "revision": "a7ee11486233ba45f5ceee0b8cb3d6629ed450ef", - "version": "5.5.0" + "revision": "114343a705df4725dfe7ab8a2a326b8883cfd79c", + "version": "5.5.1" } }, { diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 2cc22e06f..f34c6a180 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -59,6 +59,7 @@ class ChainAbstractionService { let signedTransaction = try transaction.sign(with: privateKey, chainId: chainId) print(signedTransaction.value) + print("nonce: \(signedTransaction.nonce)") signedTransactions.append((transaction: signedTransaction, chainId: chain.absoluteString)) } catch { print("Error processing transaction: \(error)") @@ -89,6 +90,7 @@ class ChainAbstractionService { func broadcastTransactions(transactions: [(transaction: EthereumSignedTransaction, chainId: String)]) async throws -> [(txHash: String, chainId: String)] { var transactionResults: [(txHash: String, chainId: String)] = [] + // do it in series for transaction in transactions { let chainId = transaction.chainId let rpcUrl = getRpcUrl(chainId: chainId) @@ -226,11 +228,11 @@ extension EthereumTransaction { init(routingTransaction: Transaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { self.init( - nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 16)!), + nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 10)!), gasPrice: nil, // Not needed for EIP1559 maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 16)!), + gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 10)!), from: try EthereumAddress(hex: routingTransaction.from, eip55: false), to: try EthereumAddress(hex: routingTransaction.to, eip55: false), value: EthereumQuantity(quantity: 0.gwei), From fb9bf83ab568c913ee30737b56e5abe638ea2a0f Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 10 Dec 2024 19:34:16 +0300 Subject: [PATCH 61/73] savepoint --- .../xcshareddata/swiftpm/Package.resolved | 9 ---- .../CATransactionPresenter.swift | 50 +++++++++++++++++-- .../CATransactionView.swift | 3 +- Package.swift | 2 +- Sources/ReownWalletKit/WalletKitClient.swift | 7 +++ 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bdedb04a3..43ef23fcc 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -270,15 +270,6 @@ "revision": "4232d34efa49f633ba61afde365d3896fc7f8740", "version": "2.15.0" } - }, - { - "package": "Yttrium", - "repositoryURL": "https://github.com/reown-com/yttrium", - "state": { - "branch": null, - "revision": "f6b33fe4ddbea6215cd8bc67cb42b105c3d12881", - "version": "0.2.22" - } } ] }, diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index b49ddd93a..3530051cf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -10,13 +10,13 @@ final class CATransactionPresenter: ObservableObject { case invalidData } // Published properties to be used in the view - @Published var payingAmount: Double = 10.00 + @Published var payingAmount: String = "" @Published var balanceAmount: Double = 5.00 @Published var bridgingAmount: Double = 5.00 @Published var appURL: String = "" @Published var networkName: String = "" - @Published var estimatedFees: Double = 4.34 - @Published var bridgeFee: Double = 3.00 + @Published var estimatedFees: String = "" + @Published var bridgeFee: String = "" @Published var purchaseFee: Double = 1.34 @Published var executionSpeed: String = "Fast (~20 sec)" @Published var transactionCompleted: Bool = false @@ -30,6 +30,7 @@ final class CATransactionPresenter: ObservableObject { } let router: CATransactionRouter let importAccount: ImportAccount + var routeUiFields: RouteUiFields? = nil private var disposeBag = Set() @@ -294,6 +295,49 @@ final class CATransactionPresenter: ObservableObject { self.appURL = session.peer.url } networkName = network(for: sessionRequest.chainId.absoluteString) + Task { try await setUpRoutUiFields() } + } + + + + func setUpRoutUiFields() async throws { + struct Tx: Codable { + let data: String + let from: String + let to: String + } + let tx = try! sessionRequest.params.get([Tx].self)[0] + + let estimates = try await WalletKit.instance.estimateFees(chainId: sessionRequest.chainId.absoluteString) + + let initTx = Transaction( + from: tx.from, + to: tx.to, + value: "0", + gas: "0", + data: tx.data, + nonce: "0x", + chainId: sessionRequest.chainId.absoluteString, + gasPrice: "0", + maxFeePerGas: estimates.maxFeePerGas, + maxPriorityFeePerGas: estimates.maxPriorityFeePerGas + ) + let routUiFields = try await WalletKit.instance.getRouteUiFieds(routeResponse: routeResponseAvailable, initialTransaction: initTx, currency: Currency.usd) + print("aaaaaaaa") + + print(routUiFields.localTotal) + print("bbbbbb") + + print(routUiFields.localTotal.formatted) + print("XXXXXXXXX") + print(routUiFields.localTotal.formattedAlt) + + await MainActor.run { + payingAmount = routUiFields.localTotal.amount + estimatedFees = routUiFields.localTotal.formattedAlt + bridgeFee = routUiFields.bridge.first!.localFee.formattedAlt + } + } func onViewOnExplorer() { diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index c7daeee19..fa16f5820 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -85,8 +85,7 @@ struct CATransactionView: View { Text("Estimated Fees") .foregroundColor(.gray) Spacer() - Text("$TODO") - // Text("$\(presenter.estimatedFees, specifier: "%.2f")") + Text("\(presenter.estimatedFees)") .font(.system(.body, design: .monospaced)) } diff --git a/Package.swift b/Package.swift index 3f6b91d72..9731a7ff3 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = false +let yttriumDebug = true diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 2db218334..7b169563f 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -360,6 +360,13 @@ public class WalletKitClient { return try await chainAbstractionClient.estimateFees(chainId: chainId) } + public func getRouteUiFieds(routeResponse: RouteResponseAvailable, initialTransaction: Transaction, currency: Currency) async throws -> RouteUiFields { + guard let chainAbstractionClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + return try await chainAbstractionClient.getRouteUiFields(routeResponse: routeResponse, initialTransaction: initialTransaction, currency: currency) + } + // public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { // guard let chainClient = chainAbstractionClient else { // throw Errors.chainAbstractionNotEnabled From de9684d1942ec9f1dcb3bb91a47414bb3f12bceb Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 12 Dec 2024 16:24:02 +0800 Subject: [PATCH 62/73] update ui --- .../Arbitrum.imageset/Arbitrum.png | Bin 0 -> 5191 bytes .../Arbitrum.imageset/Contents.json | 21 ++ .../Assets.xcassets/Base.imageset/Base.pdf | Bin 0 -> 177019 bytes .../Base.imageset/Contents.json | 12 + .../Optimism.imageset/Contents.json | 21 ++ .../Optimism.imageset/Optimism.png | Bin 0 -> 3233 bytes .../bridging.imageset/Contents.json | 21 ++ .../bridging.imageset/Frame 48096539 (2).png | Bin 0 -> 2152 bytes .../grey-section.colorset/Contents.json | 38 +++ .../grey-subsection.colorset/Contents.json | 38 +++ .../CATransactionPresenter.swift | 68 ++---- .../CATransactionView.swift | 222 +++++++++--------- .../Wallet/Main/MainPresenter.swift | 12 +- Sources/ReownWalletKit/WalletKitClient.swift | 7 + 14 files changed, 293 insertions(+), 167 deletions(-) create mode 100644 Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Arbitrum.png create mode 100644 Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Contents.json create mode 100644 Example/WalletApp/Other/Assets.xcassets/Base.imageset/Base.pdf create mode 100644 Example/WalletApp/Other/Assets.xcassets/Base.imageset/Contents.json create mode 100644 Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Contents.json create mode 100644 Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Optimism.png create mode 100644 Example/WalletApp/Other/Assets.xcassets/bridging.imageset/Contents.json create mode 100644 Example/WalletApp/Other/Assets.xcassets/bridging.imageset/Frame 48096539 (2).png create mode 100644 Example/WalletApp/Other/Colors.xcassets/grey-section.colorset/Contents.json create mode 100644 Example/WalletApp/Other/Colors.xcassets/grey-subsection.colorset/Contents.json diff --git a/Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Arbitrum.png b/Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Arbitrum.png new file mode 100644 index 0000000000000000000000000000000000000000..563cd36b2906126c2dd39896aaa3bda642f1b653 GIT binary patch literal 5191 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 B1WNz_ literal 0 HcmV?d00001 diff --git a/Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Contents.json b/Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Contents.json new file mode 100644 index 000000000..1c8b964c7 --- /dev/null +++ b/Example/WalletApp/Other/Assets.xcassets/Arbitrum.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Arbitrum.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/WalletApp/Other/Assets.xcassets/Base.imageset/Base.pdf b/Example/WalletApp/Other/Assets.xcassets/Base.imageset/Base.pdf new file mode 100644 index 0000000000000000000000000000000000000000..235444d56e8e1196432305af3cf8ea24d6ce0b64 GIT binary patch literal 177019 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�dNGnOB29U<9#I$EJB#u*o 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 literal 0 HcmV?d00001 diff --git a/Example/WalletApp/Other/Assets.xcassets/Base.imageset/Contents.json b/Example/WalletApp/Other/Assets.xcassets/Base.imageset/Contents.json new file mode 100644 index 000000000..b79f3dbcd --- /dev/null +++ b/Example/WalletApp/Other/Assets.xcassets/Base.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Base.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Contents.json b/Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Contents.json new file mode 100644 index 000000000..f700a6d72 --- /dev/null +++ b/Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Optimism.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Optimism.png b/Example/WalletApp/Other/Assets.xcassets/Optimism.imageset/Optimism.png new file mode 100644 index 0000000000000000000000000000000000000000..05cef28ad72462b8742c3395b886e400899fcdb0 GIT binary patch literal 3233 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 etER=^&HM+m%dHy)=|Qmo0000() @@ -46,6 +51,8 @@ final class CATransactionPresenter: ObservableObject { self.chainAbstractionService = ChainAbstractionService(privateKey: prvKey, routeResponseAvailable: routeResponseAvailable) self.router = router self.importAccount = importAccount + self.networkName = network(for: sessionRequest.chainId.absoluteString) + self.fundingFromNetwork = network(for: fundingFrom[0].chainId) // Any additional setup for the parameters setupInitialState() } @@ -123,11 +130,6 @@ final class CATransactionPresenter: ObservableObject { } private func sendInitialTransaction() async throws { - struct Tx: Codable { - let data: String - let from: String - let to: String - } print("📝 Preparing initial transaction...") let tx = try! sessionRequest.params.get([Tx].self)[0] @@ -296,6 +298,16 @@ final class CATransactionPresenter: ObservableObject { } networkName = network(for: sessionRequest.chainId.absoluteString) Task { try await setUpRoutUiFields() } + payingAmount = initialTransactionMetadata.amount + + let tx = try! sessionRequest.params.get([Tx].self)[0] + + Task { + let balance = try await WalletKit.instance.erc20Balance(chainId: chainId, token: tx.to, owner: importAccount.account.address) + await MainActor.run { + balanceAmount = balance + } + } } @@ -308,7 +320,7 @@ final class CATransactionPresenter: ObservableObject { } let tx = try! sessionRequest.params.get([Tx].self)[0] - let estimates = try await WalletKit.instance.estimateFees(chainId: sessionRequest.chainId.absoluteString) + let estimates = try await WalletKit.instance.estimateFees(chainId: chainId) let initTx = Transaction( from: tx.from, @@ -333,7 +345,7 @@ final class CATransactionPresenter: ObservableObject { print(routUiFields.localTotal.formattedAlt) await MainActor.run { - payingAmount = routUiFields.localTotal.amount + estimatedFees = routUiFields.localTotal.formattedAlt bridgeFee = routUiFields.bridge.first!.localFee.formattedAlt } @@ -367,37 +379,3 @@ final class CATransactionPresenter: ObservableObject { // MARK: - SceneViewModel extension CATransactionPresenter: SceneViewModel {} - - - -extension CATransactionPresenter { - // Test function that succeeds after delay - func testAsyncSuccess() async throws { - print("Starting test async operation...") - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay - print("Test async operation completed successfully") - DispatchQueue.main.async { [weak self] in - self?.transactionCompleted = true - } - } - - // Test function that throws after delay - func testAsyncError() async throws { - print("Starting test async operation that will fail...") - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second delay - - enum TestError: LocalizedError { - case sampleError - - var errorDescription: String? { - return "This is a test error" - } - - var failureReason: String? { - return "The operation failed because this is a test of error handling" - } - } - - throw TestError.sampleError - } -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift index fa16f5820..a326f75b3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionView.swift @@ -10,10 +10,10 @@ struct CATransactionView: View { ZStack { if presenter.transactionCompleted { TransactionCompletedView() - .scaleEffect(viewScale) // Apply the renamed property here + .scaleEffect(viewScale) .onAppear { withAnimation(.easeInOut(duration: 0.4)) { - viewScale = 1.0 // Reset scaling for the new view + viewScale = 1.0 } } } else { @@ -23,151 +23,144 @@ struct CATransactionView: View { .font(.headline) .padding(.top) + // FIRST SECTION (Darker background) VStack(spacing: 20) { - // Paying Section - VStack(alignment: .leading, spacing: 4) { + // Paying Row + HStack { Text("Paying") .foregroundColor(.gray) - Text("$TODO") - // Text("\(presenter.payingAmount, specifier: "%.2f") USDC") + Spacer() + // Replace with the actual amount from presenter + Text("\(presenter.hexAmountToDenominatedUSDC(presenter.payingAmount)) USDC") .font(.system(.body, design: .monospaced)) } - .frame(maxWidth: .infinity, alignment: .leading) - // Source of funds - VStack(alignment: .leading, spacing: 12) { - Text("Source of funds") - .foregroundColor(.gray) + // Source of Funds Title + // Subsection (lighter background) for Source of Funds details + VStack(spacing: 4) { + VStack(alignment: .leading, spacing: 4) { + Text("Source of funds") + .foregroundColor(.gray) + } + .frame(maxWidth: .infinity, alignment: .leading) + // Balance line + HStack { + Image(presenter.networkName) + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundColor(.gray) + Text("Balance") + .foregroundColor(.gray) + Spacer() + Text("\(presenter.hexAmountToDenominatedUSDC(presenter.balanceAmount)) USDC") + .font(.system(.body, design: .monospaced)) + } + + // Bridging line(s) ForEach(presenter.fundingFrom, id: \.chainId) { funding in HStack { - Spacer() // Push content to the right - Image(systemName: "arrow.left.arrow.right.circle.fill") + Image("bridging") + .resizable() + .scaledToFit() + .frame(width: 32, height: 32) + Text("Bridging") .foregroundColor(.gray) - VStack(alignment: .leading) { - // Use the presenter for conversion + Spacer() + VStack(alignment: .trailing) { Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") .font(.system(.body, design: .monospaced)) - Text("from \(presenter.network(for: funding.chainId))") - .font(.footnote) - .foregroundColor(.gray) + HStack{ + Text("from \(presenter.network(for: funding.chainId))") + .font(.footnote) + .foregroundColor(.gray) + Image(presenter.network(for: funding.chainId)) + .resizable() + .scaledToFit() + .frame(width: 12, height: 12) + .clipShape(Circle()) + } } } - .frame(maxWidth: .infinity, alignment: .trailing) // Ensure the entire HStack aligns to the trailing edge } } + .padding() + .background(Color("grey-subsection")) + .cornerRadius(12) + } + .padding() + .background(Color("grey-section")) + .cornerRadius(12) - // App and Network - VStack(spacing: 12) { - HStack { - Text("App") - .foregroundColor(.gray) - Spacer() - Text(presenter.appURL) + // SECOND SECTION (Darker background) + VStack(spacing: 20) { + // App + HStack { + Text("App") + .foregroundColor(.gray) + Spacer() + Text(presenter.appURL) + .foregroundColor(.blue) + } + + // Network + HStack { + Text("Network") + .foregroundColor(.gray) + Spacer() + HStack(spacing: 4) { + Image(presenter.networkName) + .resizable() // Ensure the image scales + .scaledToFit() + .frame(width: 32, height: 32) .foregroundColor(.blue) + Text(presenter.networkName) } + } + // Estimated Fees Title + + // Subsection (lighter background) for fees breakdown + VStack(spacing: 8) { HStack { - Text("Network") + Text("Estimated Fees") .foregroundColor(.gray) Spacer() - HStack(spacing: 4) { - Image(systemName: "network") - .foregroundColor(.blue) - Text(presenter.networkName) - } + Text("\(presenter.estimatedFees)") + .font(.system(.body, design: .monospaced)) } - } - // Estimated Fees Section - VStack(spacing: 12) { + HStack { - Text("Estimated Fees") + Text("Bridge") .foregroundColor(.gray) Spacer() - Text("\(presenter.estimatedFees)") + // Replace "$TODO" with actual fee if available + Text("\(presenter.bridgeFee)") .font(.system(.body, design: .monospaced)) } - VStack(spacing: 8) { - HStack { - Text("Bridge") - .foregroundColor(.gray) - Spacer() - Text("$TODO") - // Text("$\(presenter.bridgeFee, specifier: "%.2f")") - .font(.system(.body, design: .monospaced)) - } - - HStack { - Text("Purchase") - .foregroundColor(.gray) - Spacer() - // Text("$\(presenter.purchaseFee, specifier: "%.2f")") - Text("$TODO") - .font(.system(.body, design: .monospaced)) - } - - HStack { - Text("Execution") - .foregroundColor(.gray) - Spacer() - Text(presenter.executionSpeed) - .font(.system(.body, design: .monospaced)) - } + HStack { + Text("Execution") + .foregroundColor(.gray) + Spacer() + Text(presenter.executionSpeed) + .font(.system(.body, design: .monospaced)) } - .padding(.leading) } .padding() - .background(Color(.systemGray6)) + .background(Color("grey-subsection")) .cornerRadius(12) } - .padding(.horizontal) + .padding() + .background(Color("grey-section")) + .cornerRadius(12) Spacer() // Action Buttons VStack(spacing: 12) { -// AsyncButton( -// options: [ -// .showProgressViewOnLoading, -// .disableButtonOnLoading, -// .showAlertOnError, -// .enableNotificationFeedback -// ] -// ) { -// try await presenter.testAsyncSuccess() -// } label: { -// Text("Test Success") -// .fontWeight(.semibold) -// .foregroundColor(.white) -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.green) -// .cornerRadius(12) -// } -// -// // Error test button -// AsyncButton( -// options: [ -// .showProgressViewOnLoading, -// .disableButtonOnLoading, -// .showAlertOnError, -// .enableNotificationFeedback -// ] -// ) { -// try await presenter.testAsyncError() -// } label: { -// Text("Test Error") -// .fontWeight(.semibold) -// .foregroundColor(.white) -// .frame(maxWidth: .infinity) -// .padding() -// .background(Color.red) -// .cornerRadius(12) -// } - - // Original Approve button AsyncButton( options: [ .showProgressViewOnLoading, @@ -209,7 +202,6 @@ struct CATransactionView: View { } } - struct TransactionCompletedView: View { @EnvironmentObject var presenter: CATransactionPresenter @@ -219,7 +211,7 @@ struct TransactionCompletedView: View { .font(.title2) .fontWeight(.semibold) - // Original tada image in blue-purple gradient circle + // Tada image in gradient circle ZStack { Circle() .fill( @@ -247,11 +239,11 @@ struct TransactionCompletedView: View { Text("Payed") .foregroundColor(.gray) Spacer() - Text("X USDC") - .font(.system(.body, design: .monospaced)) - Text("on ") - Text("\(presenter.networkName)") - .font(.system(.body, design: .monospaced)) + Text("\(presenter.payingAmount) USDC") + .font(.system(.body, design: .monospaced)) + Text("on") + Text("\(presenter.networkName)") + .font(.system(.body, design: .monospaced)) } HStack { @@ -261,7 +253,7 @@ struct TransactionCompletedView: View { ForEach(presenter.fundingFrom, id: \.chainId) { funding in Text("\(presenter.hexAmountToDenominatedUSDC(funding.amount)) \(funding.symbol)") .font(.system(.body, design: .monospaced)) - Text("from ") + Text("from") Text("\(presenter.network(for: funding.chainId))") .font(.system(.body, design: .monospaced)) } @@ -271,7 +263,6 @@ struct TransactionCompletedView: View { .background(Color(.systemGray6)) .cornerRadius(16) - // View on Explorer button Button(action: { presenter.onViewOnExplorer() }) { @@ -285,7 +276,6 @@ struct TransactionCompletedView: View { Spacer() - // Back to App button Button(action: { presenter.dismiss() }) { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 98ed20e7a..c2bc6b2d4 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -4,6 +4,12 @@ import SwiftUI import WalletConnectUtils import ReownWalletKit +struct Tx: Codable { + let data: String + let from: String + let to: String +} + final class MainPresenter { private let interactor: MainInteractor private let importAccount: ImportAccount @@ -85,12 +91,6 @@ extension MainPresenter { } private func tryRoutCATransaction(request: Request, context: VerifyContext?) async throws { - struct Tx: Codable { - let data: String - let from: String - let to: String - } - guard request.method == "eth_sendTransaction" else { return } diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 7b169563f..bf0a5cd6b 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -367,6 +367,13 @@ public class WalletKitClient { return try await chainAbstractionClient.getRouteUiFields(routeResponse: routeResponse, initialTransaction: initialTransaction, currency: currency) } + 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) + } + // public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { // guard let chainClient = chainAbstractionClient else { // throw Errors.chainAbstractionNotEnabled From a7f0cf2deabfb5266d1aad6b60f84ff85b95d0fc Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 12 Dec 2024 17:09:24 +0800 Subject: [PATCH 63/73] update ui --- .../ChainAbstractionService.swift | 4 +-- .../tada.imageset/Contents.json | 2 +- .../tada.imageset/Frame 48096502.png | Bin 0 -> 11982 bytes .../Assets.xcassets/tada.imageset/tada.png | Bin 3068 -> 0 bytes .../CATransactionPresenter.swift | 11 ++++++--- .../CATransactionView.swift | 23 ++++-------------- 6 files changed, 16 insertions(+), 24 deletions(-) create mode 100644 Example/WalletApp/Other/Assets.xcassets/tada.imageset/Frame 48096502.png delete mode 100644 Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index f34c6a180..49b6125dc 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -228,11 +228,11 @@ extension EthereumTransaction { init(routingTransaction: Transaction, maxPriorityFeePerGas: EthereumQuantity, maxFeePerGas: EthereumQuantity) throws { self.init( - nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 10)!), + nonce: EthereumQuantity(quantity: BigUInt(routingTransaction.nonce.stripHexPrefix(), radix: 16)!), gasPrice: nil, // Not needed for EIP1559 maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 10)!), + gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 16)!), from: try EthereumAddress(hex: routingTransaction.from, eip55: false), to: try EthereumAddress(hex: routingTransaction.to, eip55: false), value: EthereumQuantity(quantity: 0.gwei), diff --git a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json index e1308ded3..18bb0061f 100644 --- a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json +++ b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "tada.png", + "filename" : "Frame 48096502.png", "idiom" : "universal", "scale" : "1x" }, diff --git a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Frame 48096502.png b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/Frame 48096502.png new file mode 100644 index 0000000000000000000000000000000000000000..7531b3e713816fb3d4029f61f02dd9ab0d990b83 GIT binary patch literal 11982 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 literal 0 HcmV?d00001 diff --git a/Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png b/Example/WalletApp/Other/Assets.xcassets/tada.imageset/tada.png deleted file mode 100644 index c885e0e2a9b6bc2acddce46330b715a451c58b10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3068 zcmV(y*Ep?nPi4+ldw%#rAcI1jsh+W^`KP|&{_{xdk8&MTH7Kj)_O`w@SmQx=hSej z0{RD`+Dgxn8mh&h_AubmDociyZtG3Bd>e;P8bTAFux95|>3d9+4BmaEimo^5?T17yKi=8rElnE$ga2p6S3SF>3OgijI}Gv!=4+ zp&e*pr*N%dG%SbN{1`NPOWB82)3=W1vm++Y)!Y|SUT$GumsCqT(9W81y+BJ^kZt)b zbV<`I2L3pDQ~0bSGRJtL2>-kMNTKWT|1}t*dJ`4%KJSIj=@- zl3e|9aD(Z3vg)ISsI$O0W4l|MnmO;kd-7@rJRj^sJX}33?H$*|Ih0TXI-1@xg+kJ< z!JZEw|9tVW5+XhB=J`+vwon8baN2D@xKMKCvjOB41*J#iKVhseJ7YP1Hm6EGCknyF zzko5#7Tf9?Q{VP}4>j*5?k{$C`Z)9$tJ9Q*-V?G?4vH zNP+4CNw0spZ+ridXRP$1au{O{Q2-N$j<>QhY>+hql9TfgEu@CsplZ?DeqS58c&_;9 z>QVL?J)*=CD<#Z{un#YeX!W`YTM!yt`k& zjI@P1eQrwvt^Abq+td-2`67iTyimbi7bmk|`U~oPT~n1hxA}1;$VI8bYHK-;*2cqd zUDyvP^GtmhhNvzFADv`^W-w0Y&)xG!ErFw1;5G&)0A-MLF6Bc^nZd{_$v*k-?| zUSDkZV9<9oUiz1M#050(Syw%Tlw6ajht3qDwUk6*a>imB?eIve2N74bKn&k=AJ31X zIWJ;j-n|$Tw4Wn8r?ZJciXIb{y&3ctOfucB&QY(_NJ0?11t9b}i#>w5srWarOuTQv zdori4z`TdQKyKj^hzA53eeyOL+}bCMe6De>O1#edIX*u99K6BC@LVUtyj_j#x`hzA zpK05SoGUCjj{>=nvWlNXHXSj8)Sd~fK%|90mVot26|-|6f}$01$^97uT@7%YdL}G@ z?vYot8kKNl^i|EEX9e>%BWr4}09tF`N5iQ%)P82$dMZ#nfLK6;!Fh`i6@evyds>nO z>EyS)6t>p5!mg~H1n~LEjcO3XNrnXUnfraF09{X6xCLhGik|m1oqQe5U%ei7bBGaw z>fz8m^6|&a-0!PI8h!nJc6(<7zBs-CfnYxzW^Qm;fYDHNmJJYlUh$klJeve3m=W|NR&;^LD_IjN9>?zWO`tY0=(a#M^ z-8R8@=AVcs6v`7Mz1A(F6K=hj%e0hn#S6Jvcc^6asTM4pzfH}{hk_LHqqX61x7v$6 z0tk^Wc7OXfT;8_^lK(Q|iI8z7+^?CP+6w19cgZa$Pb3LBHVHL|6_YLZLeLw-#*AWi zeOf+jxBMB-Bfmr^jdTRY>;+`a?}u^HQV2=uJ%rnvj-lRb#b zHUbW%>x*oGj@Zd(XW!nVwviZKXUIq4l2=jl^)?u?=fgy#MIs!D?Y+&%;Jf62=luIn zf>9?rNxjD!Olw5YyZ}OSxQ3^auW-zBKYH6&&aC9`+8f)W4DuX5UHkU99kwXQ=D;J=I%s`dc%S7R5xq{_=hpv*yE`zZ7k))nvsF z#a4KTGdl7fU`$c=Meh5}@kj1BRdc3wv(+Cwi5m@E-CYZJ)C68LO^dYkTd|>X((YX2 z8r+EJ$mdxid#P_*Kk|~;S)Q6yT$`3uj2j6d<17U7SJJ4PEBdzgBVS2|k9_{tY~v3T zlIdM5w9fq_{a0SPvVFi3b){oYMQZZRhb99qo1Dbf4fcFc@-+Ijf@mz8nEN}3aZGr|5{xo8*TS9Lg}>BUZmu1y z&yc&Z_WygcC~)P?!XMCXdX0}A9QA9K92DKmH3SH)k!+`;Bb9Ml*QTkMxG!$=ySho z^EU6m^$K@UDGTJUIHA|EW$tqGup5tu9eECQ1Fv3KW-vcgkvVlc)95U?p1`eo^Mai$ zkoUmukrL+^b^d<)ikdP>ZYmA9-bAPS0IoH7%-&YG1yV}h*7n!TWuy6QRK<%$D{Aaa zBkbz*d}!@#ev_&%4H#>1SLQ2t0)FeGHG;r??0?1Vz$g)Q<%VzQ(?Tp&q{fY1E&o8! zwHIRnPC77o4FXfv@G9&!tsZ3~Rr4zm^FkE~!76ApWlE@VZK&-4q}Bsq3D5*yg=z05 zE$fU9EyFtZj=_VMks@xZbhb71R;m<~Dh#V7&!;BE^C_6$3Dy<8AUNSJs=jpPtw5J) zftv}+Zdnv7C{B;dPNjq_&law!FQMhnRZI|;%I#;&toZFN|$X^ zHa3w^tVGJBPzptD^e4C}hvw Date: Thu, 12 Dec 2024 20:09:33 +0800 Subject: [PATCH 64/73] saveopint --- .../Wallet/CATransactionModal/CATransactionPresenter.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 4378c5c2a..73dccfa10 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -314,15 +314,10 @@ final class CATransactionPresenter: ObservableObject { } } } - func setUpRoutUiFields() async throws { - struct Tx: Codable { - let data: String - let from: String - let to: String - } + let tx = try! sessionRequest.params.get([Tx].self)[0] let estimates = try await WalletKit.instance.estimateFees(chainId: chainId) From dced64f9d0519f34618f770dbbb2e3de8db63628 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 13 Dec 2024 15:15:58 +0800 Subject: [PATCH 65/73] mark ca methods as experimental --- Sources/ReownWalletKit/WalletKitClient.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index bf0a5cd6b..43b02e5b7 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -336,6 +336,7 @@ public class WalletKitClient { // return signature // } + @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 @@ -344,6 +345,7 @@ public class WalletKitClient { return try await chainAbstractionClient.status(orchestrationId: orchestrationId) } + @available(*, message: "This method is experimental. Use with caution.") public func route(transaction: InitTransaction) async throws -> RouteResponse { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled @@ -352,6 +354,7 @@ public class WalletKitClient { return try await chainAbstractionClient.route(transaction: transaction) } + @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 @@ -360,6 +363,7 @@ public class WalletKitClient { return try await chainAbstractionClient.estimateFees(chainId: chainId) } + @available(*, message: "This method is experimental. Use with caution.") public func getRouteUiFieds(routeResponse: RouteResponseAvailable, initialTransaction: Transaction, currency: Currency) async throws -> RouteUiFields { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled @@ -367,13 +371,13 @@ public class WalletKitClient { return try await chainAbstractionClient.getRouteUiFields(routeResponse: routeResponse, initialTransaction: initialTransaction, 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) } - // public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { // guard let chainClient = chainAbstractionClient else { // throw Errors.chainAbstractionNotEnabled From a4b8c36dd1667a9ac7e0066644946295dc1c79a4 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 13 Dec 2024 17:55:41 +0800 Subject: [PATCH 66/73] respond error on rout response error --- .../PresentationLayer/Wallet/Main/MainPresenter.swift | 9 ++++++++- Sources/ReownWalletKit/WalletKitClient.swift | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index c2bc6b2d4..3a41ddab8 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -126,7 +126,14 @@ extension MainPresenter { router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) } case .error(let routeResponseError): - AlertPresenter.present(message: "Rout response error", type: .success) + AlertPresenter.present(message: "Route response error", type: .success) + Task { + try await WalletKit.instance.respond( + topic: request.topic, + requestId: request.id, + response: .error(.init(code: 0, message: "")) + ) + } } } ActivityIndicatorManager.shared.stop() diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 43b02e5b7..b5153a9a0 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -342,6 +342,7 @@ public class WalletKitClient { throw Errors.chainAbstractionNotEnabled } + return try await chainAbstractionClient.status(orchestrationId: orchestrationId) } From e6a1b36805bc6ab07cfaf51d63908ba2c7da1fc2 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Fri, 13 Dec 2024 18:30:48 +0800 Subject: [PATCH 67/73] remove comments --- .../Wallet/CATransactionModal/CATransactionPresenter.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 73dccfa10..5b147cd0f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -335,13 +335,10 @@ final class CATransactionPresenter: ObservableObject { maxPriorityFeePerGas: estimates.maxPriorityFeePerGas ) let routUiFields = try await WalletKit.instance.getRouteUiFieds(routeResponse: routeResponseAvailable, initialTransaction: initTx, currency: Currency.usd) - print("aaaaaaaa") print(routUiFields.localTotal) - print("bbbbbb") print(routUiFields.localTotal.formatted) - print("XXXXXXXXX") print(routUiFields.localTotal.formattedAlt) await MainActor.run { From ff18f9ef75624a315712d647744a813effc53c57 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Tue, 17 Dec 2024 18:46:12 +0800 Subject: [PATCH 68/73] savepoint --- .../BusinessLayer/ChainAbstractionService.swift | 10 +++++----- .../CATransactionModal/CATransactionPresenter.swift | 4 ++-- Package.swift | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index 49b6125dc..e168983af 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -40,8 +40,8 @@ class ChainAbstractionService { for tx in routeResponseAvailable.transactions { do { let estimates = try await WalletKit.instance.estimateFees(chainId: tx.chainId) - let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)! * 2) - let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)! * 2) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas.stripHexPrefix(), radix: 16)! * 2) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas.stripHexPrefix(), radix: 16)! * 2) print(maxFeePerGas) print(maxPriorityFeePerGas) @@ -69,9 +69,9 @@ class ChainAbstractionService { } private func getRpcUrl(chainId: String) -> String { -// let projectId = Networking.projectId -// -// return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" + let projectId = Networking.projectId + + return "https://rpc.walletconnect.com/v1?chainId=\(chainId)&projectId=\(projectId)" switch chainId { case "eip155:10": diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 5b147cd0f..eb1b54734 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -145,8 +145,8 @@ final class CATransactionPresenter: ObservableObject { print(" Max Priority Fee: \(estimates.maxPriorityFeePerGas)") print(" Max Fee: \(estimates.maxFeePerGas)") - let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas, radix: 10)!) - let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas, radix: 10)!) + let maxPriorityFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxPriorityFeePerGas.stripHexPrefix(), radix: 16)!) + let maxFeePerGas = EthereumQuantity(quantity: BigUInt(estimates.maxFeePerGas.stripHexPrefix(), radix: 16)!) let from = try EthereumAddress(hex: tx.from, eip55: false) print("🔢 Fetching nonce...") diff --git a/Package.swift b/Package.swift index 9731a7ff3..0ccb6a304 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription // Determine if Yttrium should be used in debug (local) mode -let yttriumDebug = true +let yttriumDebug = false @@ -28,7 +28,7 @@ func buildYttriumWrapperTarget() -> Target { path: "Sources/YttriumWrapper" ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.2.22"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.4.7"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], From 1cb452d898683d72b428e531aeb5fb778b5a5f71 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 18 Dec 2024 12:27:55 +0800 Subject: [PATCH 69/73] integrate 0.4.8 --- .../xcshareddata/swiftpm/Package.resolved | 9 +++ .../ChainAbstractionService.swift | 4 +- .../CATransactionPresenter.swift | 63 ++++++++----------- .../Wallet/Main/MainPresenter.swift | 14 ++--- Package.resolved | 9 +++ Package.swift | 2 +- Sources/ReownWalletKit/WalletKitClient.swift | 30 +++++---- 7 files changed, 69 insertions(+), 62 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 43ef23fcc..0cbc33a68 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -270,6 +270,15 @@ "revision": "4232d34efa49f633ba61afde365d3896fc7f8740", "version": "2.15.0" } + }, + { + "package": "Yttrium", + "repositoryURL": "https://github.com/reown-com/yttrium", + "state": { + "branch": null, + "revision": "533e9bacce2c7a45b1f9b3705726d03f2a37be4e", + "version": "0.4.8" + } } ] }, diff --git a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift index e168983af..3f8db2955 100644 --- a/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift +++ b/Example/WalletApp/BusinessLayer/ChainAbstractionService.swift @@ -232,11 +232,11 @@ extension EthereumTransaction { gasPrice: nil, // Not needed for EIP1559 maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gas.stripHexPrefix(), radix: 16)!), + gasLimit: EthereumQuantity(quantity: BigUInt(routingTransaction.gasLimit.stripHexPrefix(), radix: 16)!), from: try EthereumAddress(hex: routingTransaction.from, eip55: false), to: try EthereumAddress(hex: routingTransaction.to, eip55: false), value: EthereumQuantity(quantity: 0.gwei), - data: EthereumData(Array(hex: routingTransaction.data)), + data: EthereumData(Array(hex: routingTransaction.input)), accessList: [:], // Empty access list for basic transactions transactionType: .eip1559 // Specify EIP1559 transaction type ) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index eb1b54734..4d60954d9 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -32,7 +32,7 @@ final class CATransactionPresenter: ObservableObject { } let router: CATransactionRouter let importAccount: ImportAccount - var routeUiFields: RouteUiFields? = nil + var routeUiFields: UiFields? = nil var chainId: String { sessionRequest.chainId.absoluteString } @@ -91,29 +91,30 @@ final class CATransactionPresenter: ObservableObject { print("🔄 Starting status check loop...") var status: StatusResponse = try await WalletKit.instance.status(orchestrationId: orchestrationId) - loop: while true { - switch status { - case .pending(let pending): - print("⏳ Transaction pending. Waiting for \(pending.checkIn) seconds...") - let delay = try UInt64(pending.checkIn) * 1_000_000 - try await Task.sleep(nanoseconds: delay) - print("🔍 Checking status again...") - status = try await WalletKit.instance.status(orchestrationId: orchestrationId) - - case .completed(let completed): - print("✅ Transaction completed successfully!") - print("📊 Completion details: \(completed)") - AlertPresenter.present(message: "Routing transactions completed", type: .success) - break loop - - case .error(let error): - print("❌ Transaction failed with error!") - print("💥 Error details: \(error)") - AlertPresenter.present(message: "Routing failed with error: \(error)", type: .error) - ActivityIndicatorManager.shared.stop() - return - } - } + let x = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: 5) +// loop: while true { +// switch status { +// case .pending(let pending): +// print("⏳ Transaction pending. Waiting for \(pending.checkIn) seconds...") +// let delay = try UInt64(pending.checkIn) * 1_000_000 +// try await Task.sleep(nanoseconds: delay) +// print("🔍 Checking status again...") +// status = try await WalletKit.instance.status(orchestrationId: orchestrationId) +// +// case .completed(let completed): +// print("✅ Transaction completed successfully!") +// print("📊 Completion details: \(completed)") +// AlertPresenter.present(message: "Routing transactions completed", type: .success) +// break loop +// +// case .error(let error): +// print("❌ Transaction failed with error!") +// print("💥 Error details: \(error)") +// AlertPresenter.present(message: "Routing failed with error: \(error)", type: .error) +// ActivityIndicatorManager.shared.stop() +// return +// } +// } print("🚀 Initiating initial transaction...") try await sendInitialTransaction() @@ -322,19 +323,7 @@ final class CATransactionPresenter: ObservableObject { let estimates = try await WalletKit.instance.estimateFees(chainId: chainId) - let initTx = Transaction( - from: tx.from, - to: tx.to, - value: "0", - gas: "0", - data: tx.data, - nonce: "0x", - chainId: sessionRequest.chainId.absoluteString, - gasPrice: "0", - maxFeePerGas: estimates.maxFeePerGas, - maxPriorityFeePerGas: estimates.maxPriorityFeePerGas - ) - let routUiFields = try await WalletKit.instance.getRouteUiFieds(routeResponse: routeResponseAvailable, initialTransaction: initTx, currency: Currency.usd) + let routUiFields = try await WalletKit.instance.getRouteUiFieds(routeResponse: routeResponseAvailable, currency: Currency.usd) print(routUiFields.localTotal) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 3a41ddab8..15f3d1f75 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -97,21 +97,15 @@ extension MainPresenter { do { let tx = try request.params.get([Tx].self)[0] - let transaction = InitTransaction( + let transaction = InitialTransaction( + chainId: request.chainId.absoluteString, from: tx.from, to: tx.to, value: "0", - gas: "0", - gasPrice: "0", - data: tx.data, - nonce: "0", - maxFeePerGas: "0", - maxPriorityFeePerGas: "0", - chainId: request.chainId.absoluteString + input: tx.data ) - ActivityIndicatorManager.shared.start() let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) @@ -126,7 +120,7 @@ extension MainPresenter { router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) } case .error(let routeResponseError): - AlertPresenter.present(message: "Route response error", type: .success) + AlertPresenter.present(message: "Route response error: \(routeResponseError)", type: .success) Task { try await WalletKit.instance.respond( topic: request.topic, diff --git a/Package.resolved b/Package.resolved index 76976ae40..7b2a29f11 100644 --- a/Package.resolved +++ b/Package.resolved @@ -54,6 +54,15 @@ "revision": "84b3d3f25a2e3b140ec12bb0d22c35b58f817d44", "version": "1.0.0" } + }, + { + "package": "Yttrium", + "repositoryURL": "https://github.com/reown-com/yttrium", + "state": { + "branch": null, + "revision": "79fdd0d3be2d00e371b8f23998a4aa7b1c14e847", + "version": "0.4.7" + } } ] }, diff --git a/Package.swift b/Package.swift index 0ccb6a304..7e57be5ee 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ func buildYttriumWrapperTarget() -> Target { path: "Sources/YttriumWrapper" ) } else { - dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.4.7"))) + dependencies.append(.package(url: "https://github.com/reown-com/yttrium", .exact("0.4.8"))) return .target( name: "YttriumWrapper", dependencies: [.product(name: "Yttrium", package: "yttrium")], diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index b5153a9a0..5d8095feb 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -275,6 +275,7 @@ public class WalletKitClient { // MARK: Yttrium + @available(*, message: "This method is experimental. Use with caution.") public func prepareSendTransactions(_ transactions: [FfiTransaction], ownerAccount: Account) async throws -> PreparedSendTransaction { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled @@ -283,6 +284,7 @@ public class WalletKitClient { return try await client.prepareSendTransactions(transactions: transactions) } + @available(*, message: "This method is experimental. Use with caution.") public func doSendTransaction(signatures: [OwnerSignature], doSendTransactionParams: String, ownerAccount: Account) async throws -> String { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled @@ -291,6 +293,7 @@ public class WalletKitClient { return try await client.doSendTransactions(signatures: signatures, doSendTransactionParams: doSendTransactionParams) } + @available(*, message: "This method is experimental. Use with caution.") public func getSmartAccount(ownerAccount: Account) async throws -> Account { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled @@ -302,6 +305,7 @@ public class WalletKitClient { return Account(blockchain: ownerAccount.blockchain, address: address)! } + @available(*, message: "This method is experimental. Use with caution.") public func waitForUserOperationReceipt(userOperationHash: String, ownerAccount: Account) async throws -> String { guard let smartAccountsManager = smartAccountsManager else { throw Errors.smartAccountNotEnabled @@ -342,17 +346,17 @@ public class WalletKitClient { throw Errors.chainAbstractionNotEnabled } - + return try await chainAbstractionClient.status(orchestrationId: orchestrationId) } @available(*, message: "This method is experimental. Use with caution.") - public func route(transaction: InitTransaction) async throws -> RouteResponse { + public func route(transaction: InitialTransaction) async throws -> PrepareResponse { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } - return try await chainAbstractionClient.route(transaction: transaction) + return try await chainAbstractionClient.prepare(initialTransaction: transaction) } @available(*, message: "This method is experimental. Use with caution.") @@ -365,11 +369,11 @@ public class WalletKitClient { } @available(*, message: "This method is experimental. Use with caution.") - public func getRouteUiFieds(routeResponse: RouteResponseAvailable, initialTransaction: Transaction, currency: Currency) async throws -> RouteUiFields { + public func getRouteUiFieds(routeResponse: RouteResponseAvailable, currency: Currency) async throws -> UiFields { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } - return try await chainAbstractionClient.getRouteUiFields(routeResponse: routeResponse, initialTransaction: initialTransaction, currency: currency) + return try await chainAbstractionClient.getUiFields(routeResponse: routeResponse, currency: currency) } @available(*, message: "This method is experimental. Use with caution.") @@ -379,13 +383,15 @@ public class WalletKitClient { } return try await chainAbstractionClient.erc20TokenBalance(chainId: chainId, token: token, owner: owner) } -// public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { -// guard let chainClient = chainAbstractionClient else { -// throw Errors.chainAbstractionNotEnabled -// } -// -// return try await chainClient.waitForSuccess(orchestrationId: orchestrationId, checkIn: checkIn) -// } + + @available(*, message: "This method is experimental. Use with caution.") + public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { + guard let chainClient = chainAbstractionClient else { + throw Errors.chainAbstractionNotEnabled + } + + return try await chainClient.waitForSuccessWithTimeout(orchestrationId: orchestrationId, checkIn: checkIn, timeout: 120) + } } From 6b3beb94a447905c14d7b36f11e4577dda839a2a Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 18 Dec 2024 16:59:19 +0800 Subject: [PATCH 70/73] update integration --- .../CATransactionPresenter.swift | 42 +++++-------------- .../Wallet/Main/MainPresenter.swift | 2 +- Package.swift | 1 - Sources/ReownWalletKit/WalletKitClient.swift | 8 ++-- 4 files changed, 15 insertions(+), 38 deletions(-) diff --git a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift index 4d60954d9..b4ebedd71 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/CATransactionModal/CATransactionPresenter.swift @@ -88,36 +88,15 @@ final class CATransactionPresenter: ObservableObject { let orchestrationId = routeResponseAvailable.orchestrationId print("📋 Orchestration ID: \(orchestrationId)") - print("🔄 Starting status check loop...") - var status: StatusResponse = try await WalletKit.instance.status(orchestrationId: orchestrationId) - - let x = try await WalletKit.instance.waitForSuccess(orchestrationId: orchestrationId, checkIn: 5) -// loop: while true { -// switch status { -// case .pending(let pending): -// print("⏳ Transaction pending. Waiting for \(pending.checkIn) seconds...") -// let delay = try UInt64(pending.checkIn) * 1_000_000 -// try await Task.sleep(nanoseconds: delay) -// print("🔍 Checking status again...") -// status = try await WalletKit.instance.status(orchestrationId: orchestrationId) -// -// case .completed(let completed): -// print("✅ Transaction completed successfully!") -// print("📊 Completion details: \(completed)") -// AlertPresenter.present(message: "Routing transactions completed", type: .success) -// break loop -// -// case .error(let error): -// print("❌ Transaction failed with error!") -// print("💥 Error details: \(error)") -// AlertPresenter.present(message: "Routing failed with error: \(error)", type: .error) -// ActivityIndicatorManager.shared.stop() -// return -// } -// } + print("🔄 Starting status checking...") + + let completed = try await WalletKit.instance.waitForSuccessWithTimeout(orchestrationId: orchestrationId, checkIn: 5, timeout: 180) + print("✅ Routing Transactions completed successfully!") + print("📊 Completion details: \(completed)") + AlertPresenter.present(message: "Routing transactions completed", type: .success) print("🚀 Initiating initial transaction...") - try await sendInitialTransaction() + try await sendInitialTransaction(initialTransaction: routeResponseAvailable.initialTransaction) ActivityIndicatorManager.shared.stop() print("✅ Initial transaction process completed successfully") AlertPresenter.present(message: "Initial transaction sent", type: .success) @@ -131,7 +110,7 @@ final class CATransactionPresenter: ObservableObject { } } - private func sendInitialTransaction() async throws { + private func sendInitialTransaction(initialTransaction: Transaction) async throws { print("📝 Preparing initial transaction...") let tx = try! sessionRequest.params.get([Tx].self)[0] @@ -160,7 +139,7 @@ final class CATransactionPresenter: ObservableObject { gasPrice: nil, maxFeePerGas: maxFeePerGas, maxPriorityFeePerGas: maxPriorityFeePerGas, - gasLimit: EthereumQuantity(quantity: 1023618), + gasLimit: EthereumQuantity(quantity: BigUInt(initialTransaction.gasLimit.stripHexPrefix(), radix: 16)!), from: from, to: try EthereumAddress(hex: tx.to, eip55: false), value: EthereumQuantity(quantity: 0.gwei), @@ -321,9 +300,8 @@ final class CATransactionPresenter: ObservableObject { let tx = try! sessionRequest.params.get([Tx].self)[0] - let estimates = try await WalletKit.instance.estimateFees(chainId: chainId) - let routUiFields = try await WalletKit.instance.getRouteUiFieds(routeResponse: routeResponseAvailable, currency: Currency.usd) + let routUiFields = try await WalletKit.instance.getUiFields(routeResponse: routeResponseAvailable, currency: Currency.usd) print(routUiFields.localTotal) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index 15f3d1f75..1016ccc0b 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -107,7 +107,7 @@ extension MainPresenter { ActivityIndicatorManager.shared.start() - let routeResponseSuccess = try await WalletKit.instance.route(transaction: transaction) + let routeResponseSuccess = try await WalletKit.instance.prepare(transaction: transaction) await MainActor.run { switch routeResponseSuccess { diff --git a/Package.swift b/Package.swift index 7e57be5ee..0158ba623 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,6 @@ import PackageDescription let yttriumDebug = false - // Define dependencies array var dependencies: [Package.Dependency] = [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), diff --git a/Sources/ReownWalletKit/WalletKitClient.swift b/Sources/ReownWalletKit/WalletKitClient.swift index 5d8095feb..a28d7b392 100644 --- a/Sources/ReownWalletKit/WalletKitClient.swift +++ b/Sources/ReownWalletKit/WalletKitClient.swift @@ -351,7 +351,7 @@ public class WalletKitClient { } @available(*, message: "This method is experimental. Use with caution.") - public func route(transaction: InitialTransaction) async throws -> PrepareResponse { + public func prepare(transaction: InitialTransaction) async throws -> PrepareResponse { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } @@ -369,7 +369,7 @@ public class WalletKitClient { } @available(*, message: "This method is experimental. Use with caution.") - public func getRouteUiFieds(routeResponse: RouteResponseAvailable, currency: Currency) async throws -> UiFields { + public func getUiFields(routeResponse: RouteResponseAvailable, currency: Currency) async throws -> UiFields { guard let chainAbstractionClient = chainAbstractionClient else { throw Errors.chainAbstractionNotEnabled } @@ -385,12 +385,12 @@ public class WalletKitClient { } @available(*, message: "This method is experimental. Use with caution.") - public func waitForSuccess(orchestrationId: String, checkIn: UInt64) async throws -> StatusResponseCompleted { + 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: 120) + return try await chainClient.waitForSuccessWithTimeout(orchestrationId: orchestrationId, checkIn: checkIn, timeout: timeout) } } From 1ec6b8d3564fad3d89887532143710ead3ecdadc Mon Sep 17 00:00:00 2001 From: llbartekll Date: Wed, 18 Dec 2024 17:20:34 +0800 Subject: [PATCH 71/73] update protected send --- Sources/WalletConnectRelay/Dispatching.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 5ccc19e3a..ef075f729 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -68,7 +68,19 @@ final class Dispatcher: NSObject, Dispatching { logger.debug("will try to send a socket frame") // Check if the socket is already connected and ready to send if socket.isConnected { - logger.debug("socket connected, will attempt to send a socket frame") + logger.debug("Socket is connected") + } else { + logger.debug("Socket is not connected") + } + + if networkMonitor.isConnected { + logger.debug("Network is connected") + } else { + logger.debug("Network is not connected") + } + + if socket.isConnected && networkMonitor.isConnected { + logger.debug("sending a socket frame") send(string, completion: completion) return } From 6a4abe0af568f4adb86f9651e4e6b4c3b9c6bff1 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 19 Dec 2024 11:28:33 +0800 Subject: [PATCH 72/73] add ApproveEngineLoggingHelper --- .../ConfigurationService.swift | 2 +- .../Engine/Common/ApproveEngine.swift | 13 +++- .../Common/ApproveEngineLoggingHelper.swift | 76 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 Sources/WalletConnectSign/Engine/Common/ApproveEngineLoggingHelper.swift diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index cb222703f..b2fe5202a 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -32,7 +32,7 @@ final class ConfigurationService { ) Notify.instance.setLogging(level: .off) - Sign.instance.setLogging(level: .off) + Sign.instance.setLogging(level: .debug) Events.instance.setLogging(level: .off) if let clientId = try? Networking.interactor.getClientId() { diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index a56f514c7..20f9e4520 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -31,6 +31,7 @@ final class ApproveEngine { private let rpcHistory: RPCHistory private let authRequestSubscribersTracking: AuthRequestSubscribersTracking private let eventsClient: EventsClientProtocol + private let approveEngineLoggingHelper: ApproveEngineLoggingHelper private var publishers = Set() @@ -64,6 +65,7 @@ final class ApproveEngine { self.rpcHistory = rpcHistory self.authRequestSubscribersTracking = authRequestSubscribersTracking self.eventsClient = eventsClient + self.approveEngineLoggingHelper = ApproveEngineLoggingHelper(logger: logger) setupRequestSubscriptions() setupResponseSubscriptions() @@ -73,7 +75,10 @@ final class ApproveEngine { func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace], sessionProperties: [String: String]? = nil) async throws -> Session { eventsClient.startTrace(topic: "") - logger.debug("Approving session proposal") + logger.debug("Approving session proposal...") + + approveEngineLoggingHelper.logSessionNamespaces(sessionNamespaces) + eventsClient.saveTraceEvent(SessionApproveExecutionTraceEvents.approvingSessionProposal) guard !sessionNamespaces.isEmpty else { @@ -394,6 +399,12 @@ private extension ApproveEngine { func handleSessionProposeRequest(payload: RequestSubscriptionPayload) { logger.debug("Received Session Proposal") let proposal = payload.request + + approveEngineLoggingHelper.logProposalNamespaces(title: "Required Namespaces", proposal.requiredNamespaces) + if let optionalNamespaces = proposal.optionalNamespaces { + approveEngineLoggingHelper.logProposalNamespaces(title: "Optional Namespaces", optionalNamespaces) + } + do { try Namespace.validate(proposal.requiredNamespaces) } catch { return respondError(payload: payload, reason: .invalidUpdateRequest, protocolMethod: SessionProposeProtocolMethod.responseAutoReject()) } diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngineLoggingHelper.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngineLoggingHelper.swift new file mode 100644 index 000000000..4e2889d22 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngineLoggingHelper.swift @@ -0,0 +1,76 @@ + +import Foundation + + +final class ApproveEngineLoggingHelper { + + private let logger: ConsoleLogging + + init(logger: ConsoleLogging) { + self.logger = logger + } + + func logProposalNamespaces(title: String, _ namespaces: [String: ProposalNamespace]) { + logger.debug("\(title):") + for (key, namespace) in namespaces { + logger.debug(" Namespace Key: \(key)") + + if let chains = namespace.chains, !chains.isEmpty { + let chainList = chains.map { $0.absoluteString }.joined(separator: ", ") + logger.debug(" Chains: [\(chainList)]") + } else { + logger.debug(" Chains: None") + } + + if !namespace.methods.isEmpty { + let methodsList = namespace.methods.sorted().joined(separator: ", ") + logger.debug(" Methods: [\(methodsList)]") + } else { + logger.debug(" Methods: None") + } + + if !namespace.events.isEmpty { + let eventsList = namespace.events.sorted().joined(separator: ", ") + logger.debug(" Events: [\(eventsList)]") + } else { + logger.debug(" Events: None") + } + } + } + + func logSessionNamespaces(_ sessionNamespaces: [String: SessionNamespace]) { + logger.debug("Session Namespaces:") + for (namespaceKey, ns) in sessionNamespaces { + logger.debug("Namespace: \(namespaceKey)") + + if let chains = ns.chains, !chains.isEmpty { + let chainStrings = chains.map { $0.absoluteString }.joined(separator: ", ") + logger.debug(" Chains: [\(chainStrings)]") + } else { + logger.debug(" Chains: None") + } + + if !ns.accounts.isEmpty { + // Assuming `Account` has a property `address` and `blockchain` + let accountStrings = ns.accounts.map { "\($0.blockchain.absoluteString):\($0.address)" }.joined(separator: ", ") + logger.debug(" Accounts: [\(accountStrings)]") + } else { + logger.debug(" Accounts: None") + } + + if !ns.methods.isEmpty { + let methodList = ns.methods.sorted().joined(separator: ", ") + logger.debug(" Methods: [\(methodList)]") + } else { + logger.debug(" Methods: None") + } + + if !ns.events.isEmpty { + let eventList = ns.events.sorted().joined(separator: ", ") + logger.debug(" Events: [\(eventList)]") + } else { + logger.debug(" Events: None") + } + } + } +} From e8a5b1737580281bcc5191d643ac739c078ae890 Mon Sep 17 00:00:00 2001 From: llbartekll Date: Thu, 19 Dec 2024 12:12:18 +0800 Subject: [PATCH 73/73] fix platform tests --- .../Web3Wallet/XPlatformW3WTests.swift | 3 ++- .../WalletKitClientFactory.swift | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift index cbbffc570..2d6f7bfa2 100644 --- a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift +++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift @@ -66,7 +66,8 @@ final class XPlatformW3WTests: XCTestCase { signClient: signClient, pairingClient: pairingClient, pushClient: PushClientMock(), - config: WalletKit.Config(crypto: DefaultCryptoProvider(), pimlicoApiKey: nil)) + config: WalletKit.Config(crypto: DefaultCryptoProvider(), pimlicoApiKey: nil), + projectId: InputConfig.projectId) } func testSessionSettle() async throws { diff --git a/Sources/ReownWalletKit/WalletKitClientFactory.swift b/Sources/ReownWalletKit/WalletKitClientFactory.swift index a4fa0c797..625804107 100644 --- a/Sources/ReownWalletKit/WalletKitClientFactory.swift +++ b/Sources/ReownWalletKit/WalletKitClientFactory.swift @@ -21,4 +21,28 @@ struct WalletKitClientFactory { chainAbstractionClient: chainAbstractionClient ) } + +#if DEBUG + static func create( + signClient: SignClientProtocol, + pairingClient: PairingClientProtocol, + pushClient: PushClientProtocol, + config: WalletKit.Config, + projectId: String + ) -> WalletKitClient { + var safesManager: SafesManager? = nil + if let pimlicoApiKey = config.pimlicoApiKey { + safesManager = SafesManager(pimlicoApiKey: pimlicoApiKey) + } + let chainAbstractionClient = ChainAbstractionClient(projectId: projectId) + return WalletKitClient( + signClient: signClient, + pairingClient: pairingClient, + pushClient: pushClient, + smartAccountsManager: safesManager, + chainAbstractionClient: chainAbstractionClient + ) + } +#endif + }