├── .gitattributes ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── userop-swift │ ├── Encodable+Ext.swift │ ├── Signer.swift │ ├── UserOperation.swift │ ├── abi │ └── Abi.swift │ ├── context │ ├── Client.swift │ └── UserOperationMiddlewareContext.swift │ ├── contracts │ ├── EntryPoint.swift │ ├── P256Account.swift │ ├── P256AccountFactory.swift │ ├── SimpleAccount.swift │ └── SimpleAccountFactory.swift │ ├── middleware │ ├── GasEstimateMiddleware.swift │ ├── GasPriceMiddleware.swift │ ├── SignatureMiddleware.swift │ ├── UserOperationMiddleware.swift │ └── VerifyingPaymaster.swift │ ├── preset │ ├── P256AccountBuilder.swift │ └── SimpleAccountBuilder.swift │ └── provider │ ├── BundlerJsonRpcProvider.swift │ ├── JsonRpcProvider.swift │ └── Web3Provider+Ex.swift ├── Tests └── userop-swiftTests │ └── P256AccountTests.swift └── userop.podspec /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,intellij+all 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,intellij+all 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # AWS User-specific 16 | .idea/**/aws.xml 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # SonarLint plugin 69 | .idea/sonarlint/ 70 | 71 | # Crashlytics plugin (for Android Studio and IntelliJ) 72 | com_crashlytics_export_strings.xml 73 | crashlytics.properties 74 | crashlytics-build.properties 75 | fabric.properties 76 | 77 | # Editor-based Rest Client 78 | .idea/httpRequests 79 | 80 | # Android studio 3.1+ serialized cache file 81 | .idea/caches/build_file_checksums.ser 82 | 83 | ### Intellij+all Patch ### 84 | # Ignore everything but code style settings and run configurations 85 | # that are supposed to be shared within teams. 86 | 87 | .idea/* 88 | 89 | !.idea/codeStyles 90 | !.idea/runConfigurations 91 | 92 | ### Swift ### 93 | # Xcode 94 | # 95 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 96 | 97 | ## User settings 98 | xcuserdata/ 99 | 100 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 101 | *.xcscmblueprint 102 | *.xccheckout 103 | 104 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 105 | build/ 106 | DerivedData/ 107 | *.moved-aside 108 | *.pbxuser 109 | !default.pbxuser 110 | *.mode1v3 111 | !default.mode1v3 112 | *.mode2v3 113 | !default.mode2v3 114 | *.perspectivev3 115 | !default.perspectivev3 116 | 117 | ## Obj-C/Swift specific 118 | *.hmap 119 | 120 | ## App packaging 121 | *.ipa 122 | *.dSYM.zip 123 | *.dSYM 124 | 125 | ## Playgrounds 126 | timeline.xctimeline 127 | playground.xcworkspace 128 | 129 | # Swift Package Manager 130 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 131 | # Packages/ 132 | # Package.pins 133 | # Package.resolved 134 | # *.xcodeproj 135 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 136 | # hence it is not needed unless you have added a package configuration file to your project 137 | .swiftpm 138 | 139 | .build/ 140 | 141 | # CocoaPods 142 | # We recommend against adding the Pods directory to your .gitignore. However 143 | # you should judge for yourself, the pros and cons are mentioned at: 144 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 145 | # Pods/ 146 | # Add this line if you want to avoid checking in source code from the Xcode workspace 147 | # *.xcworkspace 148 | 149 | # Carthage 150 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 151 | # Carthage/Checkouts 152 | 153 | Carthage/Build/ 154 | 155 | # Accio dependency management 156 | Dependencies/ 157 | .accio/ 158 | 159 | # fastlane 160 | # It is recommended to not store the screenshots in the git repo. 161 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 162 | # For more information about the recommended setup visit: 163 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 164 | 165 | fastlane/report.xml 166 | fastlane/Preview.html 167 | fastlane/screenshots/**/*.png 168 | fastlane/test_output 169 | 170 | # Code Injection 171 | # After new code Injection tools there's a generated folder /iOSInjectionProject 172 | # https://github.com/johnno1962/injectionforxcode 173 | 174 | iOSInjectionProject/ 175 | 176 | ### Xcode ### 177 | 178 | ## Xcode 8 and earlier 179 | 180 | ### Xcode Patch ### 181 | *.xcodeproj/* 182 | !*.xcodeproj/project.pbxproj 183 | !*.xcodeproj/xcshareddata/ 184 | !*.xcodeproj/project.xcworkspace/ 185 | !*.xcworkspace/contents.xcworkspacedata 186 | /*.gcno 187 | **/xcshareddata/WorkspaceSettings.xcsettings 188 | 189 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,intellij+all 190 | 191 | .DS_Store 192 | 193 | Tests/userop-swiftTests/privateTests 194 | .vscode 195 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 august 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "bigint", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/attaswift/BigInt.git", 7 | "state" : { 8 | "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", 9 | "version" : "5.3.0" 10 | } 11 | }, 12 | { 13 | "identity" : "cryptoswift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", 16 | "state" : { 17 | "revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", 18 | "version" : "1.7.2" 19 | } 20 | }, 21 | { 22 | "identity" : "web3swift", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/zhangliugang/web3swift.git", 25 | "state" : { 26 | "branch" : "userop", 27 | "revision" : "b65adc29130f93f5328032570f477ebb3ef43da4" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "userop-swift", 8 | platforms: [ 9 | .macOS(.v12), .iOS(.v14) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, making them visible to other packages. 13 | .library( 14 | name: "userop-swift", 15 | targets: ["userop-swift"]) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/attaswift/BigInt.git", from: "5.3.0"), 19 | .package(url: "https://github.com/zhangliugang/web3swift.git", branch: "userop") 20 | // .package(path: "../../web3swift") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package, defining a module or a test suite. 24 | // Targets can depend on other targets in this package and products from dependencies. 25 | .target( 26 | name: "userop-swift", dependencies: ["BigInt", "web3swift"]), 27 | .testTarget( 28 | name: "userop-swiftTests", 29 | dependencies: ["userop-swift"]) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # userop-swift 3 | 4 | [userop-kotlin version is here](https://github.com/iotexproject/userop-kt) 5 | 6 | 7 | ## About Account Abstraction Using Alt Mempool 8 | An account abstraction proposal which completely avoids the need for consensus-layer protocol changes. Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal instead introduces a higher-layer pseudo-transaction object called a UserOperation. Users send UserOperation objects into a separate mempool. A special class of actor called bundlers package up a set of these objects into a transaction making a handleOps call to a special contract, and that transaction then gets included in a block. 9 | 10 | [ERC-4337: Account Abstraction Using Alt Mempool](https://eips.ethereum.org/EIPS/eip-4337) 11 | 12 | 13 | ## Advanced Supported P256 Account for Trusted Environment 14 | 15 | On the basis of [userop.js](https://github.com/stackup-wallet/userop.js) signature, we advanced supported secp256r1-based signature. 16 | 17 | The "secp256r1" elliptic curve is a standardized curve by NIST which has the same calculations by different input parameters with""secp256k1” elliptic curve used by the "ecrecover" precompiled contract. The cost of combined attacks and the security conditions are almost the same for both curves. Adding a precompiled contract which is similar to "ecrecover" can provide signature verifications using the "secp256r1" elliptic curve in the smart contracts and multi-faceted benefits can occur. One important factor is that this curve is widely used and supported in many modern devices such as Apple’s Secure Enclave, Webauthn, Android Keychain which proves the user adoption. Additionally, the introduction of this precompile could enable valuable features in the account abstraction which allows more efficient and flexible management of accounts by transaction signs in mobile devices. Most of the modern devices and applications rely on the "secp256r1" elliptic curve. The addition of this precompiled contract enables the verification of device native transaction signing mechanisms. For example: 18 | 19 | + **Apple’s Secure Enclave** :shipit:: There is a separate "Trusted Execution Environment" in Apple hardware which can sign arbitrary messages and can only be accessed by biometric identification. 20 | Webauthn: Web Authentication (WebAuthn) is a web standard published by the World Wide Web Consortium (W3C). WebAuthn aims to standardize an interface for authenticating users to web-based applications and services using public-key cryptography. It is being used by almost all of the modern web browsers. 21 | * **Android Keystore**: Android Keystore is an API that manages the private keys and signing methods. The private keys are not processed while using Keystore as the applications’ signing method. Also, it can be done in the "Trusted Execution Environment" in the microchip. 22 | 23 | 24 | [Reffer to EIP-7212](https://eips.ethereum.org/EIPS/eip-7212) 25 | 26 | 27 | ## Install 28 | 29 | ```swift 30 | .package(url: "https://github.com/iotexproject/userop-swift.git", from: "x.y.z") 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### Create Signer 36 | Example for secp256k1 37 | ```swift 38 | struct SimpleSigner: Signer { 39 | private let privateKey: Data 40 | 41 | init(privateKey: Data) { 42 | self.privateKey = privateKey 43 | } 44 | 45 | func getAddress() async -> EthereumAddress { 46 | try! await Utilities.publicToAddress(getPublicKey())! 47 | } 48 | 49 | func getPublicKey() async throws -> Data { 50 | Utilities.privateToPublic(privateKey)! 51 | } 52 | 53 | func signMessage(_ data: Data) async throws -> Data { 54 | let (compressedSignature, _) = SECP256K1.signForRecovery(hash: data, 55 | privateKey: privateKey, 56 | useExtraEntropy: false) 57 | return compressedSignature! 58 | } 59 | } 60 | 61 | ``` 62 | 63 | Example for secpk256r1 64 | ```swift 65 | struct P256Signer: Signer { 66 | private let privateKey: SecKey! 67 | 68 | func getAddress() async -> Web3Core.EthereumAddress { 69 | fatalError("Don't call this function, get address from p256 key is not supported.") 70 | } 71 | 72 | func getPublicKey() async throws -> Data { 73 | let data = SecKeyCopyExternalRepresentation(privateKey, nil) 74 | return (data! as Data)[1...] 75 | } 76 | 77 | func signMessage(_ data: Data) async throws -> Data { 78 | let signed = SecKeyCreateSignature(privateKey, .ecdsaSignatureMessageX962SHA256, data as CFData, nil)! as Data 79 | let xLength = UInt(from: signed[3..<4].toHexString())! 80 | 81 | let signatureArray = [ 82 | signed[4.. Once you activeated your account, you can pass it as `senderAddress` into AccountBuilder. In this case, `salt` will be ignored. 114 | 115 | ### Send ETH 116 | ```swift 117 | accountBuilder.execute(to: address, value: Utilities.parseToBigUInt("1", units: .ether)!, data: Data()) 118 | 119 | let response = try await client.sendUserOperation(builder: accountBuilder) 120 | ``` 121 | 122 | ### Send ERC20 123 | ```swift 124 | let erc20 = try EthereumContract(erc20_abi) 125 | let data = erc20.method("transfer", parameters: [to, value]) 126 | 127 | accountBuilder.execute(to: erc20_address, value: 0, data: data) 128 | 129 | let response = try await client.sendUserOperation(builder: accountBuilder) 130 | ``` 131 | -------------------------------------------------------------------------------- /Sources/userop-swift/Encodable+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Ext.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/29. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | 11 | public protocol EncodableToHex: Encodable { 12 | var hexString: String { get } 13 | } 14 | 15 | extension KeyedEncodingContainer { 16 | public mutating func encodeHex(_ value: T, forKey: KeyedDecodingContainer.Key) throws where T: EncodableToHex { 17 | try encode(value.hexString, forKey: forKey) 18 | } 19 | 20 | public mutating func encodeHexIfPresent(_ value: T?, forKey: KeyedDecodingContainer.Key) throws where T: EncodableToHex { 21 | try encodeIfPresent(value?.hexString, forKey: forKey) 22 | } 23 | } 24 | 25 | extension Data: EncodableToHex { 26 | public var hexString: String { 27 | toHexString().addHexPrefix() 28 | } 29 | } 30 | 31 | extension BigUInt: EncodableToHex { 32 | public var hexString: String { 33 | String(self, radix: 16).addHexPrefix() 34 | } 35 | } 36 | 37 | extension BigInt: EncodableToHex { 38 | public var hexString: String { 39 | String(self, radix: 16).addHexPrefix() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/userop-swift/Signer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Signer.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import Web3Core 10 | 11 | public protocol Signer { 12 | func getAddress() async -> EthereumAddress 13 | 14 | func getPublicKey() async throws -> Data 15 | 16 | func signMessage(_ data: Data) async throws -> Data 17 | } 18 | -------------------------------------------------------------------------------- /Sources/userop-swift/UserOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserOperation.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/21. 6 | // 7 | 8 | import BigInt 9 | import Foundation 10 | import Web3Core 11 | 12 | let defaultVerificationGasLimit = BigUInt(70000) 13 | let defaultCallGasLimit = BigUInt(35000) 14 | let defaultPreVerificationGas = BigUInt(21000) 15 | 16 | /// UserOperation Entity 17 | public struct UserOperation: Encodable { 18 | public var sender: EthereumAddress 19 | public var nonce: BigUInt 20 | public var initCode: Data 21 | public var callData: Data 22 | public var callGasLimit: BigUInt 23 | public var verificationGasLimit: BigUInt 24 | public var preVerificationGas: BigUInt 25 | public var maxFeePerGas: BigUInt 26 | public var maxPriorityFeePerGas: BigUInt 27 | public var paymasterAndData: Data 28 | public var signature: Data 29 | 30 | static var `default`: Self { 31 | UserOperation( 32 | sender: EthereumAddress.zero, 33 | nonce: 0, 34 | initCode: Data(), 35 | callData: Data(), 36 | callGasLimit: defaultCallGasLimit, 37 | verificationGasLimit: defaultVerificationGasLimit, 38 | preVerificationGas: defaultPreVerificationGas, 39 | maxFeePerGas: 0, 40 | maxPriorityFeePerGas: 0, 41 | paymasterAndData: Data(), 42 | signature: Data() 43 | ) 44 | } 45 | 46 | enum CodingKeys: CodingKey { 47 | case sender 48 | case nonce 49 | case initCode 50 | case callData 51 | case callGasLimit 52 | case verificationGasLimit 53 | case preVerificationGas 54 | case maxFeePerGas 55 | case maxPriorityFeePerGas 56 | case paymasterAndData 57 | case signature 58 | } 59 | 60 | public func encode(to encoder: Encoder) throws { 61 | var container = encoder.container(keyedBy: CodingKeys.self) 62 | try container.encode(sender, forKey: .sender) 63 | try container.encodeHex(nonce, forKey: .nonce) 64 | try container.encodeHex(initCode, forKey: .initCode) 65 | try container.encodeHex(callData, forKey: .callData) 66 | try container.encodeHex(callGasLimit, forKey: .callGasLimit) 67 | try container.encodeHex(verificationGasLimit, forKey: .verificationGasLimit) 68 | try container.encodeHex(preVerificationGas, forKey: .preVerificationGas) 69 | try container.encodeHex(maxFeePerGas, forKey: .maxFeePerGas) 70 | try container.encodeHex(maxPriorityFeePerGas, forKey: .maxPriorityFeePerGas) 71 | try container.encodeHex(paymasterAndData, forKey: .paymasterAndData) 72 | try container.encodeHex(signature, forKey: .signature) 73 | } 74 | } 75 | 76 | public protocol IUserOperationBuilder { 77 | var sender: EthereumAddress { get set } 78 | var nonce: BigUInt { get set } 79 | var initCode: Data { get set } 80 | var callData: Data { get set } 81 | var callGasLimit: BigUInt { get set } 82 | var verificationGasLimit: BigUInt { get set } 83 | var preVerificationGas: BigUInt { get set } 84 | var maxFeePerGas: BigUInt { get set } 85 | var maxPriorityFeePerGas: BigUInt { get set } 86 | var paymasterAndData: Data { get set } 87 | var signature: Data { get set } 88 | 89 | func useMiddleware(_ middleware: UserOperationMiddleware) 90 | func resetMiddleware() 91 | 92 | func build(entryPoint: EthereumAddress, chainId: BigUInt) async throws -> UserOperation 93 | 94 | func reset() 95 | } 96 | 97 | /// Default implement for `UserOperation` builder 98 | open class UserOperationBuilder: IUserOperationBuilder { 99 | private var op: UserOperation 100 | private var middlewares = [UserOperationMiddleware]() 101 | 102 | init( 103 | op: UserOperation? = nil 104 | ) { 105 | self.op = op ?? UserOperation.default 106 | } 107 | 108 | public var sender: EthereumAddress { 109 | get { return op.sender } 110 | set { op.sender = newValue } 111 | } 112 | 113 | public var nonce: BigUInt { 114 | get { return op.nonce } 115 | set { op.nonce = newValue } 116 | } 117 | 118 | public var initCode: Data { 119 | get { return op.initCode } 120 | set { op.initCode = newValue } 121 | } 122 | 123 | public var callData: Data { 124 | get { return op.callData } 125 | set { op.callData = newValue } 126 | } 127 | 128 | public var callGasLimit: BigUInt { 129 | get { return op.callGasLimit } 130 | set { op.callGasLimit = newValue } 131 | } 132 | 133 | public var verificationGasLimit: BigUInt { 134 | get { return op.verificationGasLimit } 135 | set { op.verificationGasLimit = newValue } 136 | } 137 | 138 | public var preVerificationGas: BigUInt { 139 | get { return op.preVerificationGas} 140 | set { op.preVerificationGas = newValue } 141 | } 142 | 143 | public var maxFeePerGas: BigUInt { 144 | get { return op.maxFeePerGas } 145 | set { op.maxFeePerGas = newValue } 146 | } 147 | 148 | public var maxPriorityFeePerGas: BigUInt { 149 | get { return op.maxPriorityFeePerGas } 150 | set { op.maxPriorityFeePerGas = newValue } 151 | } 152 | 153 | public var paymasterAndData: Data { 154 | get { return op.paymasterAndData } 155 | set { op.paymasterAndData = newValue } 156 | } 157 | 158 | public var signature: Data { 159 | get { return op.signature } 160 | set { op.signature = newValue } 161 | } 162 | 163 | public func useMiddleware(_ middleware: UserOperationMiddleware) { 164 | middlewares.append(middleware) 165 | } 166 | 167 | public func resetMiddleware() { 168 | middlewares = [] 169 | } 170 | 171 | public func build(entryPoint: EthereumAddress, chainId: BigUInt) async throws -> UserOperation { 172 | var ctx = UserOperationMiddlewareContext(op: op, entryPoint: entryPoint, chainId: chainId) 173 | 174 | for middleware in middlewares { 175 | try await middleware.process(&ctx) 176 | } 177 | 178 | return ctx.op 179 | } 180 | 181 | public func reset() { 182 | op = UserOperation.default 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/userop-swift/abi/Abi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Abi.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Abi { 11 | public static let simpleAccount = """ 12 | [{"inputs":[{"internalType":"contract IEntryPoint","name":"anEntryPoint","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"beacon","type":"address"}],"name":"BeaconUpgraded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract IEntryPoint","name":"entryPoint","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"}],"name":"SimpleAccountInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"inputs":[],"name":"addDeposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"entryPoint","outputs":[{"internalType":"contract IEntryPoint","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"dest","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"func","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"dest","type":"address[]"},{"internalType":"bytes[]","name":"func","type":"bytes[]"}],"name":"executeBatch","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"anOwner","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"tokensReceived","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation","name":"userOp","type":"tuple"},{"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"internalType":"uint256","name":"missingAccountFunds","type":"uint256"}],"name":"validateUserOp","outputs":[{"internalType":"uint256","name":"validationData","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawDepositTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 13 | """ 14 | 15 | public static let simpleAccountFactory = """ 16 | [{"inputs":[{"internalType":"contract IEntryPoint","name":"_entryPoint","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"accountImplementation","outputs":[{"internalType":"contract SimpleAccount","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"createAccount","outputs":[{"internalType":"contract SimpleAccount","name":"ret","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"getAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] 17 | """ 18 | 19 | public static let p256Account = """ 20 | [{"inputs":[{"internalType":"contract IEntryPoint","name":"anEntryPoint","type":"address"},{"internalType":"contract ISecp256r1","name":"aSecp256r1","type":"address"},{"internalType":"contract EmailGuardian","name":"anEmailGuardian","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"}],"name":"AccountPendingRecovey","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"}],"name":"AccountRecovered","type":"event"},{"anonymous":false,"inputs":[],"name":"AccountRecoveryStopped","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"}],"name":"AccountResetted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"beacon","type":"address"}],"name":"BeaconUpgraded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bytes32","name":"email","type":"bytes32"}],"name":"EmailGuardianAdded","type":"event"},{"anonymous":false,"inputs":[],"name":"EmailGuardianRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"contract IEntryPoint","name":"entryPoint","type":"address"},{"indexed":false,"internalType":"contract ISecp256r1","name":"validator","type":"address"},{"indexed":false,"internalType":"contract EmailGuardian","name":"guardian","type":"address"},{"indexed":false,"internalType":"bytes","name":"publicKey","type":"bytes"}],"name":"P256AccountInitialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"inputs":[],"name":"addDeposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_email","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"}],"name":"addEmailGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"entryPoint","outputs":[{"internalType":"contract IEntryPoint","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"dest","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"func","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"dest","type":"address[]"},{"internalType":"uint256[]","name":"values","type":"uint256[]"},{"internalType":"bytes[]","name":"func","type":"bytes[]"}],"name":"executeBatch","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"_publicKey","type":"bytes"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155BatchReceived","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC1155Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"pendingPublicKey","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"},{"internalType":"bytes","name":"publicKey","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"server","type":"bytes32"},{"internalType":"bytes","name":"data","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"},{"internalType":"bytes","name":"pubkey","type":"bytes"}],"name":"pendingRecovery","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"publicKey","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"recovery","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"removeEmailGuardian","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"pubkey","type":"bytes"}],"name":"resetPublicKey","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"stopRecovery","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"bytes","name":"","type":"bytes"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"tokensReceived","outputs":[],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation","name":"userOp","type":"tuple"},{"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"internalType":"uint256","name":"missingAccountFunds","type":"uint256"}],"name":"validateUserOp","outputs":[{"internalType":"uint256","name":"validationData","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawDepositTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 21 | """ 22 | 23 | public static let p256AccountFactory = """ 24 | [{"inputs":[{"internalType":"contract IEntryPoint","name":"_entryPoint","type":"address"},{"internalType":"contract ISecp256r1","name":"_validator","type":"address"},{"internalType":"contract EmailGuardian","name":"_guardian","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"accountImplementation","outputs":[{"internalType":"contract P256Account","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"publicKey","type":"bytes"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"createAccount","outputs":[{"internalType":"contract P256Account","name":"ret","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"publicKey","type":"bytes"},{"internalType":"uint256","name":"salt","type":"uint256"}],"name":"getAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}] 25 | """ 26 | 27 | public static let entryPoint = """ 28 | [{"inputs":[{"internalType":"uint256","name":"preOpGas","type":"uint256"},{"internalType":"uint256","name":"paid","type":"uint256"},{"internalType":"uint48","name":"validAfter","type":"uint48"},{"internalType":"uint48","name":"validUntil","type":"uint48"},{"internalType":"bool","name":"targetSuccess","type":"bool"},{"internalType":"bytes","name":"targetResult","type":"bytes"}],"name":"ExecutionResult","type":"error"},{"inputs":[{"internalType":"uint256","name":"opIndex","type":"uint256"},{"internalType":"string","name":"reason","type":"string"}],"name":"FailedOp","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"SenderAddressResult","type":"error"},{"inputs":[{"internalType":"address","name":"aggregator","type":"address"}],"name":"SignatureValidationFailed","type":"error"},{"inputs":[{"components":[{"internalType":"uint256","name":"preOpGas","type":"uint256"},{"internalType":"uint256","name":"prefund","type":"uint256"},{"internalType":"bool","name":"sigFailed","type":"bool"},{"internalType":"uint48","name":"validAfter","type":"uint48"},{"internalType":"uint48","name":"validUntil","type":"uint48"},{"internalType":"bytes","name":"paymasterContext","type":"bytes"}],"internalType":"struct IEntryPoint.ReturnInfo","name":"returnInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"senderInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"factoryInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"paymasterInfo","type":"tuple"}],"name":"ValidationResult","type":"error"},{"inputs":[{"components":[{"internalType":"uint256","name":"preOpGas","type":"uint256"},{"internalType":"uint256","name":"prefund","type":"uint256"},{"internalType":"bool","name":"sigFailed","type":"bool"},{"internalType":"uint48","name":"validAfter","type":"uint48"},{"internalType":"uint48","name":"validUntil","type":"uint48"},{"internalType":"bytes","name":"paymasterContext","type":"bytes"}],"internalType":"struct IEntryPoint.ReturnInfo","name":"returnInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"senderInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"factoryInfo","type":"tuple"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"paymasterInfo","type":"tuple"},{"components":[{"internalType":"address","name":"aggregator","type":"address"},{"components":[{"internalType":"uint256","name":"stake","type":"uint256"},{"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"internalType":"struct IStakeManager.StakeInfo","name":"stakeInfo","type":"tuple"}],"internalType":"struct IEntryPoint.AggregatorStakeInfo","name":"aggregatorInfo","type":"tuple"}],"name":"ValidationResultWithAggregation","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"address","name":"factory","type":"address"},{"indexed":false,"internalType":"address","name":"paymaster","type":"address"}],"name":"AccountDeployed","type":"event"},{"anonymous":false,"inputs":[],"name":"BeforeExecution","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"totalDeposit","type":"uint256"}],"name":"Deposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"aggregator","type":"address"}],"name":"SignatureAggregatorChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"totalStaked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"name":"StakeLocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"withdrawTime","type":"uint256"}],"name":"StakeUnlocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"StakeWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"paymaster","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"bool","name":"success","type":"bool"},{"indexed":false,"internalType":"uint256","name":"actualGasCost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actualGasUsed","type":"uint256"}],"name":"UserOperationEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"revertReason","type":"bytes"}],"name":"UserOperationRevertReason","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"inputs":[],"name":"SIG_VALIDATION_FAILED","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"}],"name":"_validateSenderAndPaymaster","outputs":[],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"}],"name":"addStake","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"depositTo","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"deposits","outputs":[{"internalType":"uint112","name":"deposit","type":"uint112"},{"internalType":"bool","name":"staked","type":"bool"},{"internalType":"uint112","name":"stake","type":"uint112"},{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"},{"internalType":"uint48","name":"withdrawTime","type":"uint48"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getDepositInfo","outputs":[{"components":[{"internalType":"uint112","name":"deposit","type":"uint112"},{"internalType":"bool","name":"staked","type":"bool"},{"internalType":"uint112","name":"stake","type":"uint112"},{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"},{"internalType":"uint48","name":"withdrawTime","type":"uint48"}],"internalType":"struct IStakeManager.DepositInfo","name":"info","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint192","name":"key","type":"uint192"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"}],"name":"getSenderAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation","name":"userOp","type":"tuple"}],"name":"getUserOpHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation[]","name":"userOps","type":"tuple[]"},{"internalType":"contract IAggregator","name":"aggregator","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct IEntryPoint.UserOpsPerAggregator[]","name":"opsPerAggregator","type":"tuple[]"},{"internalType":"address payable","name":"beneficiary","type":"address"}],"name":"handleAggregatedOps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation[]","name":"ops","type":"tuple[]"},{"internalType":"address payable","name":"beneficiary","type":"address"}],"name":"handleOps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"key","type":"uint192"}],"name":"incrementNonce","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"callData","type":"bytes"},{"components":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"address","name":"paymaster","type":"address"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"}],"internalType":"struct EntryPoint.MemoryUserOp","name":"mUserOp","type":"tuple"},{"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"internalType":"uint256","name":"prefund","type":"uint256"},{"internalType":"uint256","name":"contextOffset","type":"uint256"},{"internalType":"uint256","name":"preOpGas","type":"uint256"}],"internalType":"struct EntryPoint.UserOpInfo","name":"opInfo","type":"tuple"},{"internalType":"bytes","name":"context","type":"bytes"}],"name":"innerHandleOp","outputs":[{"internalType":"uint256","name":"actualGasCost","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint192","name":"","type":"uint192"}],"name":"nonceSequenceNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation","name":"op","type":"tuple"},{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"targetCallData","type":"bytes"}],"name":"simulateHandleOp","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct UserOperation","name":"userOp","type":"tuple"}],"name":"simulateValidation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unlockStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"}],"name":"withdrawStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"},{"internalType":"uint256","name":"withdrawAmount","type":"uint256"}],"name":"withdrawTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] 29 | """ 30 | } 31 | -------------------------------------------------------------------------------- /Sources/userop-swift/context/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/24. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | import web3swift 12 | 13 | /// Wrap the response of `eth_sendUserOperation` RPC call 14 | public struct SendUserOperationResponse { 15 | public let userOpHash: String 16 | public let entryPoint: IEntryPoint 17 | 18 | /// Loop to wait the transaction to be mined 19 | /// 20 | /// - Returns: `UserOperationEvent` event log 21 | public func wait() async throws -> EventLog? { 22 | let end = Date().addingTimeInterval(300) 23 | while Date().distance(to: end) > 0 { 24 | let events = try await entryPoint.queryUserOperationEvent(userOpHash: userOpHash) 25 | if !events.isEmpty { 26 | return events[0] 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | } 33 | 34 | public protocol IClient { 35 | func buildUserOperation(builder: IUserOperationBuilder) async throws -> UserOperation 36 | 37 | func sendUserOperation(builder: IUserOperationBuilder, onBuild: ((UserOperation) -> Void)?) async throws -> SendUserOperationResponse 38 | } 39 | 40 | extension IClient { 41 | public func sendUserOperation(builder: IUserOperationBuilder) async throws -> SendUserOperationResponse { 42 | try await sendUserOperation(builder: builder, onBuild: nil) 43 | } 44 | } 45 | 46 | public class Client: IClient { 47 | private let provider: JsonRpcProvider 48 | private let web3: Web3 49 | 50 | public let entryPoint: EntryPoint 51 | 52 | public var chainId: BigUInt { 53 | web3.provider.network!.chainID 54 | } 55 | 56 | public init(rpcUrl: URL, 57 | overrideBundlerRpc: URL? = nil, 58 | entryPoint: EthereumAddress) async throws { 59 | self.provider = try await BundlerJsonRpcProvider(url: rpcUrl, bundlerRpc: overrideBundlerRpc) 60 | self.web3 = Web3(provider: provider) 61 | self.entryPoint = EntryPoint(web3: web3, address: entryPoint) 62 | } 63 | 64 | public func buildUserOperation(builder: IUserOperationBuilder) async throws -> UserOperation { 65 | try await builder.build(entryPoint: entryPoint.address, chainId: chainId) 66 | } 67 | 68 | public func sendUserOperation(builder: IUserOperationBuilder, onBuild: ((UserOperation) -> Void)?) async throws -> SendUserOperationResponse { 69 | let op = try await buildUserOperation(builder: builder) 70 | onBuild?(op) 71 | 72 | defer { 73 | builder.reset() 74 | } 75 | 76 | return try await sendUserOperation(userOp: op) 77 | } 78 | 79 | public func sendUserOperation(userOp: UserOperation) async throws -> SendUserOperationResponse { 80 | let userOphash: String = try await provider.send("eth_sendUserOperation", parameter: [userOp, entryPoint.address]).result 81 | return .init(userOpHash: userOphash, entryPoint: entryPoint) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/userop-swift/context/UserOperationMiddlewareContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserOperationMiddlewareContext.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/21. 6 | // 7 | 8 | import BigInt 9 | import Foundation 10 | import Web3Core 11 | 12 | public protocol UserOperationMiddlewareContextType { 13 | var op: UserOperation { get set } 14 | var entryPoint: EthereumAddress { get } 15 | var chainId: BigUInt { get } 16 | 17 | func getUserOpHash() -> String 18 | } 19 | 20 | public class UserOperationMiddlewareContext: UserOperationMiddlewareContextType { 21 | public var op: UserOperation 22 | 23 | public let entryPoint: EthereumAddress 24 | 25 | public let chainId: BigUInt 26 | 27 | public init(op: UserOperation, entryPoint: EthereumAddress, chainId: BigUInt) { 28 | self.op = op 29 | self.entryPoint = entryPoint 30 | self.chainId = chainId 31 | } 32 | 33 | /// Encode `UserOperationHash` 34 | /// - Returns: UserOperationHash 35 | public func getUserOpHash() -> String { 36 | let packed = ABIEncoder.encode(types: [ 37 | .address, 38 | .uint(bits: 256), 39 | .bytes(length: 32), 40 | .bytes(length: 32), 41 | .uint(bits: 256), 42 | .uint(bits: 256), 43 | .uint(bits: 256), 44 | .uint(bits: 256), 45 | .uint(bits: 256), 46 | .bytes(length: 32) 47 | ], values: [ 48 | op.sender, 49 | op.nonce, 50 | op.initCode.sha3(.keccak256), 51 | op.callData.sha3(.keccak256), 52 | op.callGasLimit, 53 | op.verificationGasLimit, 54 | op.preVerificationGas, 55 | op.maxFeePerGas, 56 | op.maxPriorityFeePerGas, 57 | op.paymasterAndData.sha3(.keccak256) 58 | ])! 59 | 60 | let enc = ABIEncoder.encode(types: [ 61 | .bytes(length: 32), 62 | .address, 63 | .uint(bits: 256) 64 | ], values: [ 65 | packed.sha3(.keccak256), 66 | entryPoint, 67 | chainId 68 | ])! 69 | 70 | return enc.sha3(.keccak256).toHexString() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/userop-swift/contracts/EntryPoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntryPoint.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | import web3swift 12 | 13 | public protocol IEntryPoint { 14 | var contract: EthereumContract { get } 15 | 16 | func getSenderAddress(initCode: Data) async throws -> EthereumAddress 17 | func getNonce(sender: EthereumAddress, key: BigInt) async throws -> BigUInt 18 | func queryUserOperationEvent(userOpHash: Hash) async throws -> [EventLog] 19 | } 20 | 21 | public class EntryPoint: IEntryPoint { 22 | public var web3: Web3 23 | public var address: EthereumAddress 24 | public let contract: EthereumContract 25 | 26 | init(web3: Web3, address: EthereumAddress) { 27 | self.web3 = web3 28 | self.address = address 29 | self.contract = try! EthereumContract(Abi.entryPoint, at: address) 30 | } 31 | 32 | public func getSenderAddress(initCode: Data) async throws -> EthereumAddress { 33 | do { 34 | try await contract.callStatic("getSenderAddress", parameters: [initCode], provider: web3.provider) 35 | throw Web3Error.dataError 36 | } catch Web3Error.revertCustom(_, let args) { 37 | guard let address = args["sender"] as? EthereumAddress else { 38 | throw Web3Error.dataError 39 | } 40 | return address 41 | } 42 | } 43 | 44 | public func getNonce(sender: EthereumAddress, key: BigInt) async throws -> BigUInt { 45 | let response = try await contract.callStatic("getNonce", parameters: [sender, key], provider: web3.provider) 46 | guard let nonce = response["0"] as? BigUInt else { 47 | throw Web3Error.dataError 48 | } 49 | return nonce 50 | } 51 | 52 | public func queryUserOperationEvent(userOpHash: Hash) async throws -> [EventLog] { 53 | let block = try await web3.eth.block(by: .latest) 54 | return try await contract.queryFilter("UserOperationEvent", parameters: [userOpHash], fromBlock: .exact(block.number - 100), provider: web3.provider) 55 | } 56 | } 57 | 58 | extension ContractProtocol { 59 | func queryFilter(_ event: String, parameters: [Encodable], fromBlock: BlockNumber = .latest, toBlock: BlockNumber = .latest, provider: Web3Provider) async throws -> [EventLog] { 60 | guard let event = events[event], let address = address else { 61 | throw Web3Error.dataError 62 | } 63 | let topics = event.encodeParameters(parameters) 64 | let filter = EventFilterParameters(fromBlock: fromBlock, toBlock: toBlock, address: [address], topics: topics) 65 | return try await provider.send(.getLogs(filter)).result 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/userop-swift/contracts/P256Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P256Account.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/30. 6 | // 7 | 8 | import Foundation 9 | import Web3Core 10 | import web3swift 11 | 12 | public protocol IP256Account { 13 | var contract: EthereumContract { get } 14 | } 15 | 16 | public class P256Account: IP256Account { 17 | public var web3: Web3 18 | public var address: EthereumAddress 19 | public let contract: EthereumContract 20 | 21 | init(web3: Web3, address: EthereumAddress) { 22 | self.web3 = web3 23 | self.address = address 24 | self.contract = try! EthereumContract(Abi.p256Account, at: address) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/userop-swift/contracts/P256AccountFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P256AccountFactory.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/30. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | import web3swift 12 | 13 | public protocol IP256AccountFactory { 14 | var contract: EthereumContract { get } 15 | 16 | func getAddress(publicKey: Data, salt: BigInt) async throws -> EthereumAddress 17 | } 18 | 19 | public class P256AccountFactory: IP256AccountFactory { 20 | public var web3: Web3 21 | public var address: EthereumAddress 22 | public let contract: EthereumContract 23 | 24 | init(web3: Web3, address: EthereumAddress) { 25 | self.web3 = web3 26 | self.address = address 27 | self.contract = try! EthereumContract(Abi.p256AccountFactory, at: address) 28 | } 29 | 30 | public func getAddress(publicKey: Data, salt: BigInt) async throws -> EthereumAddress { 31 | let data = contract.method("getAddress", parameters: [publicKey, salt], extraData: nil)! 32 | let transaction = CodableTransaction(to: address, data: data) 33 | let result = try await web3.eth.callTransaction(transaction) 34 | let decoded = try contract.decodeReturnData("getAddress", data: result) 35 | guard let returnAddress = decoded[""] as? EthereumAddress else { 36 | throw Web3Error.typeError 37 | } 38 | return returnAddress 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/userop-swift/contracts/SimpleAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleAccount.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import Web3Core 10 | import web3swift 11 | 12 | public protocol ISimpleAccount { 13 | var contract: EthereumContract { get } 14 | } 15 | 16 | public class SimpleAccount: ISimpleAccount { 17 | public var web3: Web3 18 | public var address: EthereumAddress 19 | public let contract: EthereumContract 20 | 21 | init(web3: Web3, address: EthereumAddress) { 22 | self.web3 = web3 23 | self.address = address 24 | self.contract = try! EthereumContract(Abi.entryPoint, at: address) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/userop-swift/contracts/SimpleAccountFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleAccountFactory.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | import web3swift 12 | 13 | public protocol ISimpleAccountFactory { 14 | var contract: EthereumContract { get } 15 | 16 | func getAddress(owner: EthereumAddress, salt: BigInt) async throws -> EthereumAddress 17 | } 18 | 19 | public class SimpleAccountFactory: ISimpleAccountFactory { 20 | public var web3: Web3 21 | public var address: EthereumAddress 22 | public let contract: EthereumContract 23 | 24 | init(web3: Web3, address: EthereumAddress) { 25 | self.web3 = web3 26 | self.address = address 27 | self.contract = try! EthereumContract(Abi.simpleAccountFactory, at: address) 28 | } 29 | 30 | public func getAddress(owner: EthereumAddress, salt: BigInt) async throws -> EthereumAddress { 31 | let data = contract.method("getAddress", parameters: [owner, salt], extraData: nil)! 32 | let transaction = CodableTransaction(to: address, data: data) 33 | let result = try await web3.eth.callTransaction(transaction) 34 | let decoded = try contract.decodeReturnData("getAddress", data: result) 35 | guard let returnAddress = decoded[""] as? EthereumAddress else { 36 | throw Web3Error.typeError 37 | } 38 | return returnAddress 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/userop-swift/middleware/GasEstimateMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GasEstimateMiddleware.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | 12 | struct GasEstimate: APIResultType { 13 | let preVerificationGas: BigUInt 14 | let verificationGasLimit: BigUInt 15 | let callGasLimit: BigUInt 16 | } 17 | 18 | extension GasEstimate { 19 | enum CodingKeys: CodingKey { 20 | case preVerificationGas 21 | case verificationGasLimit 22 | case callGasLimit 23 | } 24 | 25 | init(from decoder: Decoder) throws { 26 | let container = try decoder.container(keyedBy: CodingKeys.self) 27 | do { 28 | let preVerificationGas = try container.decodeHex(BigUInt.self, forKey: .preVerificationGas) 29 | let verificationGasLimit = try container.decodeHex(BigUInt.self, forKey: .verificationGasLimit) 30 | let callGasLimit = try container.decodeHex(BigUInt.self, forKey: .callGasLimit) 31 | self.init(preVerificationGas: preVerificationGas, 32 | verificationGasLimit: verificationGasLimit, 33 | callGasLimit: callGasLimit) 34 | } catch { 35 | let preVerificationGas = try container.decode(Int.self, forKey: .preVerificationGas) 36 | let verificationGasLimit = try container.decode(Int.self, forKey: .verificationGasLimit) 37 | let callGasLimit = try container.decode(Int.self, forKey: .callGasLimit) 38 | self.init(preVerificationGas: BigUInt(preVerificationGas), 39 | verificationGasLimit: BigUInt(verificationGasLimit), 40 | callGasLimit: BigUInt(callGasLimit)) 41 | } 42 | } 43 | } 44 | 45 | /// Middleware to estiamte `UserOperation` gas from bundler server. 46 | public struct GasEstimateMiddleware: UserOperationMiddleware { 47 | let rpcProvider: JsonRpcProvider 48 | 49 | public init(rpcProvider: JsonRpcProvider) { 50 | self.rpcProvider = rpcProvider 51 | } 52 | 53 | public func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 54 | let estimate: GasEstimate = try await rpcProvider.send("eth_estimateUserOperationGas", parameter: [ctx.op, ctx.entryPoint]).result 55 | 56 | ctx.op.preVerificationGas = estimate.preVerificationGas 57 | ctx.op.verificationGasLimit = estimate.verificationGasLimit 58 | ctx.op.callGasLimit = estimate.callGasLimit 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/userop-swift/middleware/GasPriceMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GasPriceMiddleware.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import BigInt 10 | import Web3Core 11 | 12 | /// Middleware to get gas price from rpc server. 13 | /// 14 | /// If rpc server did not provider `eth_maxPriorityFeePerGas` method, user `eth_getPrice` instead. 15 | public struct GasPriceMiddleware: UserOperationMiddleware { 16 | private let provider: JsonRpcProvider 17 | 18 | public init(provider: JsonRpcProvider) { 19 | self.provider = provider 20 | } 21 | 22 | public func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 23 | do { 24 | let (maxFeePerGas, maxPriorityFeePerGas) = try await eip1559GasPrice() 25 | ctx.op.maxFeePerGas = maxFeePerGas 26 | ctx.op.maxPriorityFeePerGas = maxPriorityFeePerGas 27 | } catch { 28 | let (maxFeePerGas, maxPriorityFeePerGas) = try await legacyGasPrice() 29 | ctx.op.maxFeePerGas = maxFeePerGas 30 | ctx.op.maxPriorityFeePerGas = maxPriorityFeePerGas 31 | } 32 | } 33 | 34 | private func eip1559GasPrice() async throws -> (BigUInt, BigUInt) { 35 | let fee: BigUInt = try await provider.send("eth_maxPriorityFeePerGas", parameter: []).result 36 | let block: Block = try await provider.send(.getBlockByNumber(.latest, false)).result 37 | 38 | let buffer = fee / 100 * 13 39 | let maxPriorityFeePerGas = fee + buffer 40 | let maxFeePerGas = block.baseFeePerGas != nil ? block.baseFeePerGas! * 2 + maxPriorityFeePerGas : maxPriorityFeePerGas 41 | return (maxFeePerGas, maxPriorityFeePerGas) 42 | } 43 | 44 | private func legacyGasPrice() async throws -> (BigUInt, BigUInt) { 45 | let gas: BigUInt = try await provider.send(.gasPrice).result 46 | return (gas, gas) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/userop-swift/middleware/SignatureMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignatureMiddleware.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/30. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Middleware to sign `UserOperation` signature 11 | public struct SignatureMiddleware: UserOperationMiddleware { 12 | let signer: Signer 13 | 14 | public init(signer: Signer) { 15 | self.signer = signer 16 | } 17 | 18 | public func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 19 | ctx.op.signature = try await signer.signMessage(Data(hex: ctx.getUserOpHash())) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/userop-swift/middleware/UserOperationMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by liugang zhang on 2023/8/21. 3 | // 4 | 5 | import BigInt 6 | import Foundation 7 | 8 | /// Implement middleware protocol to process `UserOperation` 9 | public protocol UserOperationMiddleware { 10 | func process(_ ctx: inout UserOperationMiddlewareContext) async throws 11 | } 12 | -------------------------------------------------------------------------------- /Sources/userop-swift/middleware/VerifyingPaymaster.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerifyingPaymaster.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/22. 6 | // 7 | 8 | import BigInt 9 | import Foundation 10 | import web3swift 11 | import Web3Core 12 | 13 | struct VerifyingPaymasterResult: APIResultType { 14 | var paymasterAndData: Data 15 | var preVerificationGas: BigUInt 16 | var verificationGasLimit: BigUInt 17 | var callGasLimit: BigUInt 18 | } 19 | 20 | extension VerifyingPaymasterResult { 21 | enum CodingKeys: CodingKey { 22 | case paymasterAndData 23 | case preVerificationGas 24 | case verificationGasLimit 25 | case callGasLimit 26 | } 27 | 28 | init(from decoder: Decoder) throws { 29 | let container = try decoder.container(keyedBy: CodingKeys.self) 30 | let paymasterAndData = try container.decodeHex(Data.self, forKey: .paymasterAndData) 31 | do { 32 | let preVerificationGas = try container.decodeHex(BigUInt.self, forKey: .preVerificationGas) 33 | let verificationGasLimit = try container.decodeHex(BigUInt.self, forKey: .verificationGasLimit) 34 | let callGasLimit = try container.decodeHex(BigUInt.self, forKey: .callGasLimit) 35 | self.init(paymasterAndData: paymasterAndData, 36 | preVerificationGas: preVerificationGas, 37 | verificationGasLimit: verificationGasLimit, 38 | callGasLimit: callGasLimit) 39 | } catch { 40 | let preVerificationGas = try container.decode(Int.self, forKey: .preVerificationGas) 41 | let verificationGasLimit = try container.decode(Int.self, forKey: .verificationGasLimit) 42 | let callGasLimit = try container.decode(Int.self, forKey: .callGasLimit) 43 | self.init(paymasterAndData: paymasterAndData, 44 | preVerificationGas: BigUInt(preVerificationGas), 45 | verificationGasLimit: BigUInt(verificationGasLimit), 46 | callGasLimit: BigUInt(callGasLimit)) 47 | } 48 | } 49 | } 50 | 51 | /// Middleware to get gas sponsor from paymaster service. 52 | public struct VerifyingPaymasterMiddleware: UserOperationMiddleware { 53 | let paymasterRpc: URL 54 | 55 | public init(paymasterRpc: URL) { 56 | self.paymasterRpc = paymasterRpc 57 | } 58 | 59 | public func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 60 | ctx.op.verificationGasLimit = ctx.op.verificationGasLimit * 3 61 | let provider = try await JsonRpcProvider(url: paymasterRpc, ignoreNet: true) 62 | 63 | let response: VerifyingPaymasterResult = try await provider.send("pm_sponsorUserOperation", parameter: [ctx.op, ctx.entryPoint, ""]).result 64 | 65 | ctx.op.paymasterAndData = response.paymasterAndData 66 | ctx.op.preVerificationGas = response.preVerificationGas 67 | ctx.op.verificationGasLimit = response.verificationGasLimit 68 | ctx.op.callGasLimit = response.callGasLimit 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/userop-swift/preset/P256AccountBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P256AccountBuilder.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/30. 6 | // 7 | 8 | import BigInt 9 | import Foundation 10 | import Web3Core 11 | import web3swift 12 | 13 | /// Implement `UserOperationBuilder` for P256Account 14 | public class P256AccountBuilder: UserOperationBuilder { 15 | /// If sender has been deployed, reset `initCode` to empty 16 | struct ResolveAccountMiddleware: UserOperationMiddleware { 17 | private let entryPoint: IEntryPoint 18 | private let initCode: Data 19 | 20 | init(entryPoint: IEntryPoint, initCode: Data) { 21 | self.entryPoint = entryPoint 22 | self.initCode = initCode 23 | } 24 | 25 | func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 26 | ctx.op.nonce = try await entryPoint.getNonce(sender: ctx.op.sender, key: 0) 27 | ctx.op.initCode = ctx.op.nonce == 0 ? initCode : Data() 28 | } 29 | } 30 | 31 | private let signer: Signer 32 | private let web3: Web3 33 | private let provider: JsonRpcProvider 34 | public let entryPoint: EntryPoint 35 | public let factory: IP256AccountFactory 36 | public let proxy: IP256Account 37 | 38 | /// Initializer 39 | /// 40 | /// - Parameters: 41 | /// - signer: A signer to sign `UserOperation` 42 | /// - rpcUrl: JSON-RPC server address 43 | /// - bundleRpcUrl: Bundler address. if not porvided, bundler RPC methods will be sent to `rpcUrl` 44 | /// - entryPoint: EntryPoint contract address 45 | /// - factory: SimpleAccountFactory contract address 46 | /// - salt: Salt, if sender is not nil and has been deployed, this parameter will be ignored 47 | /// - senderAddress: Pass this parameter if already got from entryPoint 48 | /// - paymasterMiddleware: Paymaster entry address, if not provided, `GasEstimateMiddleware` will be used 49 | public init(signer: Signer, 50 | rpcUrl: URL, 51 | bundleRpcUrl: URL? = nil, 52 | entryPoint: EthereumAddress, 53 | factory: EthereumAddress, 54 | salt: BigInt? = nil, 55 | senderAddress: EthereumAddress? = nil, 56 | paymasterMiddleware: UserOperationMiddleware? = nil) async throws { 57 | self.signer = signer 58 | self.provider = try await BundlerJsonRpcProvider(url: rpcUrl, bundlerRpc: bundleRpcUrl) 59 | self.web3 = Web3(provider: self.provider) 60 | self.entryPoint = EntryPoint(web3: web3, address: entryPoint) 61 | self.factory = P256AccountFactory(web3: web3, address: entryPoint) 62 | self.proxy = P256Account(web3: web3, address: EthereumAddress.zero) 63 | super.init() 64 | 65 | initCode = try await factory.addressData + 66 | self.factory.contract.method("createAccount", parameters: [signer.getPublicKey(), salt ?? 0], extraData: nil)! 67 | 68 | if let senderAddress = senderAddress { 69 | self.proxy.contract.address = senderAddress 70 | self.sender = senderAddress 71 | } else { 72 | let address = try await self.entryPoint.getSenderAddress(initCode: initCode) 73 | self.proxy.contract.address = address 74 | self.sender = address 75 | } 76 | self.signature = try await signer.signMessage(Data(hex: "0xdead").sha3(.keccak256)) 77 | 78 | useMiddleware(ResolveAccountMiddleware(entryPoint: self.entryPoint, initCode: initCode)) 79 | useMiddleware(GasPriceMiddleware(provider: provider)) 80 | useMiddleware(paymasterMiddleware != nil ? paymasterMiddleware! : GasEstimateMiddleware(rpcProvider: provider)) 81 | useMiddleware(SignatureMiddleware(signer: signer)) 82 | } 83 | 84 | public func execute(to: EthereumAddress, value: BigUInt, data: Data) { 85 | callData = proxy.contract.method("execute", parameters: [to, value, data], extraData: nil)! 86 | } 87 | 88 | public func executeBatch(to: [EthereumAddress], value: [BigUInt], data: [Data]) { 89 | callData = proxy.contract.method("executeBatch", parameters: [to, value, data], extraData: nil)! 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/userop-swift/preset/SimpleAccountBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleAccountBuilder.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/22. 6 | // 7 | 8 | import BigInt 9 | import Foundation 10 | import Web3Core 11 | import web3swift 12 | 13 | public class SimpleAccountBuilder: UserOperationBuilder { 14 | struct ResolveAccountMiddleware: UserOperationMiddleware { 15 | private let entryPoint: IEntryPoint 16 | private let initCode: Data 17 | 18 | init(entryPoint: IEntryPoint, initCode: Data) { 19 | self.entryPoint = entryPoint 20 | self.initCode = initCode 21 | } 22 | 23 | func process(_ ctx: inout UserOperationMiddlewareContext) async throws { 24 | ctx.op.nonce = try await entryPoint.getNonce(sender: ctx.op.sender, key: 0) 25 | ctx.op.initCode = ctx.op.nonce == 0 ? initCode : Data() 26 | } 27 | } 28 | 29 | private let signer: Signer 30 | private let web3: Web3 31 | private let provider: JsonRpcProvider 32 | private let entryPoint: EntryPoint 33 | private let factory: SimpleAccountFactory 34 | private let proxy: SimpleAccount 35 | 36 | public init(signer: Signer, 37 | rpcUrl: URL, 38 | bundleRpcUrl: URL? = nil, 39 | entryPoint: EthereumAddress, 40 | factory: EthereumAddress, 41 | salt: BigInt? = nil, 42 | senderAddress: EthereumAddress? = nil, 43 | paymasterMiddleware: UserOperationMiddleware? = nil) async throws { 44 | self.signer = signer 45 | self.provider = try await BundlerJsonRpcProvider(url: rpcUrl, bundlerRpc: bundleRpcUrl) 46 | self.web3 = Web3(provider: self.provider) 47 | self.entryPoint = EntryPoint(web3: web3, address: entryPoint) 48 | self.factory = SimpleAccountFactory(web3: web3, address: entryPoint) 49 | self.proxy = SimpleAccount(web3: web3, address: EthereumAddress.zero) 50 | super.init() 51 | 52 | initCode = await factory.addressData + 53 | self.factory.contract.method("createAccount", parameters: [signer.getAddress(), salt ?? 0], extraData: nil)! 54 | 55 | if let senderAddress = senderAddress { 56 | self.proxy.address = senderAddress 57 | self.sender = senderAddress 58 | } else { 59 | let address = try await self.entryPoint.getSenderAddress(initCode: initCode) 60 | self.proxy.address = address 61 | self.sender = address 62 | } 63 | self.signature = try await signer.signMessage(Data(hex: "0xdead").sha3(.keccak256)) 64 | 65 | useMiddleware(ResolveAccountMiddleware(entryPoint: self.entryPoint, initCode: initCode)) 66 | useMiddleware(GasPriceMiddleware(provider: provider)) 67 | useMiddleware(paymasterMiddleware != nil ? paymasterMiddleware! : GasEstimateMiddleware(rpcProvider: provider)) 68 | useMiddleware(SignatureMiddleware(signer: signer)) 69 | } 70 | 71 | public func execute(to: EthereumAddress, value: BigUInt, data: Data) async throws { 72 | callData = proxy.contract.method("execute", parameters: [to, value, data], extraData: nil)! 73 | } 74 | 75 | public func executeBatch(to: [EthereumAddress], data: [Data]) async throws { 76 | callData = proxy.contract.method("executeBatch", parameters: [to, data], extraData: nil)! 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/userop-swift/provider/BundlerJsonRpcProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundlerJsonRpcProvider.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/23. 6 | // 7 | 8 | import Foundation 9 | import web3swift 10 | import Web3Core 11 | 12 | public class BundlerJsonRpcProvider: JsonRpcProvider { 13 | private var bundlerProvider: JsonRpcProvider? = nil 14 | private let bundlerMethods = [ 15 | "eth_sendUserOperation", 16 | "eth_estimateUserOperationGas", 17 | "eth_getUserOperationByHash", 18 | "eth_getUserOperationReceipt", 19 | "eth_supportedEntryPoints" 20 | ] 21 | 22 | public init(url: URL, bundlerRpc: URL? = nil, network net: Networks? = nil, ignoreNet: Bool = false) async throws { 23 | try await super.init(url: url, network: net, ignoreNet: ignoreNet) 24 | if let bundlerRpc = bundlerRpc { 25 | self.bundlerProvider = try await JsonRpcProvider(url: bundlerRpc, network: network) 26 | } 27 | } 28 | 29 | public override func send(_ method: String, parameter: [Encodable]) async throws -> APIResponse { 30 | if bundlerMethods.contains(method) && bundlerProvider != nil { 31 | return try await bundlerProvider!.send(method, parameter: parameter) 32 | } 33 | return try await super.send(method, parameter: parameter) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/userop-swift/provider/JsonRpcProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JsonRpcProvider.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/22. 6 | // 7 | 8 | import Foundation 9 | import Web3Core 10 | import BigInt 11 | 12 | public class JsonRpcProvider: Web3Provider { 13 | public let url: URL 14 | public var network: Networks? 15 | public var policies: Policies = .auto 16 | public var attachedKeystoreManager: KeystoreManager? = nil 17 | public var session: URLSession = {() -> URLSession in 18 | let config = URLSessionConfiguration.default 19 | let urlSession = URLSession(configuration: config) 20 | return urlSession 21 | }() 22 | 23 | public init(url: URL, network net: Networks? = nil, ignoreNet: Bool = false) async throws { 24 | guard url.scheme == "http" || url.scheme == "https" else { 25 | throw Web3Error.inputError(desc: "Web3HttpProvider endpoint must have scheme http or https. Given scheme \(url.scheme ?? "none"). \(url.absoluteString)") 26 | } 27 | 28 | self.url = url 29 | if let net = net { 30 | network = net 31 | } else if !ignoreNet { 32 | /// chain id could be a hex string or an int value. 33 | let response: String = try await APIRequest.send(APIRequest.getNetwork.call, parameter: [], with: self).result 34 | let result: UInt 35 | if response.hasHexPrefix() { 36 | result = UInt(BigUInt(response, radix: 16) ?? Networks.Mainnet.chainID) 37 | } else { 38 | result = UInt(response) ?? UInt(Networks.Mainnet.chainID) 39 | } 40 | self.network = Networks.fromInt(result) 41 | } 42 | } 43 | 44 | public func send(_ method: String, parameter: [Encodable]) async throws -> APIResponse { 45 | return try await APIRequest.send(method, parameter: parameter, with: self) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/userop-swift/provider/Web3Provider+Ex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Web3Provider+Ex.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/25. 6 | // 7 | 8 | import Foundation 9 | import Web3Core 10 | 11 | extension Web3Provider { 12 | public func send(_ call: APIRequest) async throws -> APIResponse { 13 | try await APIRequest.sendRequest(with: self, for: call) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/userop-swiftTests/P256AccountTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P256AccountTests.swift 3 | // 4 | // 5 | // Created by liugang zhang on 2023/8/30. 6 | // 7 | 8 | import XCTest 9 | import Web3Core 10 | import web3swift 11 | import BigInt 12 | 13 | @testable import userop_swift 14 | 15 | final class P256AccountTests: XCTestCase { 16 | // func testBindEmail() async throws { 17 | // let account = try await P256AccountBuilder(signer: P256R1Signer(), 18 | // rpcUrl: rpc, 19 | // bundleRpcUrl: bundler, 20 | // entryPoint: entryPointAddress, 21 | // factory: factoryAddress, 22 | // salt: 1) 23 | // let data = account.proxy.contract.method("addEmailGuardian", parameters: [ 24 | // "0x36387ffce3ddd8c35b790148d6e6134689f74fe32471a27e8a243634ce213098", 25 | // "0x416bf2958e0965619fe574411312d6963673c87443f2ca65b34cc4415badc96749b5509d0ef2c43000e34fdd9ef5503bf2a12963c0190c25c1f56889d2efb9031b" 26 | // ], extraData: nil)! 27 | // account.execute(to: account.sender, value: 0, data: data) 28 | // 29 | // let client = try await Client(rpcUrl: rpc, overrideBundlerRpc: bundler, entryPoint: entryPointAddress) 30 | // let response = try await client.sendUserOperation(builder: account) 31 | // } 32 | // 33 | // func testRemoveEmail() async throws { 34 | // let account = try await P256AccountBuilder(signer: P256R1Signer(), 35 | // rpcUrl: rpc, 36 | // bundleRpcUrl: bundler, 37 | // entryPoint: entryPointAddress, 38 | // factory: factoryAddress, 39 | // salt: 1) 40 | // let data = account.proxy.contract.method("removeEmailGuardian", parameters: [], extraData: nil)! 41 | // account.execute(to: account.sender, value: 0, data: data) 42 | // 43 | // let client = try await Client(rpcUrl: rpc, overrideBundlerRpc: bundler, entryPoint: entryPointAddress) 44 | // let response = try await client.sendUserOperation(builder: account) 45 | // } 46 | } 47 | -------------------------------------------------------------------------------- /userop.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'userop' 3 | spec.version = '0.0.1' 4 | spec.ios.deployment_target = "14.0" 5 | spec.osx.deployment_target = "12.0" 6 | spec.license = { :type => 'MIT License', :file => 'LICENSE.md' } 7 | spec.summary = 'swift version of https://github.com/stackup-wallet/userop.js' 8 | spec.homepage = 'https://github.com/iotexproject/userop-swift' 9 | spec.author = { 'zhanliugang' => 'zhangliugang@gmail.com' } 10 | spec.source = { :git => 'https://github.com/iotexproject/userop-swift.git', :tag => spec.version.to_s } 11 | spec.swift_version = '5.8' 12 | 13 | spec.source_files = "Sources/userop-swift/**/*.swift" 14 | spec.frameworks = 'Foundation' 15 | 16 | spec.dependency 'Web3Core' 17 | spec.dependency 'web3swift' 18 | end 19 | --------------------------------------------------------------------------------