├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 1.bug_report.yml │ └── config.yml └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example ├── Podfile ├── README.md ├── Tests │ ├── CryptoTests.swift │ ├── DeeplinkClientTests.swift │ ├── DeeplinkManagerTests.swift │ ├── EthereumConnectTests.swift │ ├── EthereumConvenienceMethodsTests.swift │ ├── EthereumRequestTests.swift │ ├── EthereumTests.swift │ ├── Info.plist │ ├── InfuraProviderTests.swift │ ├── KeyExchangeTests.swift │ ├── MockCommClient.swift │ ├── MockCommClientFactory.swift │ ├── MockDeeplinkCommClient.swift │ ├── MockDeeplinkManager.swift │ ├── MockEthereumDelegate.swift │ ├── MockInfuraProvider.swift │ ├── MockKeyExchange.swift │ ├── MockNetwork.swift │ ├── MockSessionManager.swift │ ├── MockSocket.swift │ ├── MockSocketChannel.swift │ ├── MockSocketCommClient.swift │ ├── MockSocketManager.swift │ ├── MockURLOpener.swift │ ├── SecureStorageTests.swift │ ├── SessionConfigTests.swift │ ├── SessionManagerTests.swift │ ├── SocketChannelTests.swift │ ├── SocketClientTests.swift │ ├── TestCodableData.swift │ └── Tests.swift ├── metamask-ios-sdk.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── metamask-ios-sdk-Example.xcscheme ├── metamask-ios-sdk.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── metamask-ios-sdk │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── ButtonStyle.swift │ ├── ConnectView.swift │ ├── Curvature.swift │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ ├── NetworkView.swift │ ├── ReadOnlyCallsView.swift │ ├── SignView.swift │ ├── SwitchChainView.swift │ ├── TextStyle.swift │ ├── ToastOverlay.swift │ ├── ToastView.swift │ ├── TransactionView.swift │ ├── ViewController.swift │ └── ViewExtension.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── metamask-ios-sdk │ ├── Assets │ └── .gitkeep │ ├── Classes │ ├── API │ │ ├── Endpoint.swift │ │ ├── InfuraProvider.swift │ │ ├── Network.swift │ │ └── NetworkError.swift │ ├── Analytics │ │ ├── Analytics.swift │ │ └── Event.swift │ ├── Backgroundtaskmanager │ │ └── BackgroundTaskManager.swift │ ├── CommunicationLayer │ │ ├── CommClient.swift │ │ ├── CommClientFactory.swift │ │ ├── CommLayer.swift │ │ ├── DeeplinkCommLayer │ │ │ ├── Deeplink.swift │ │ │ ├── DeeplinkClient.swift │ │ │ ├── DeeplinkManager.swift │ │ │ ├── String.swift │ │ │ └── URLOpener.swift │ │ └── SocketCommLayer │ │ │ ├── ClientEvent.swift │ │ │ ├── SocketChannel.swift │ │ │ ├── SocketClient.swift │ │ │ ├── SocketClientProtocol.swift │ │ │ └── SocketMessage.swift │ ├── Crypto │ │ ├── Crypto.swift │ │ ├── Encoding.swift │ │ └── KeyExchange.swift │ ├── DeviceInfo │ │ └── DeviceInfo.swift │ ├── Ethereum │ │ ├── AppMetadata.swift │ │ ├── ErrorType.swift │ │ ├── Ethereum.swift │ │ ├── EthereumMethod.swift │ │ ├── EthereumRequest.swift │ │ ├── EthereumWrapper.swift │ │ ├── RPCRequest.swift │ │ ├── RequestError.swift │ │ ├── ResponseMethod.swift │ │ ├── SubmitRequest.swift │ │ └── TimestampGenerator.swift │ ├── Extensions │ │ ├── NSRecursiveLock.swift │ │ └── Notification.swift │ ├── Logger │ │ └── Logging.swift │ ├── Models │ │ ├── AddChainParameters.swift │ │ ├── Mappable.swift │ │ ├── NativeCurrency.swift │ │ ├── OriginatorInfo.swift │ │ ├── RequestInfo.swift │ │ ├── SignContract.swift │ │ └── Typealiases.swift │ ├── Persistence │ │ ├── SecureStore.swift │ │ ├── SessionConfig.swift │ │ └── SessionManager.swift │ └── SDK │ │ ├── Dependencies.swift │ │ ├── MetaMaskSDK.swift │ │ ├── SDKInfo.swift │ │ └── SDKOptions.swift │ └── Frameworks │ └── Ecies.xcframework │ ├── Info.plist │ ├── ios-arm64-simulator │ ├── Headers │ │ ├── ecies.h │ │ └── module.modulemap │ └── libecies.a │ └── ios-arm64 │ ├── Headers │ ├── ecies.h │ └── module.modulemap │ └── libecies.a ├── metamask-ios-sdk.podspec └── sonar-project.properties /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @MetaMask/sdk-devs 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for the MetaMask iOS SDK 3 | labels: ["template: bug"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Provide environment information 8 | description: "Give us a general overview of the environment where the issue is happening" 9 | validations: 10 | required: true 11 | - type: input 12 | attributes: 13 | label: MetaMask iOS SDK Version 14 | description: "Please specify the exact MetaMask iOS SDK version" 15 | validations: 16 | required: true 17 | - type: input 18 | attributes: 19 | label: MetaMask Wallet Version 20 | description: "Please specify the exact MetaMask Wallet AppStore version or release version if compiling from source" 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: Xcode Version 26 | description: "Give us information about the Xcode version you are building your dapp from" 27 | validations: 28 | required: true 29 | - type: input 30 | attributes: 31 | label: iOS Version 32 | description: "Give us information about the iOS version your target device or simulator is running on" 33 | validations: 34 | required: true 35 | - type: textarea 36 | attributes: 37 | label: Describe the Bug 38 | description: A clear and concise description of what the bug is. 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Expected Behavior 44 | description: A clear and concise description of what you expected to happen. 45 | validations: 46 | required: true 47 | - type: input 48 | attributes: 49 | label: Link to reproduction - Issues with a link to complete (but minimal) reproduction code tend to be addressed faster 50 | description: A link to a GitHub repository or gist to recreate the issue. Include steps to run the repository and make sure it contains only code to reproduce the bug. I.E. don't invite us to your production application repo. 51 | validations: 52 | required: false 53 | - type: textarea 54 | attributes: 55 | label: Steps To Reproduce 56 | description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken. 57 | validations: 58 | required: true 59 | - type: markdown 60 | attributes: 61 | value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. 62 | - type: markdown 63 | attributes: 64 | value: Thank you for helping us improve the MetaMask iOS SDK by providing detailed bug reports. 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentation 4 | url: https://docs.metamask.io/sdk 5 | about: Check the official documentation for help using the SDK 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | sonarcloud-scan: 10 | name: Run SonarCloud Scan 11 | uses: MetaMask/metamask-sdk/.github/workflows/sonar-cloud.yml@535a911b02d28a61ab305841f8b14b83e91c7000 12 | secrets: 13 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | Example/Pods/ 5 | Example/Podfile.lock 6 | /*.xcodeproj 7 | xcuserdata/ 8 | DerivedData/ 9 | .swiftpm/config/registries.json 10 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 11 | .netrc 12 | .swiftformat 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/realm/SwiftLint 2 | rev: 0.55.1 3 | hooks: 4 | - id: swiftlint 5 | entry: swiftlint --fix --strict -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # By default, SwiftLint uses a set of sensible default rules you can adjust: 2 | disabled_rules: # rule identifiers turned on by default to exclude from running 3 | - control_statement 4 | opt_in_rules: # some rules are turned off by default, so you need to opt-in 5 | - empty_count # find all the available rules by running: `swiftlint rules` 6 | 7 | # Alternatively, specify all rules explicitly by uncommenting this option: 8 | # only_rules: # delete `disabled_rules` & `opt_in_rules` if using this 9 | # - empty_parameters 10 | # - vertical_whitespace 11 | 12 | analyzer_rules: # rules run by `swiftlint analyze` 13 | - explicit_self 14 | 15 | included: # case-sensitive paths to include during linting. `--path` is ignored if present 16 | - Sources/metamask-ios-sdk/Classes 17 | - Example/metamask-ios-sdk 18 | - Example/Tests 19 | excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` 20 | - Example/Pods 21 | - Sources/ExcludedFolder 22 | - Sources/ExcludedFile.swift 23 | - Sources/*/ExcludedFile.swift # exclude files with a wildcard 24 | 25 | # If true, SwiftLint will not fail if no lintable files are found. 26 | allow_zero_lintable_files: false 27 | 28 | # If true, SwiftLint will treat all warnings as errors. 29 | strict: false 30 | 31 | # configurable rules can be customized from this configuration file 32 | # binary rules can set their severity level 33 | # force_cast: warning # implicitly 34 | # force_try: 35 | # severity: warning # explicitly 36 | # rules that have both warning and error levels, can set just the warning level 37 | # implicitly 38 | line_length: 110 39 | # they can set both implicitly with an array 40 | type_body_length: 41 | - 300 # warning 42 | - 400 # error 43 | # or they can set both explicitly 44 | file_length: 45 | warning: 500 46 | error: 1200 47 | # naming rules can set warnings/errors for min_length and max_length 48 | # additionally they can set excluded names 49 | type_name: 50 | min_length: 2 # only warning 51 | max_length: # warning and error 52 | warning: 40 53 | error: 50 54 | excluded: iPhone # excluded via string 55 | allowed_symbols: ["_"] # these are allowed in type names 56 | identifier_name: 57 | min_length: # only min_length 58 | error: 4 # only error 59 | excluded: # excluded via string array 60 | - id 61 | - URL 62 | - GlobalAPIKey 63 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | platform :ios, '15.0' 4 | 5 | target 'metamask-ios-sdk_Example' do 6 | pod 'metamask-ios-sdk', :path => '../' 7 | 8 | target 'metamask-ios-sdk_Tests' do 9 | inherit! :search_paths 10 | end 11 | end 12 | 13 | post_install do |installer| 14 | installer.generated_projects.each do |project| 15 | project.targets.each do |target| 16 | target.build_configurations.each do |config| 17 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Example/README.md: -------------------------------------------------------------------------------- 1 | # iOS SDK example 2 | 3 | This iOS dapp is an example of how to connect to Ethereum and make requests using the SDK. 4 | The example requests include: 5 | 6 | - [`ConnectView.swift`](metamask-ios-sdk/ConnectView.swift) - Connect to the Ethereum blockchain 7 | using the SDK. 8 | The other examples are based on a successful connection as demonstrated in this example. 9 | - [`TransactionView.swift`](metamask-ios-sdk/TransactionView.swift) - Send a transaction. 10 | - [`SignView.swift`](metamask-ios-sdk/SignView.swift) - Sign a transaction. 11 | - [`SwitchChainView.swift`](metamask-ios-sdk/SwitchChainView.swift) - Switch to a different network 12 | chain (you need to call the 13 | [`wallet_addEthereumChain`](https://docs.metamask.io/wallet/reference/wallet_addethereumchain/) 14 | RPC method first if it doesn't already exist in the MetaMask wallet). 15 | 16 | To run the example dapp: 17 | 18 | 1. Make sure you meet the [prerequisites](../README.md#prerequisites). 19 | 2. Clone this repository. 20 | 3. Change directory to `metamask-ios-sdk/Example`. 21 | 4. Run `pod install`. 22 | 5. Open `metamask-ios-sdk.xcworkspace` and run the project. 23 | -------------------------------------------------------------------------------- /Example/Tests/CryptoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class CryptoTests: XCTestCase { 10 | 11 | func testGeneratePrivateKey() { 12 | let privateKey = Ecies.generatePrivateKey() 13 | XCTAssertNotNil(privateKey) 14 | } 15 | 16 | func testPublicKeyFromPrivateKey() { 17 | let privateKey = Ecies.generatePrivateKey() 18 | let publicKey = try? Ecies.publicKey(from: privateKey) 19 | XCTAssertNotNil(publicKey) 20 | } 21 | 22 | func testEncryptAndDecrypt() { 23 | // Generate a private key and corresponding public key 24 | let privateKey = Ecies.generatePrivateKey() 25 | 26 | // Define a plaintext message to be encrypted 27 | let plaintext = "Hello, crypto enthusiasts! :)" 28 | 29 | do { 30 | let publicKey = try Ecies.publicKey(from: privateKey) 31 | // Encrypt the plaintext 32 | let encryptedText = try Ecies.encrypt(plaintext, publicKey: publicKey) 33 | 34 | // Decrypt the encrypted text 35 | do { 36 | let decryptedText = try Ecies.decrypt(encryptedText, privateKey: privateKey) 37 | 38 | // Check if the decrypted text is the same as the original plaintext 39 | XCTAssertEqual(decryptedText, plaintext) 40 | } catch { 41 | XCTFail("CryptoTests:: Decryption failed with error: \(error)") 42 | } 43 | } catch { 44 | XCTFail("CryptoTests:: Encryption failed with error: \(error)") 45 | } 46 | } 47 | 48 | func testGeneratePublicKeyWithInvalidPrivateKeyShouldFail() { 49 | // Generate a private key and corresponding public key 50 | let privateKey = Ecies.generatePrivateKey() 51 | let modifiedPrivateKey = privateKey.dropLast().appending("") 52 | 53 | do { 54 | _ = try Ecies.publicKey(from: modifiedPrivateKey) 55 | XCTFail("CryptoTests:: Public key generation should fail") 56 | } catch { 57 | XCTAssert(error as? CryptoError == CryptoError.publicKeyGenerationFailure) 58 | } 59 | } 60 | 61 | func testEncryptWithInvalidPublicKeyShouldFail() { 62 | let privateKey = Ecies.generatePrivateKey() 63 | let plaintext = "Hello, crypto enthusiasts! :)" 64 | 65 | do { 66 | let publicKey = try Ecies.publicKey(from: privateKey) 67 | 68 | let modifiedPublicKey = publicKey.dropLast().appending("") 69 | _ = try Ecies.encrypt(plaintext, publicKey: modifiedPublicKey) 70 | XCTFail("CryptoTests:: Encryption with invalid public key should fail") 71 | } catch { 72 | XCTAssert(error as? CryptoError == CryptoError.encryptionFailure) 73 | } 74 | } 75 | 76 | func testDecryptWithInvalidPrivateKeyShouldFail() { 77 | let privateKey = Ecies.generatePrivateKey() 78 | let plaintext = "Hello, crypto enthusiasts! :)" 79 | 80 | do { 81 | let publicKey = try Ecies.publicKey(from: privateKey) 82 | 83 | let encryptedText = try Ecies.encrypt(plaintext, publicKey: publicKey) 84 | let modifiedPrivateKey = privateKey.dropLast().appending("") 85 | _ = try Ecies.decrypt(encryptedText, privateKey: modifiedPrivateKey) 86 | XCTFail("CryptoTests:: Decryption with invalid private key should fail") 87 | } catch { 88 | XCTAssert(error as? CryptoError == CryptoError.decryptionFailure) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Example/Tests/DeeplinkClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeeplinkClientTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class DeeplinkClientTests: XCTestCase { 10 | var deeplinkClient: DeeplinkClient! 11 | var mockDeeplinkManager: MockDeeplinkManager! 12 | 13 | var secureStore: SecureStore! 14 | var mockURLOpener: MockURLOpener! 15 | var mockKeyExchange: MockKeyExchange! 16 | var mockSessionManager: MockSessionManager! 17 | 18 | private let DAPP_SCHEME = "testDapp" 19 | 20 | override func setUp() { 21 | super.setUp() 22 | mockDeeplinkManager = MockDeeplinkManager() 23 | 24 | secureStore = Keychain(service: "com.example.deeplinkTestKeychain") 25 | mockKeyExchange = MockKeyExchange(storage: secureStore) 26 | mockURLOpener = MockURLOpener() 27 | mockSessionManager = MockSessionManager(store: secureStore, sessionDuration: 3600) 28 | deeplinkClient = DeeplinkClient( 29 | session: mockSessionManager, 30 | keyExchange: mockKeyExchange, 31 | deeplinkManager: mockDeeplinkManager, 32 | dappScheme: DAPP_SCHEME, 33 | urlOpener: mockURLOpener 34 | ) 35 | } 36 | 37 | func testSetupClient() { 38 | XCTAssertEqual(deeplinkClient.channelId, "mockSessionId") 39 | } 40 | 41 | func testSetupCallbacks() { 42 | XCTAssertNotNil(deeplinkClient.deeplinkManager.onReceiveMessage) 43 | XCTAssertNotNil(deeplinkClient.deeplinkManager.decryptMessage) 44 | } 45 | 46 | func testClearSession() { 47 | deeplinkClient.clearSession() 48 | XCTAssertTrue(mockSessionManager.clearCalled) 49 | XCTAssertTrue(mockSessionManager.fetchSessionConfigCalled) 50 | XCTAssertEqual(deeplinkClient.channelId, "mockSessionId") 51 | } 52 | 53 | func testGetSessionDuration() { 54 | XCTAssertEqual(deeplinkClient.sessionDuration, 3600) 55 | } 56 | 57 | func testSetSessionDuration() { 58 | deeplinkClient.sessionDuration = 60 59 | XCTAssertEqual(deeplinkClient.sessionDuration, 60) 60 | } 61 | 62 | func testHandleUrl() { 63 | let url = URL(string: "https://example.com")! 64 | deeplinkClient.handleUrl(url) 65 | XCTAssertTrue(mockDeeplinkManager.handleUrlCalled) 66 | } 67 | 68 | func testConnect() { 69 | deeplinkClient.connect(with: "testRequest") 70 | let openedUrl = mockURLOpener.openedURL?.absoluteString ?? "" 71 | XCTAssertTrue(openedUrl.contains("metamask://connect?")) 72 | XCTAssertTrue(openedUrl.contains("?scheme=testDapp")) 73 | XCTAssertTrue(openedUrl.contains("&channelId=mockSessionId")) 74 | XCTAssertTrue(openedUrl.contains("&comm=deeplinking")) 75 | XCTAssertTrue(openedUrl.contains("&request=testRequest")) 76 | } 77 | 78 | func testSendMessage() { 79 | deeplinkClient.sendMessage("testMessage") 80 | XCTAssertEqual(mockURLOpener.openedURL?.absoluteString, "metamask://testMessage") 81 | } 82 | 83 | func testSendMessageWithDeeplink() { 84 | let account = "testAccount" 85 | let chainId = "0x1" 86 | let options: [String: String] = ["account": account, "chainId": chainId] 87 | deeplinkClient.sendMessage(.mmsdk(message: "testMessage", pubkey: nil, channelId: "testChannelId"), options: options) 88 | let openedUrl = mockURLOpener.openedURL?.absoluteString ?? "" 89 | let expectedDeeplink = "metamask://mmsdk?scheme=testDapp&message=testMessage&channelId=testChannelId&account=\(account)@\(chainId)" 90 | XCTAssertEqual(openedUrl, expectedDeeplink) 91 | } 92 | 93 | func testSendStringMessageWithOptions() { 94 | let account = "testAccount" 95 | let chainId = "0x1" 96 | let message = "testMessage" 97 | let based64Message = message.base64Encode() ?? "" 98 | let options: [String: String] = ["account": account, "chainId": chainId] 99 | 100 | deeplinkClient.sendMessage(message, encrypt: false, options: options) 101 | let openedUrl = mockURLOpener.openedURL?.absoluteString ?? "" 102 | let expectedDeeplink = "metamask://mmsdk?scheme=testDapp&message=\(based64Message)&channelId=mockSessionId&account=\(account)@\(chainId)" 103 | XCTAssertEqual(openedUrl, expectedDeeplink) 104 | } 105 | 106 | func testConnectIsTracked() { 107 | var tracked = false 108 | var trackedEvent: Event! 109 | 110 | deeplinkClient.trackEvent = { event, _ in 111 | tracked = true 112 | trackedEvent = event 113 | } 114 | 115 | deeplinkClient.connect() 116 | 117 | XCTAssert(tracked) 118 | XCTAssert(trackedEvent == .connectionRequest) 119 | } 120 | 121 | func testDisconnectIsTracked() { 122 | var tracked = false 123 | var trackedEvent: Event! 124 | 125 | deeplinkClient.trackEvent = { event, _ in 126 | tracked = true 127 | trackedEvent = event 128 | } 129 | 130 | deeplinkClient.disconnect() 131 | 132 | XCTAssert(tracked) 133 | XCTAssert(trackedEvent == .disconnected) 134 | } 135 | 136 | func testTerminateConnectionIsTracked() { 137 | var tracked = false 138 | var trackedEvent: Event! 139 | 140 | deeplinkClient.trackEvent = { event, _ in 141 | tracked = true 142 | trackedEvent = event 143 | } 144 | 145 | deeplinkClient.terminateConnection() 146 | 147 | XCTAssert(tracked) 148 | XCTAssert(trackedEvent == .disconnected) 149 | } 150 | 151 | func testAddJobIncreasesQueuedJobsByOne() { 152 | let job: RequestJob = {} 153 | XCTAssertEqual(deeplinkClient.requestJobs.count, 0) 154 | deeplinkClient.addRequest(job) 155 | XCTAssertEqual(deeplinkClient.requestJobs.count, 1) 156 | } 157 | 158 | func testRunQueuedJobsClearsJobQueue() { 159 | let job1: RequestJob = {} 160 | let job2: RequestJob = {} 161 | deeplinkClient.addRequest(job1) 162 | deeplinkClient.addRequest(job2) 163 | XCTAssertEqual(deeplinkClient.requestJobs.count, 2) 164 | 165 | deeplinkClient.runQueuedJobs() 166 | XCTAssertEqual(deeplinkClient.requestJobs.count, 0) 167 | } 168 | 169 | func testTypeReadyMessageRunsQueuedJobs() { 170 | let job: RequestJob = {} 171 | deeplinkClient.addRequest(job) 172 | 173 | XCTAssertEqual(deeplinkClient.requestJobs.count, 1) 174 | 175 | let response = ["type": "ready"] 176 | let message = response.toJsonString() ?? "" 177 | 178 | deeplinkClient.handleMessage(message) 179 | 180 | XCTAssertEqual(deeplinkClient.requestJobs.count, 0) 181 | } 182 | 183 | func testTypeTerminateMessageDisconnects() { 184 | var trackedEvent: Event! 185 | let response = ["type": "terminate"] 186 | let message = response.toJsonString() ?? "" 187 | 188 | deeplinkClient.trackEvent = { event, _ in 189 | trackedEvent = event 190 | } 191 | 192 | deeplinkClient.handleMessage(message) 193 | 194 | XCTAssertEqual(trackedEvent, .disconnected) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Example/Tests/DeeplinkManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeeplinkManagerTests.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import XCTest 7 | import metamask_ios_sdk 8 | 9 | class DeeplinkManagerTests: XCTestCase { 10 | var deeplinkManager: DeeplinkManager! 11 | 12 | override func setUp() { 13 | super.setUp() 14 | deeplinkManager = DeeplinkManager() 15 | } 16 | 17 | func testOnReceiveMessageIsCalledWhenHandlingMessage() { 18 | let message = "Message to send".base64Encode() ?? "" 19 | let urlString = "metamask://mmsdk?message=\(message)" 20 | let url = URL(string: urlString)! 21 | var messageReceived = false 22 | 23 | deeplinkManager.onReceiveMessage = { _ in 24 | messageReceived = true 25 | } 26 | 27 | deeplinkManager.handleUrl(url) 28 | XCTAssert(messageReceived) 29 | } 30 | 31 | func testConnectDeeplinkHasCorrectChannelId() { 32 | let channelId = "2468" 33 | let url = "target://connect?scheme=testdapp&channelId=\(channelId)" 34 | let deeplink = deeplinkManager.getDeeplink(url) 35 | XCTAssert(deeplink == Deeplink.connect(pubkey: nil, channelId: channelId, request: nil)) 36 | } 37 | 38 | func testConnectDeeplinkHasCorrectPublicKey() { 39 | let pubkey = "asdfghjkl" 40 | let channelId = "2468" 41 | let url = "target://connect?scheme=testdapp&pubkey=\(pubkey)&channelId=\(channelId)" 42 | let deeplink = deeplinkManager.getDeeplink(url) 43 | XCTAssert(deeplink == Deeplink.connect(pubkey: pubkey, channelId: channelId, request: nil)) 44 | } 45 | 46 | func testConnectDeeplinkHasRequest() { 47 | let pubkey = "asdfghjkl" 48 | let channelId = "2468" 49 | let request = "This is a json request".base64Encode() ?? "" 50 | let url = "target://connect?scheme=testdapp&pubkey=\(pubkey)&channelId=\(channelId)&request=\(request)" 51 | let deeplink = deeplinkManager.getDeeplink(url) 52 | XCTAssert(deeplink == Deeplink.connect(pubkey: pubkey, channelId: channelId, request: request)) 53 | } 54 | 55 | func testMessageDeeplinkHasCorrectMessageAndPubkey() { 56 | let pubkey = "asdfghjkl" 57 | let channelId = "2468" 58 | let message = "base64EncodedRequest" 59 | let url = "target://mmsdk?scheme=testdapp&message=\(message)&pubkey=\(pubkey)&channelId=\(channelId)" 60 | let deeplink = deeplinkManager.getDeeplink(url) 61 | XCTAssert(deeplink == Deeplink.mmsdk(message: message, pubkey: pubkey, channelId: channelId)) 62 | } 63 | 64 | func testDeeplinkMissingSchemeIsInvalid() { 65 | let channelId = "2468" 66 | let request = "base64EncodedRequest" 67 | let url = "connect?channelId=\(channelId)&request=\(request)" 68 | let deeplink = deeplinkManager.getDeeplink(url) 69 | XCTAssertNil(deeplink) 70 | } 71 | 72 | func testDeeplinkMissingHostIsInvalid() { 73 | let channelId = "2468" 74 | let request = "base64EncodedRequest" 75 | let url = "target://?channelId=\(channelId)&request=\(request)" 76 | let deeplink = deeplinkManager.getDeeplink(url) 77 | XCTAssertNil(deeplink) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Tests/EthereumRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EthereumRequestTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class EthereumRequestTests: XCTestCase { 10 | func testInitializationWithStringMethod() { 11 | let params = ["from": "0x1234567890", "to": "0x0987654321"] 12 | let request = EthereumRequest(id: "12345", method: "eth_sendTransaction", params: params) 13 | 14 | XCTAssertEqual(request.id, "12345") 15 | XCTAssertEqual(request.method, "eth_sendTransaction") 16 | XCTAssertEqual(request.params, params) 17 | XCTAssertEqual(request.methodType, .ethSendTransaction) 18 | } 19 | 20 | func testInitializationWithEthereumMethod() { 21 | let params = ["0x1234567890", "latest"] 22 | let request = EthereumRequest(id: "12345", method: .ethGetBalance, params: params) 23 | 24 | XCTAssertEqual(request.id, "12345") 25 | XCTAssertEqual(request.method, "eth_getBalance") 26 | XCTAssertEqual(request.params, params) 27 | XCTAssertEqual(request.methodType, .ethGetBalance) 28 | } 29 | 30 | func testEthereumMethodHasIdByDefault() { 31 | let params = ["0x1234567890", "latest"] 32 | let request = EthereumRequest(method: .ethGetBalance, params: params) 33 | 34 | XCTAssertNotNil(request.id) 35 | XCTAssertNotEqual(request.id, "") 36 | } 37 | 38 | func testKnownMethodTypeConversion() { 39 | let params = ["from": "0x1234567890", "to": "0x0987654321"] 40 | let request = EthereumRequest(id: "12345", method: "eth_sendTransaction", params: params) 41 | 42 | XCTAssertEqual(request.methodType, .ethSendTransaction) 43 | } 44 | 45 | func testUnknownMethodTypeConversion() { 46 | let params = ["from": "0x1234567890", "to": "0x0987654321"] 47 | let unknownRequest = EthereumRequest(id: "12345", method: "eth_unkown_method", params: params) 48 | 49 | XCTAssertEqual(unknownRequest.methodType, .unknownMethod) 50 | } 51 | 52 | func testDictionarySocketRepresentation() { 53 | let params = ["from": "0x1234567890", "to": "0x0987654321"] 54 | let request = EthereumRequest(id: "12345", method: .ethSendTransaction, params: params) 55 | let socketRep = request.socketRepresentation() as? [String: Any] ?? [:] 56 | 57 | XCTAssertEqual(socketRep["id"] as? String, "12345") 58 | XCTAssertEqual(socketRep["method"] as? String, "eth_sendTransaction") 59 | XCTAssertEqual(socketRep["parameters"] as? [String: String], params) 60 | } 61 | 62 | func testArraySocketRepresentation() { 63 | let params = ["0x1234567890", "latest"] 64 | let request = EthereumRequest(id: "24680", method: .ethGetBalance, params: params) 65 | let socketRep = request.socketRepresentation() as? [String: Any] ?? [:] 66 | 67 | XCTAssertEqual(socketRep["id"] as? String, "24680") 68 | XCTAssertEqual(socketRep["method"] as? String, "eth_getBalance") 69 | XCTAssertEqual(socketRep["parameters"] as? [String], params) 70 | } 71 | 72 | func testStructSocketRepresentation() { 73 | let transaction = Transaction( 74 | to: "0x0000000000000000000000000000000000000000", 75 | from: "0x1234567890", 76 | value: "0x000000000000000001" 77 | ) 78 | 79 | let parameters: [Transaction] = [transaction] 80 | 81 | let transactionRequest = EthereumRequest( 82 | id: "24680", 83 | method: .ethSendTransaction, 84 | params: parameters 85 | ) 86 | 87 | let socketRep = transactionRequest.socketRepresentation() as? [String: Any] ?? [:] 88 | 89 | XCTAssertEqual(socketRep["id"] as? String, "24680") 90 | XCTAssertEqual(socketRep["method"] as? String, "eth_sendTransaction") 91 | 92 | let socketParams = socketRep["parameters"] as? [Transaction] ?? [] 93 | XCTAssertEqual(socketParams.first?.from, "0x1234567890") 94 | XCTAssertEqual(socketParams.first?.to, "0x0000000000000000000000000000000000000000") 95 | XCTAssertEqual(socketParams.first?.value, "0x000000000000000001") 96 | XCTAssertNil(socketParams.first?.data) 97 | } 98 | } 99 | 100 | struct Transaction: CodableData { 101 | let to: String 102 | let from: String 103 | let value: String 104 | let data: String? 105 | 106 | init(to: String, from: String, value: String, data: String? = nil) { 107 | self.to = to 108 | self.from = from 109 | self.value = value 110 | self.data = data 111 | } 112 | 113 | func socketRepresentation() -> NetworkData { 114 | [ 115 | "to": to, 116 | "from": from, 117 | "value": value, 118 | "data": data 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/InfuraProviderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadOnlyRPCProviderTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class ReadOnlyRPCProviderTests: XCTestCase { 10 | 11 | var readOnlyRPCProvider: ReadOnlyRPCProvider! 12 | var mockNetwork: MockNetwork! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | mockNetwork = MockNetwork() 17 | readOnlyRPCProvider = ReadOnlyRPCProvider(infuraAPIKey: "testAPIKey", readonlyRPCMap: [:], network: mockNetwork) 18 | } 19 | 20 | override func tearDown() { 21 | readOnlyRPCProvider = nil 22 | mockNetwork = nil 23 | super.tearDown() 24 | } 25 | 26 | func testEndpoint() { 27 | let ethereumMainnet = readOnlyRPCProvider.endpoint(for: "0x1") 28 | XCTAssertEqual(ethereumMainnet, "https://mainnet.infura.io/v3/testAPIKey") 29 | 30 | let polygonMainnet = readOnlyRPCProvider.endpoint(for: "0x89") 31 | XCTAssertEqual(polygonMainnet, "https://polygon-mainnet.infura.io/v3/testAPIKey") 32 | 33 | let unknownChain = readOnlyRPCProvider.endpoint(for: "0x999") 34 | XCTAssertNil(unknownChain) 35 | } 36 | 37 | func testSendRequestSuccess() async { 38 | let mockResponseData = """ 39 | { 40 | "jsonrpc": "2.0", 41 | "id": 1, 42 | "result": "0x1" 43 | } 44 | """.data(using: .utf8)! 45 | 46 | mockNetwork.responseData = mockResponseData 47 | 48 | let request = EthereumRequest(id: "1", method: "eth_chainId") 49 | let appMetadata = AppMetadata(name: "TestApp", url: "https://testapp.com") 50 | 51 | let result = await readOnlyRPCProvider.sendRequest(request, chainId: "0x1", appMetadata: appMetadata) 52 | 53 | XCTAssertEqual(result as? String, "0x1") 54 | } 55 | 56 | func testSendRequestEndpointUnavailable() async { 57 | let request = EthereumRequest(id: "1", method: "eth_chainId") 58 | let appMetadata = AppMetadata(name: "TestApp", url: "https://testapp.com") 59 | 60 | let result = await readOnlyRPCProvider.sendRequest(request, chainId: "0x999", appMetadata: appMetadata) 61 | 62 | XCTAssertNil(result) 63 | } 64 | 65 | func testSendRequestNetworkError() async { 66 | mockNetwork.error = NSError(domain: "test", code: 1, userInfo: nil) 67 | 68 | let request = EthereumRequest(id: "1", method: "eth_chainId") 69 | let appMetadata = AppMetadata(name: "TestApp", url: "https://testapp.com") 70 | 71 | let result = await readOnlyRPCProvider.sendRequest(request, chainId: "0x1", appMetadata: appMetadata) 72 | 73 | XCTAssertNil(result) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Example/Tests/MockCommClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockClient.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import metamask_ios_sdk 7 | import XCTest 8 | 9 | class MockCommClient: CommClient { 10 | var channelId: String = "randomId" 11 | 12 | var connectCalled = false 13 | var sendMessageCalled = false 14 | var disConnectCalled = false 15 | var addRequestCalled = false 16 | var requestAuthorisationCalled = false 17 | 18 | var addRequestJob: (() -> Void)? 19 | 20 | var expectation: XCTestExpectation? 21 | 22 | var appMetadata: AppMetadata? 23 | 24 | var sessionDuration: TimeInterval = 3600 25 | 26 | var trackEvent: ((Event, [String : Any]) -> Void)? 27 | 28 | var handleResponse: (([String : Any]) -> Void)? 29 | 30 | var onClientsTerminated: (() -> Void)? 31 | 32 | func requestAuthorisation() { 33 | requestAuthorisationCalled = true 34 | } 35 | 36 | func connect(with request: String?) { 37 | connectCalled = true 38 | } 39 | 40 | func disconnect() { 41 | disConnectCalled = true 42 | } 43 | 44 | func clearSession() { 45 | disConnectCalled = true 46 | } 47 | 48 | func addRequest(_ job: @escaping RequestJob) { 49 | addRequestCalled = true 50 | addRequestJob = job 51 | } 52 | 53 | func sendMessage(_ message: T, encrypt: Bool, options: [String : String]) { 54 | sendMessageCalled = true 55 | expectation?.fulfill() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/Tests/MockCommClientFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockCommClientFactory.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | 8 | class MockCommClientFactory: CommClientFactory { 9 | override func socketClient() -> CommClient { 10 | MockSocketCommClient() 11 | } 12 | 13 | override func deeplinkClient(dappScheme: String) -> CommClient { 14 | MockDeeplinkCommClient() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example/Tests/MockDeeplinkCommClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDeeplinkCommClient.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | class MockDeeplinkCommClient: MockCommClient { } 7 | -------------------------------------------------------------------------------- /Example/Tests/MockDeeplinkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDeeplinkManager.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | 8 | class MockDeeplinkManager: DeeplinkManager { 9 | var handleUrlCalled = false 10 | 11 | override func handleUrl(_ url: URL) { 12 | handleUrlCalled = true 13 | } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /Example/Tests/MockEthereumDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockEthereumDelegate.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | 8 | class MockEthereumDelegate: EthereumEventsDelegate { 9 | var chainIdChangedCalled = false 10 | var accountChangedCalled = false 11 | 12 | var chainId: String? 13 | var account: String? 14 | 15 | func chainIdChanged(_ chainId: String) { 16 | self.chainId = chainId 17 | chainIdChangedCalled = true 18 | } 19 | 20 | func accountChanged(_ account: String) { 21 | self.account = account 22 | accountChangedCalled = true 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Example/Tests/MockInfuraProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockReadOnlyRPCProvider.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | import XCTest 8 | 9 | class MockReadOnlyRPCProvider: ReadOnlyRPCProvider { 10 | var sendRequestCalled = false 11 | var response: Any? = "{}" 12 | var expectation: XCTestExpectation? 13 | 14 | override func sendRequest(_ request: any RPCRequest, 15 | params: Any = "", 16 | chainId: String, 17 | appMetadata: AppMetadata) async -> Any? { 18 | sendRequestCalled = true 19 | expectation?.fulfill() 20 | return response 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Tests/MockKeyExchange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockKeyExchange.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | 8 | class MockKeyExchange: KeyExchange { 9 | var throwEncryptError = false 10 | var throwDecryptError = false 11 | var encryptCalled = false 12 | var decryptCalled = false 13 | 14 | override func decryptMessage(_ message: String) throws -> String { 15 | if throwDecryptError { 16 | throw CryptoError.decryptionFailure 17 | } 18 | 19 | decryptCalled = true 20 | 21 | return "decrypted \(message)" 22 | } 23 | 24 | override func encrypt(_ message: String) throws -> String { 25 | if throwEncryptError { 26 | throw CryptoError.encryptionFailure 27 | } 28 | encryptCalled = true 29 | 30 | return "encrypted \(message)" 31 | } 32 | 33 | override func encryptMessage(_ message: T) throws -> String { 34 | if throwEncryptError { 35 | throw CryptoError.encryptionFailure 36 | } 37 | 38 | encryptCalled = true 39 | 40 | return "encrypted \(message)" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Tests/MockNetwork.swift: -------------------------------------------------------------------------------- 1 | // Mock classes for testing 2 | 3 | import metamask_ios_sdk 4 | 5 | class MockNetwork: Networking { 6 | func post(_ parameters: [String : Any], endpoint: String) async throws -> Data { 7 | if let error = error { 8 | throw error 9 | } 10 | 11 | return responseData ?? Data() 12 | } 13 | 14 | var responseData: Data? 15 | var error: Error? 16 | 17 | func post(_ parameters: [String : Any], endpoint: Endpoint) async throws -> Data { 18 | if let error = error { 19 | throw error 20 | } 21 | 22 | return responseData ?? Data() 23 | } 24 | 25 | public func fetch(_ Type: T.Type, endpoint: Endpoint) async throws -> T { 26 | return responseData as! T 27 | } 28 | 29 | 30 | func addHeaders(_ headers: [String : String]) { 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/Tests/MockSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSessionManager.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | 8 | class MockSessionManager: SessionManager { 9 | var fetchSessionConfigCalled = false 10 | var clearCalled = false 11 | private let DEFAULT_SESSION_DURATION: TimeInterval = 24 * 7 * 3600 12 | 13 | override func fetchSessionConfig() -> (SessionConfig, Bool) { 14 | fetchSessionConfigCalled = true 15 | return (SessionConfig(sessionId: "mockSessionId", expiry: Date(timeIntervalSinceNow: DEFAULT_SESSION_DURATION)), false) 16 | } 17 | 18 | override func clear() { 19 | clearCalled = true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Tests/MockSocket.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSocket.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import Foundation 7 | @testable import metamask_ios_sdk 8 | import SocketIO 9 | 10 | class MockSocket: SocketProtocol { 11 | var status: SocketIOStatus = .notConnected 12 | var connectCalled = false 13 | var disconnectCalled = false 14 | var emitCalled = false 15 | var onCalled = false 16 | 17 | var eventCallbacks: [String: ([Any], SocketAckEmitter) -> Void] = [:] 18 | var clientEventCallbacks: [SocketClientEvent: ([Any], SocketAckEmitter) -> Void] = [:] 19 | 20 | func connect(withPayload payload: [String: Any]?) { 21 | connectCalled = true 22 | } 23 | 24 | func disconnect() { 25 | disconnectCalled = true 26 | } 27 | 28 | func emit(_ event: String, _ items: SocketData..., completion: (() -> Void)?) { 29 | emitCalled = true 30 | } 31 | 32 | @discardableResult 33 | func on(clientEvent event: SocketClientEvent, callback: @escaping ([Any], SocketAckEmitter) -> Void) -> UUID { 34 | onCalled = true 35 | clientEventCallbacks[event] = callback 36 | return UUID() 37 | } 38 | 39 | @discardableResult 40 | func on(_ event: String, callback: @escaping ([Any], SocketAckEmitter) -> Void) -> UUID { 41 | onCalled = true 42 | eventCallbacks[event] = callback 43 | return UUID() 44 | } 45 | 46 | func called(_ event: String) -> Bool { 47 | eventCallbacks[event] != nil 48 | } 49 | 50 | func called(_ event: SocketClientEvent) -> Bool { 51 | clientEventCallbacks[event] != nil 52 | } 53 | 54 | func removeAllHandlers() { 55 | eventCallbacks.removeAll() 56 | clientEventCallbacks.removeAll() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/Tests/MockSocketChannel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSocketChannel.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | import SocketIO 8 | 9 | class MockSocketChannel: SocketChannel { 10 | private var connected = false 11 | var lastEmittedEvent: String? 12 | var lastEmittedMessage: CodableData? 13 | var eventHandlers: [String: ([Any]) -> Void] = [:] 14 | 15 | override var isConnected: Bool { 16 | connected 17 | } 18 | 19 | override func connect() { 20 | connected = true 21 | } 22 | 23 | override func disconnect() { 24 | connected = false 25 | } 26 | 27 | override func on(_ event: SocketClientEvent, completion: @escaping ([Any]) -> Void) { 28 | eventHandlers[event.rawValue] = completion 29 | } 30 | 31 | override func on(_ event: String, completion: @escaping ([Any]) -> Void) { 32 | eventHandlers[event] = completion 33 | } 34 | 35 | override func emit(_ event: String, _ item: CodableData) { 36 | lastEmittedEvent = event 37 | lastEmittedMessage = item 38 | } 39 | 40 | func simulateEvent(_ event: String, data: [Any]) { 41 | eventHandlers[event]?(data) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/Tests/MockSocketCommClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSocketCommClient.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | class MockSocketCommClient: MockCommClient { } 7 | -------------------------------------------------------------------------------- /Example/Tests/MockSocketManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSocketManager.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | @testable import metamask_ios_sdk 7 | import SocketIO 8 | 9 | class MockSocketManager: SocketManagerProtocol { 10 | var standardSocket: SocketProtocol 11 | 12 | init() { 13 | standardSocket = MockSocket() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/Tests/MockURLOpener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLOpener.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import Foundation 7 | import metamask_ios_sdk 8 | 9 | class MockURLOpener: URLOpener { 10 | var openedURL: URL? 11 | 12 | func open(_ url: URL) { 13 | openedURL = url 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/Tests/SecureStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureStorageTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class KeychainTests: XCTestCase { 10 | var keychain: SecureStore! 11 | var testKey = "testKey" 12 | 13 | override func setUp() { 14 | super.setUp() 15 | keychain = Keychain(service: "com.example.testKeychain") 16 | } 17 | 18 | override func tearDown() { 19 | // Clean up after each test, delete data that might have been saved 20 | keychain.deleteData(for: testKey) 21 | super.tearDown() 22 | } 23 | 24 | func testSaveAndRetrieveString() { 25 | let value = "String input" 26 | 27 | // Save a string 28 | XCTAssertTrue(keychain.save(string: value, key: testKey)) 29 | 30 | // Retrieve the saved string 31 | let retrievedValue = keychain.string(for: testKey) 32 | 33 | XCTAssertEqual(retrievedValue, value) 34 | } 35 | 36 | func testSaveAndOverwriteString() { 37 | let initialValue = "Initial string input" 38 | keychain.save(string: initialValue, key: testKey) 39 | 40 | let replacementValue = "Replacement string input" 41 | keychain.save(string: replacementValue, key: testKey) 42 | 43 | // Retrieve the last saved string for key 44 | let retrievedValue = keychain.string(for: testKey) 45 | 46 | XCTAssertEqual(retrievedValue, replacementValue) 47 | } 48 | 49 | func testSaveAndRetrieveData() { 50 | guard let data = "Test Data".data(using: .utf8) else { 51 | XCTFail("Could not convert string to data") 52 | return 53 | } 54 | 55 | // Save data 56 | XCTAssertTrue(keychain.save(data: data, key: testKey)) 57 | 58 | // Retrieve the saved data 59 | let retrievedData = keychain.data(for: testKey) 60 | 61 | XCTAssertEqual(retrievedData, data) 62 | } 63 | 64 | func testDeleteString() { 65 | let value = "Test String" 66 | 67 | // Save a string 68 | XCTAssertTrue(keychain.save(string: value, key: testKey)) 69 | 70 | // Delete the saved data 71 | XCTAssertTrue(keychain.deleteData(for: testKey)) 72 | 73 | // Attempt to retrieve the deleted data 74 | let retrievedValue = keychain.string(for: testKey) 75 | 76 | XCTAssertNil(retrievedValue) 77 | } 78 | 79 | func testDeleteData() { 80 | guard let data = "Test String".data(using: .utf8) else { 81 | XCTFail("Could not convert string to data") 82 | return 83 | } 84 | 85 | // Save a string 86 | XCTAssertTrue(keychain.save(data: data, key: testKey)) 87 | 88 | // Delete the saved data 89 | XCTAssertTrue(keychain.deleteData(for: testKey)) 90 | 91 | // Attempt to retrieve the deleted data 92 | let retrievedValue = keychain.data(for: testKey) 93 | 94 | XCTAssertNil(retrievedValue) 95 | } 96 | 97 | func testSaveAndRetrievModel() { 98 | let sessionId = "SHDKy-SSY872-FGHQ" 99 | let date = Date() 100 | 101 | let sessionConfigModel = SessionConfig( 102 | sessionId: sessionId, 103 | expiry: date) 104 | 105 | guard let sessionConfigModelData = try? JSONEncoder().encode(sessionConfigModel) else { 106 | XCTFail("Could not convert SessionConfig to data") 107 | return 108 | } 109 | 110 | // Encode and save a Codable object 111 | XCTAssertTrue(keychain.save(data: sessionConfigModelData, key: testKey)) 112 | 113 | // Retrieve and decode the saved model 114 | guard let retrievedModel: SessionConfig = keychain.model(for: testKey) else { 115 | XCTFail("Could not create SessionConfig model from data") 116 | return 117 | } 118 | 119 | XCTAssertEqual(retrievedModel, sessionConfigModel) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Example/Tests/SessionConfigTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionConfigTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class SessionConfigTests: XCTestCase { 10 | 11 | func testFutureExpiryDateIsValid() { 12 | let futureDate = Date(timeIntervalSinceNow: 1000) 13 | let session = SessionConfig(sessionId: "12345", expiry: futureDate) 14 | XCTAssertTrue(session.isValid) 15 | } 16 | 17 | func testPastExpiryDateIsInvalid() { 18 | let pastDate = Date(timeIntervalSinceNow: -10) 19 | let session = SessionConfig(sessionId: "12345", expiry: pastDate) 20 | XCTAssertFalse(session.isValid) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/Tests/SessionManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionManagerTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class SessionManagerTests: XCTestCase { 10 | var keychain: SecureStore! 11 | var sessionManager: SessionManager! 12 | let sessionDuration: TimeInterval = 3600 13 | 14 | override func setUp() { 15 | super.setUp() 16 | keychain = Keychain(service: "com.example.testKeychain") 17 | sessionManager = SessionManager(store: keychain, sessionDuration: sessionDuration) 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | keychain.deleteAll() 23 | sessionManager.clear() 24 | } 25 | 26 | func testInitiallyNoSessionExists() { 27 | let fetchCurrentSessionConfig = sessionManager.fetchCurrentSessionConfig() 28 | XCTAssertNil(fetchCurrentSessionConfig) 29 | } 30 | 31 | func testNewSessionConfigIsValid() { 32 | sessionManager.createNewSessionConfig() 33 | guard let newSessionConfig = sessionManager.fetchCurrentSessionConfig() else { 34 | XCTFail("Could not create new session") 35 | return 36 | } 37 | XCTAssertTrue(newSessionConfig.isValid) 38 | } 39 | 40 | func testClearSessionDeletesCurrentSession() { 41 | sessionManager.createNewSessionConfig() 42 | 43 | let sessionConfig = sessionManager.fetchCurrentSessionConfig() 44 | XCTAssertNotNil(sessionConfig) 45 | 46 | sessionManager.clear() 47 | 48 | let newSessionConfig = sessionManager.fetchCurrentSessionConfig() 49 | XCTAssertNil(newSessionConfig) 50 | } 51 | 52 | func testFetchSessionAfterClearReturnsNewSession() { 53 | sessionManager.createNewSessionConfig() 54 | 55 | let sessionConfig = sessionManager.fetchCurrentSessionConfig() 56 | 57 | sessionManager.clear() 58 | 59 | let newSessionConfig = sessionManager.fetchCurrentSessionConfig() 60 | XCTAssertNotEqual(sessionConfig?.sessionId, newSessionConfig?.sessionId) 61 | } 62 | 63 | func testFetchSessionAfterSettingInvalidSessionCreatesANewValidSession() { 64 | sessionManager = SessionManager(store: keychain, sessionDuration: -sessionDuration) 65 | sessionManager.createNewSessionConfig() 66 | 67 | guard let sessionConfig = sessionManager.fetchCurrentSessionConfig() else { 68 | XCTFail("Could not create new session") 69 | return 70 | } 71 | 72 | XCTAssertFalse(sessionConfig.isValid) 73 | 74 | let newSessionConfig = sessionManager.fetchSessionConfig().0 75 | 76 | XCTAssertTrue(newSessionConfig.isValid) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/Tests/SocketChannelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketChannelTests.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import metamask_ios_sdk 8 | 9 | class SocketChannelTests: XCTestCase { 10 | var socketChannel: SocketChannel! 11 | var mockSocket: MockSocket! 12 | var mockSocketManager: MockSocketManager! 13 | 14 | override func setUp() { 15 | super.setUp() 16 | mockSocketManager = MockSocketManager() 17 | socketChannel = SocketChannel(url: "http://mockurl.com") 18 | mockSocket = mockSocketManager.standardSocket as? MockSocket 19 | socketChannel.socket = mockSocket 20 | socketChannel.socketManager = mockSocketManager 21 | } 22 | 23 | func testNetworkUrl() { 24 | XCTAssertEqual(socketChannel.networkUrl, "http://mockurl.com") 25 | 26 | socketChannel.networkUrl = "http://newurl.com" 27 | XCTAssertEqual(socketChannel.networkUrl, "http://newurl.com") 28 | } 29 | 30 | func testIsConnected() { 31 | mockSocket.status = .connected 32 | XCTAssertTrue(socketChannel.isConnected) 33 | 34 | mockSocket.status = .disconnected 35 | XCTAssertFalse(socketChannel.isConnected) 36 | } 37 | 38 | func testConnect() { 39 | socketChannel.connect() 40 | XCTAssertTrue(mockSocket.connectCalled) 41 | } 42 | 43 | func testDisconnect() { 44 | socketChannel.disconnect() 45 | XCTAssertTrue(mockSocket.disconnectCalled) 46 | } 47 | 48 | func testTearDown() { 49 | socketChannel.tearDown() 50 | } 51 | 52 | func testOnClientEvent() { 53 | socketChannel.on(.connect) { _ in } 54 | 55 | XCTAssertTrue(mockSocket.called(.connect)) 56 | } 57 | 58 | func testOnStringEvent() { 59 | socketChannel.on("testEvent") { _ in } 60 | 61 | XCTAssertTrue(mockSocket.called("testEvent")) 62 | } 63 | 64 | func testEmit() { 65 | socketChannel.emit("testEvent", "testData") 66 | XCTAssertTrue(mockSocket.emitCalled) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Example/Tests/TestCodableData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCodableData.swift 3 | // metamask-ios-sdk_Tests 4 | // 5 | 6 | import Foundation 7 | @testable import metamask_ios_sdk 8 | 9 | struct TestCodableData: CodableData, Equatable { 10 | var id: String 11 | var message: String 12 | } 13 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import metamask_ios_sdk 3 | 4 | class Tests: XCTestCase { 5 | 6 | func testExample() { 7 | // This is an example of a functional test case. 8 | XCTAssert(true, "Pass") 9 | } 10 | 11 | func testPerformanceExample() { 12 | // This is an example of a performance test case. 13 | measure { 14 | // Put the code you want to measure the time of here. 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcodeproj/xcshareddata/xcschemes/metamask-ios-sdk-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 63 | 64 | 65 | 66 | 67 | 77 | 79 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "collectionconcurrencykit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", 7 | "state" : { 8 | "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", 9 | "version" : "0.2.0" 10 | } 11 | }, 12 | { 13 | "identity" : "cryptoswift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", 16 | "state" : { 17 | "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", 18 | "version" : "1.8.2" 19 | } 20 | }, 21 | { 22 | "identity" : "sourcekitten", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/jpsim/SourceKitten.git", 25 | "state" : { 26 | "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", 27 | "version" : "0.35.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-argument-parser", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-argument-parser.git", 34 | "state" : { 35 | "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", 36 | "version" : "1.4.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-syntax", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-syntax.git", 43 | "state" : { 44 | "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", 45 | "version" : "510.0.2" 46 | } 47 | }, 48 | { 49 | "identity" : "swiftlint", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/realm/SwiftLint", 52 | "state" : { 53 | "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", 54 | "version" : "0.55.1" 55 | } 56 | }, 57 | { 58 | "identity" : "swiftytexttable", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", 61 | "state" : { 62 | "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", 63 | "version" : "0.9.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swxmlhash", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/drmohundro/SWXMLHash.git", 70 | "state" : { 71 | "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", 72 | "version" : "7.0.2" 73 | } 74 | }, 75 | { 76 | "identity" : "yams", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/jpsim/Yams.git", 79 | "state" : { 80 | "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", 81 | "version" : "5.1.2" 82 | } 83 | } 84 | ], 85 | "version" : 2 86 | } 87 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import metamask_ios_sdk 8 | 9 | @UIApplicationMain 10 | class AppDelegate: UIResponder, UIApplicationDelegate { 11 | var window: UIWindow? 12 | 13 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | return true 16 | } 17 | 18 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 19 | if URLComponents(url: url, resolvingAgainstBaseURL: true)?.host == "mmsdk" { 20 | MetaMaskSDK.sharedInstance?.handleUrl(url) 21 | } else { 22 | // handle other deeplinks 23 | } 24 | return true 25 | } 26 | 27 | func applicationWillResignActive(_: UIApplication) { 28 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 29 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 30 | } 31 | 32 | func applicationDidEnterBackground(_: UIApplication) { 33 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 34 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 35 | } 36 | 37 | func applicationWillEnterForeground(_: UIApplication) { 38 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 39 | } 40 | 41 | func applicationDidBecomeActive(_: UIApplication) { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | func applicationWillTerminate(_: UIApplication) { 46 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonStyle.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ButtonStyle: ViewModifier { 9 | func body(content: Content) -> some View { 10 | content 11 | .font(.title3) 12 | .foregroundColor(.white) 13 | .padding(.vertical, 10) 14 | .padding(.horizontal) 15 | .background(Color.blue.grayscale(0.5)) 16 | .modifier(ButtonCurvature()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/Curvature.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Curvature.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ButtonCurvature: ViewModifier { 9 | func body(content: Content) -> some View { 10 | content 11 | .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous)) 12 | .overlay(RoundedRectangle(cornerRadius: 30, style: .continuous) 13 | .stroke(Color.clear, lineWidth: 1) 14 | ) 15 | } 16 | } 17 | 18 | struct TextCurvature: ViewModifier { 19 | func body(content: Content) -> some View { 20 | content 21 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 22 | .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous) 23 | .stroke(Color.clear, lineWidth: 1) 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | org.demo.metamask-ios-sdk-Example 28 | CFBundleURLSchemes 29 | 30 | dubdapp 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSRequiresIPhoneOS 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | 50 | UISupportedInterfaceOrientations~ipad 51 | 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationPortrait 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/NetworkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkView.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | import metamask_ios_sdk 8 | 9 | @MainActor 10 | struct NetworkView: View { 11 | @EnvironmentObject var metamaskSDK: MetaMaskSDK 12 | @Environment(\.presentationMode) var presentationMode 13 | @State var networkUrl: String = "" 14 | 15 | var body: some View { 16 | Form { 17 | Section { 18 | Text("Current network URL") 19 | .modifier(TextCallout()) 20 | TextField("Network url", text: $metamaskSDK.networkUrl) 21 | .modifier(TextCaption()) 22 | .frame(minHeight: 32) 23 | .modifier(TextCurvature()) 24 | .disabled(true) 25 | } 26 | 27 | Section { 28 | Text("New network URL") 29 | .modifier(TextCallout()) 30 | TextField("Network url", text: $networkUrl) 31 | .modifier(TextCaption()) 32 | .frame(minHeight: 32) 33 | .modifier(TextCurvature()) 34 | .autocapitalization(.none) 35 | } 36 | 37 | Section { 38 | Button { 39 | changeNetwork() 40 | } label: { 41 | Text("Update") 42 | .modifier(TextButton()) 43 | .frame(maxWidth: .infinity, maxHeight: 32) 44 | } 45 | .font(.title3) 46 | .foregroundColor(.white) 47 | .padding(.vertical, 10) 48 | .padding(.horizontal) 49 | .background(Color.blue.grayscale(0.5)) 50 | .modifier(ButtonCurvature()) 51 | } footer: { 52 | Text("You can replace with your local IP address etc") 53 | .modifier(TextCaption()) 54 | } 55 | } 56 | } 57 | 58 | func changeNetwork() { 59 | metamaskSDK.networkUrl = networkUrl 60 | presentationMode.wrappedValue.dismiss() 61 | } 62 | } 63 | 64 | struct NetworkView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | NetworkView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ReadOnlyCallsView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // 4 | // ReadOnlyCallsView.swift 5 | // metamask-ios-sdk_Example 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import metamask_ios_sdk 11 | 12 | @MainActor 13 | struct ReadOnlyCallsView: View { 14 | @EnvironmentObject var metamaskSDK: MetaMaskSDK 15 | 16 | @State private var showProgressView = false 17 | 18 | @State var balanceResult: String = "" 19 | @State var gasPriceResult: String = "" 20 | @State var web3ClientVersionResult: String = "" 21 | @State private var errorMessage = "" 22 | @State private var showError = false 23 | 24 | var body: some View { 25 | NavigationView { 26 | ZStack { 27 | VStack(spacing: 16) { 28 | Spacer() 29 | 30 | VStack { 31 | Button { 32 | Task { 33 | await getBalance() 34 | } 35 | } label: { 36 | Text("Get Balance") 37 | .modifier(TextButton()) 38 | .frame(maxWidth: .infinity, maxHeight: 32) 39 | } 40 | .modifier(ButtonStyle()) 41 | 42 | Text(balanceResult) 43 | .modifier(TextCaption()) 44 | } 45 | 46 | VStack { 47 | Button { 48 | Task { 49 | await getGasPrice() 50 | } 51 | } label: { 52 | Text("Get Gas Price") 53 | .modifier(TextButton()) 54 | .frame(maxWidth: .infinity, maxHeight: 32) 55 | } 56 | .modifier(ButtonStyle()) 57 | 58 | Text(gasPriceResult) 59 | .modifier(TextCaption()) 60 | } 61 | 62 | VStack { 63 | Button { 64 | Task { 65 | await getWeb3ClientVersion() 66 | } 67 | } label: { 68 | Text("Get Web3 Client Version") 69 | .modifier(TextButton()) 70 | .frame(maxWidth: .infinity, maxHeight: 32) 71 | } 72 | .modifier(ButtonStyle()) 73 | 74 | Text(web3ClientVersionResult) 75 | .modifier(TextCaption()) 76 | } 77 | } 78 | .padding(.horizontal) 79 | 80 | if showProgressView { 81 | ProgressView() 82 | .scaleEffect(1.5, anchor: .center) 83 | .progressViewStyle(CircularProgressViewStyle(tint: .black)) 84 | } 85 | } 86 | .alert(isPresented: $showError) { 87 | Alert( 88 | title: Text("Error"), 89 | message: Text(errorMessage) 90 | ) 91 | } 92 | .onAppear { 93 | showProgressView = false 94 | } 95 | } 96 | .navigationTitle("Read-Only Calls") 97 | } 98 | 99 | func getBalance() async { 100 | let from = metamaskSDK.account 101 | let params: [String] = [from, "latest"] 102 | let getBalanceRequest = EthereumRequest( 103 | method: .ethGetBalance, 104 | params: params 105 | ) 106 | 107 | showProgressView = true 108 | let requestResult = await metamaskSDK.request(getBalanceRequest) 109 | showProgressView = false 110 | 111 | switch requestResult { 112 | case let .success(value): 113 | balanceResult = value 114 | errorMessage = "" 115 | case let .failure(error): 116 | errorMessage = error.localizedDescription 117 | showError = true 118 | } 119 | } 120 | 121 | func getGasPrice() async { 122 | let params: [String] = [] 123 | let getGasPriceRequest = EthereumRequest( 124 | method: .ethGasPrice, 125 | params: params 126 | ) 127 | 128 | showProgressView = true 129 | let requestResult = await metamaskSDK.request(getGasPriceRequest) 130 | showProgressView = false 131 | 132 | switch requestResult { 133 | case let .success(value): 134 | gasPriceResult = value 135 | errorMessage = "" 136 | case let .failure(error): 137 | errorMessage = error.localizedDescription 138 | showError = true 139 | } 140 | } 141 | 142 | func getWeb3ClientVersion() async { 143 | let params: [String] = [] 144 | let getWeb3ClientVersionRequest = EthereumRequest( 145 | method: .web3ClientVersion, 146 | params: params 147 | ) 148 | 149 | showProgressView = true 150 | let requestResult = await metamaskSDK.request(getWeb3ClientVersionRequest) 151 | showProgressView = false 152 | 153 | switch requestResult { 154 | case let .success(value): 155 | web3ClientVersionResult = value 156 | errorMessage = "" 157 | case let .failure(error): 158 | errorMessage = error.localizedDescription 159 | showError = true 160 | } 161 | } 162 | } 163 | 164 | struct ReadOnlyCalls_Previews: PreviewProvider { 165 | static var previews: some View { 166 | ReadOnlyCallsView() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/SignView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignView.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | import Combine 8 | import metamask_ios_sdk 9 | 10 | @MainActor 11 | struct SignView: View { 12 | @EnvironmentObject var metamaskSDK: MetaMaskSDK 13 | 14 | @State var signMessage = "" 15 | @State private var showProgressView = false 16 | 17 | @State var result: String = "" 18 | @State private var errorMessage = "" 19 | @State private var showError = false 20 | @State var isConnectAndSign = false 21 | @State var isChainedSigning = false 22 | 23 | private let signButtonTitle = "Sign" 24 | private let connectAndSignButtonTitle = "Connect & Sign" 25 | 26 | var body: some View { 27 | GeometryReader { geometry in 28 | Form { 29 | Section { 30 | Text("Message") 31 | .modifier(TextCallout()) 32 | TextEditor(text: $signMessage) 33 | .modifier(TextCaption()) 34 | .frame(height: geometry.size.height / 2) 35 | .modifier(TextCurvature()) 36 | } 37 | 38 | Section { 39 | Text("Result") 40 | .modifier(TextCallout()) 41 | TextEditor(text: $result) 42 | .modifier(TextCaption()) 43 | .frame(minHeight: 40) 44 | .modifier(TextCurvature()) 45 | } 46 | 47 | Section { 48 | ZStack { 49 | Button { 50 | Task { 51 | await 52 | if isConnectAndSign { connectAndSign() } else if isChainedSigning { signChainedMessages() } else { signMessage() } 53 | } 54 | } label: { 55 | Text(isConnectAndSign ? connectAndSignButtonTitle : signButtonTitle) 56 | .modifier(TextButton()) 57 | .frame(maxWidth: .infinity, maxHeight: 32) 58 | } 59 | .modifier(ButtonStyle()) 60 | 61 | if showProgressView { 62 | ProgressView() 63 | .scaleEffect(1.5, anchor: .center) 64 | .progressViewStyle(CircularProgressViewStyle(tint: .black)) 65 | } 66 | } 67 | .alert(isPresented: $showError) { 68 | Alert( 69 | title: Text("Error"), 70 | message: Text(errorMessage) 71 | ) 72 | } 73 | } 74 | } 75 | } 76 | .onAppear { 77 | updateMessage() 78 | showProgressView = false 79 | } 80 | .onChange(of: metamaskSDK.chainId) { _ in 81 | updateMessage() 82 | } 83 | } 84 | 85 | func updateMessage() { 86 | if isChainedSigning { 87 | let chainedSigningMessages: [String] = [ 88 | ChainedSigningMessage.helloWorld, 89 | ChainedSigningMessage.transactionData, 90 | ChainedSigningMessage.byeWorld 91 | ] 92 | signMessage = chainedSigningMessages.joined(separator: "\n======================\n") 93 | } else if isConnectAndSign { 94 | signMessage = "{\"domain\":{\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Linda!\",\"from\":{\"name\":\"Aliko\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Linda\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}]},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}" 95 | } else { 96 | let jsonData = "{\"types\": {\"EIP712Domain\": [{ \"name\": \"name\", \"type\": \"string\" },{ \"name\": \"version\", \"type\": \"string\" },{ \"name\": \"chainId\", \"type\": \"uint256\" },{ \"name\": \"verifyingContract\", \"type\": \"address\" }],\"Person\": [{ \"name\": \"name\", \"type\": \"string\" },{ \"name\": \"wallet\", \"type\": \"address\" }],\"Mail\": [{ \"name\": \"from\", \"type\": \"Person\" },{ \"name\": \"to\", \"type\": \"Person\" },{ \"name\": \"contents\", \"type\": \"string\" }]},\"primaryType\": \"Mail\",\"domain\": {\"name\": \"Ether Mail\",\"version\": \"1\",\"chainId\": \"\(metamaskSDK.chainId)\",\"verifyingContract\": \"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\": {\"from\": { \"name\": \"Kinno\", \"wallet\": \"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\" },\"to\": { \"name\": \"Bob\", \"wallet\": \"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\" },\"contents\": \"Hello, Busa!\"}}".data(using: .utf8)! 97 | 98 | do { 99 | let decoder = JSONDecoder() 100 | let signParams = try decoder.decode(SignContractParameter.self, from: jsonData) 101 | signMessage = signParams.toJsonString() ?? "" 102 | } catch { 103 | Logging.error("SignView:: Decoding error: \(error.localizedDescription)") 104 | signMessage = "" 105 | } 106 | } 107 | } 108 | 109 | func signMessage() async { 110 | let account = metamaskSDK.account 111 | 112 | showProgressView = true 113 | let requestResult = await metamaskSDK.signTypedDataV4(typedData: signMessage, address: account) 114 | showProgressView = false 115 | 116 | switch requestResult { 117 | case let .success(value): 118 | result = value 119 | errorMessage = "" 120 | case let .failure(error): 121 | errorMessage = error.localizedDescription 122 | showError = true 123 | } 124 | } 125 | 126 | func signChainedMessages() async { 127 | let from = metamaskSDK.account 128 | let helloWorldParams: [String] = [ChainedSigningMessage.helloWorld, from] 129 | let transactionDataParams: [String] = [ChainedSigningMessage.transactionData, from] 130 | let byeWorldParams: [String] = [ChainedSigningMessage.byeWorld, from] 131 | 132 | let helloWorldSignRequest = EthereumRequest( 133 | method: .personalSign, 134 | params: helloWorldParams 135 | ) 136 | 137 | let transactionDataSignRequest = EthereumRequest( 138 | method: .personalSign, 139 | params: transactionDataParams 140 | ) 141 | 142 | let byeWorldSignRequest = EthereumRequest( 143 | method: .personalSign, 144 | params: byeWorldParams 145 | ) 146 | 147 | let requestBatch: [EthereumRequest] = [helloWorldSignRequest, transactionDataSignRequest, byeWorldSignRequest] 148 | 149 | showProgressView = true 150 | let requestResult = await metamaskSDK.batchRequest(requestBatch) 151 | showProgressView = false 152 | 153 | switch requestResult { 154 | case let .success(value): 155 | result = value.joined(separator: "\n======================\n") 156 | errorMessage = "" 157 | case let .failure(error): 158 | errorMessage = error.localizedDescription 159 | showError = true 160 | } 161 | } 162 | 163 | func connectAndSign() async { 164 | showProgressView = true 165 | let connectSignResult = await metamaskSDK.connectAndSign(message: signMessage) 166 | showProgressView = false 167 | 168 | switch connectSignResult { 169 | case let .success(value): 170 | result = value 171 | errorMessage = "" 172 | case let .failure(error): 173 | errorMessage = error.localizedDescription 174 | showError = true 175 | } 176 | } 177 | } 178 | 179 | struct SignView_Previews: PreviewProvider { 180 | static var previews: some View { 181 | SignView() 182 | } 183 | } 184 | 185 | struct ChainedSigningMessage { 186 | static let helloWorld = "Hello, world, signing in!" 187 | static let transactionData = "{\"data\":\"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675\",\"from\": \"0x0000000000000000000000000000000000000000\",\"gas\": \"0x76c0\",\"gasPrice\": \"0x9184e72a000\",\"to\": \"0xd46e8dd67c5d32be8058bb8eb970870f07244567\",\"value\": \"0x9184e72a\"}" 188 | static let byeWorld = "Last message to sign!" 189 | } 190 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/SwitchChainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchChainView.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | import Combine 8 | import metamask_ios_sdk 9 | 10 | @MainActor 11 | struct SwitchChainView: View { 12 | @Environment(\.presentationMode) var presentationMode 13 | @EnvironmentObject var metamaskSDK: MetaMaskSDK 14 | 15 | @State private var alert: AlertInfo? 16 | @State var networkSelection: Network = .polygon 17 | 18 | struct AlertInfo: Identifiable { 19 | enum Status { 20 | case error 21 | case success 22 | case chainDoesNotExist 23 | } 24 | 25 | let id: Status 26 | let title: String 27 | let message: String 28 | 29 | var primaryButton: Alert.Button? 30 | var secondarButton: Alert.Button? 31 | var dismissButton: Alert.Button? 32 | } 33 | 34 | enum Network: String, CaseIterable, Identifiable { 35 | case avalanche = "0xa86a" 36 | case ethereum = "0x1" 37 | case polygon = "0x89" 38 | 39 | var id: Self { self } 40 | 41 | var chainId: String { 42 | rawValue 43 | } 44 | 45 | var name: String { 46 | switch self { 47 | case .polygon: return "Polygon" 48 | case .ethereum: return "Ethereum" 49 | case .avalanche: return "Avalanche" 50 | } 51 | } 52 | 53 | var symbol: String { 54 | switch self { 55 | case .polygon: return "MATIC" 56 | case .ethereum: return "ETH" 57 | case .avalanche: return "AVAX" 58 | } 59 | } 60 | 61 | var rpcUrls: [String] { 62 | switch self { 63 | case .polygon: return ["https://polygon-rpc.com"] 64 | case .avalanche: return ["https://api.avax.network/ext/bc/C/rpc"] 65 | default: return [] 66 | } 67 | } 68 | 69 | static func chain(for chainId: String) -> String { 70 | self.allCases.first(where: { $0.rawValue == chainId })?.name ?? "" 71 | } 72 | } 73 | 74 | var body: some View { 75 | Form { 76 | Section { 77 | HStack { 78 | Text("Current chain:") 79 | .modifier(TextCallout()) 80 | Spacer() 81 | Text("\(Network.chain(for: metamaskSDK.chainId)) (\(metamaskSDK.chainId))") 82 | .modifier(TextCalloutLight()) 83 | } 84 | Picker("Switch to:", selection: $networkSelection) { 85 | ForEach(Network.allCases) { network in 86 | Text("\(network.name)") 87 | } 88 | } 89 | } 90 | 91 | Section { 92 | Button { 93 | Task { 94 | await switchEthereumChain() 95 | } 96 | } label: { 97 | Text("Switch Chain ID") 98 | .modifier(TextButton()) 99 | .frame(maxWidth: .infinity, maxHeight: 32) 100 | } 101 | .alert(item: $alert, content: { info in 102 | if info.dismissButton != nil { 103 | return Alert( 104 | title: Text(info.title), 105 | message: Text(info.message), 106 | dismissButton: info.dismissButton 107 | ) 108 | } else { 109 | return Alert( 110 | title: Text(info.title), 111 | message: Text(info.message), 112 | primaryButton: info.primaryButton!, 113 | secondaryButton: info.secondarButton! 114 | ) 115 | } 116 | }) 117 | .modifier(ButtonStyle()) 118 | } 119 | } 120 | .onAppear { 121 | networkSelection = metamaskSDK.chainId == networkSelection.rawValue 122 | ? .ethereum 123 | : .polygon 124 | } 125 | .background(Color.blue.grayscale(0.5)) 126 | } 127 | 128 | func switchEthereumChain() async { 129 | let switchChainResult = await metamaskSDK.switchEthereumChain(chainId: networkSelection 130 | .chainId) 131 | 132 | switch switchChainResult { 133 | case .success: 134 | alert = AlertInfo( 135 | id: .success, 136 | title: "Success", 137 | message: "Successfully switched to \(networkSelection.name)", 138 | dismissButton: SwiftUI.Alert.Button.default(Text("OK"), action: { 139 | presentationMode.wrappedValue.dismiss() 140 | }) 141 | ) 142 | case let .failure(error): 143 | if error.codeType == .unrecognizedChainId || error.codeType == .serverError { 144 | alert = AlertInfo( 145 | id: .chainDoesNotExist, 146 | title: "Error", 147 | message: "\(networkSelection.name) (\(networkSelection.chainId)) has not been added to your MetaMask wallet. Add chain?", 148 | primaryButton: SwiftUI.Alert.Button.default(Text("OK"), action: { 149 | Task { 150 | await addEthereumChain() 151 | } 152 | }), 153 | secondarButton: SwiftUI.Alert.Button.default(Text("Cancel")) 154 | ) 155 | } else { 156 | alert = AlertInfo( 157 | id: .error, 158 | title: "Error", 159 | message: error.localizedDescription, 160 | dismissButton: SwiftUI.Alert.Button.default(Text("OK")) 161 | ) 162 | } 163 | } 164 | } 165 | 166 | func addEthereumChain() async { 167 | let addChainResult = await metamaskSDK.addEthereumChain( 168 | chainId: networkSelection.chainId, 169 | chainName: networkSelection.name, 170 | rpcUrls: networkSelection.rpcUrls, 171 | iconUrls: [], 172 | blockExplorerUrls: nil, 173 | nativeCurrency: NativeCurrency( 174 | name: networkSelection.name, 175 | symbol: networkSelection.symbol, 176 | decimals: 18) 177 | ) 178 | 179 | switch addChainResult { 180 | case .success: 181 | alert = AlertInfo( 182 | id: .success, 183 | title: "Success", 184 | message: metamaskSDK.chainId == networkSelection.chainId 185 | ? "Successfully switched to \(networkSelection.name)" 186 | : "Successfully added \(networkSelection.name)", 187 | dismissButton: SwiftUI.Alert.Button.default(Text("OK"), action: { 188 | presentationMode.wrappedValue.dismiss() 189 | }) 190 | ) 191 | case let .failure(error): 192 | alert = AlertInfo( 193 | id: .error, 194 | title: "Error", 195 | message: error.localizedDescription 196 | ) 197 | } 198 | } 199 | } 200 | 201 | struct SwitchChainView_Previews: PreviewProvider { 202 | static var previews: some View { 203 | SwitchChainView() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyle.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct TextCaption: ViewModifier { 9 | func body(content: Content) -> some View { 10 | content 11 | .font(.system(.caption, design: .rounded)) 12 | .foregroundColor(.black) 13 | } 14 | } 15 | 16 | struct TextCallout: ViewModifier { 17 | func body(content: Content) -> some View { 18 | content 19 | .font(.system(.callout, design: .rounded)) 20 | .foregroundColor(.black) 21 | } 22 | } 23 | 24 | struct TextCalloutLight: ViewModifier { 25 | func body(content: Content) -> some View { 26 | content 27 | .font(.system(.callout, design: .rounded)) 28 | .foregroundColor(.gray) 29 | } 30 | } 31 | 32 | struct TextButton: ViewModifier { 33 | func body(content: Content) -> some View { 34 | content 35 | .font(.system(.body, design: .rounded)) 36 | .foregroundColor(.white) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ToastOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastOverlay.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ToastOverlay: View where ToastContent: View { 9 | let content: ToastContent 10 | @Binding var isPresented: Bool 11 | 12 | var body: some View { 13 | GeometryReader { geometry in 14 | VStack { 15 | Spacer() 16 | HStack { 17 | Spacer() 18 | content 19 | .frame(width: geometry.size.width * 0.8, height: 8) 20 | .animation(.easeIn) 21 | Spacer() 22 | } 23 | Spacer() 24 | } 25 | } 26 | .background(Color.clear) 27 | .edgesIgnoringSafeArea(.bottom) 28 | .onAppear { 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 30 | isPresented = false 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ToastView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToastView.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | struct ToastView: View { 9 | let message: String 10 | 11 | var body: some View { 12 | VStack { 13 | Text(message) 14 | .padding() 15 | .foregroundColor(.white) 16 | .background(Color.black) 17 | .clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous)) 18 | } 19 | } 20 | } 21 | 22 | struct ToastView_Previews: PreviewProvider { 23 | static var previews: some View { 24 | ToastView(message: "Test message") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/TransactionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransactionView.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | import Combine 8 | import metamask_ios_sdk 9 | 10 | @MainActor 11 | struct TransactionView: View { 12 | @EnvironmentObject var metamaskSDK: MetaMaskSDK 13 | 14 | @State private var value = "0x8ac7230489e80000" 15 | @State var result: String = "" 16 | @State private var errorMessage = "" 17 | @State private var showError = false 18 | @State private var to = "0x0000000000000000000000000000000000000000" 19 | @State var isConnectWith: Bool = false 20 | @State private var sendTransactionTitle = "Send Transaction" 21 | @State private var connectWithSendTransactionTitle = "Connect & Send Transaction" 22 | 23 | @State private var showProgressView = false 24 | 25 | var body: some View { 26 | Form { 27 | Section { 28 | Text("From") 29 | .modifier(TextCallout()) 30 | TextField("Enter sender address", text: $metamaskSDK.account) 31 | .modifier(TextCaption()) 32 | .frame(minHeight: 32) 33 | .modifier(TextCurvature()) 34 | } 35 | 36 | Section { 37 | Text("To") 38 | .modifier(TextCallout()) 39 | TextEditor(text: $to) 40 | .modifier(TextCaption()) 41 | .frame(minHeight: 32) 42 | .modifier(TextCurvature()) 43 | } 44 | 45 | Section { 46 | Text("Value") 47 | .modifier(TextCallout()) 48 | TextField("Value", text: $value) 49 | .modifier(TextCaption()) 50 | .frame(minHeight: 32) 51 | .modifier(TextCurvature()) 52 | } 53 | 54 | Section { 55 | Text("Result") 56 | .modifier(TextCallout()) 57 | TextEditor(text: $result) 58 | .modifier(TextCaption()) 59 | .frame(minHeight: 40) 60 | .modifier(TextCurvature()) 61 | } 62 | 63 | Section { 64 | ZStack { 65 | Button { 66 | Task { 67 | await sendTransaction() 68 | } 69 | } label: { 70 | Text(isConnectWith ? connectWithSendTransactionTitle : sendTransactionTitle) 71 | .modifier(TextButton()) 72 | .frame(maxWidth: .infinity, maxHeight: 32) 73 | } 74 | .alert(isPresented: $showError) { 75 | Alert( 76 | title: Text("Error"), 77 | message: Text(errorMessage) 78 | ) 79 | } 80 | .modifier(ButtonStyle()) 81 | 82 | if showProgressView { 83 | ProgressView() 84 | .scaleEffect(1.5, anchor: .center) 85 | .progressViewStyle(CircularProgressViewStyle(tint: .black)) 86 | } 87 | } 88 | } 89 | } 90 | .background(Color.blue.grayscale(0.5)) 91 | } 92 | 93 | func sendTransaction() async { 94 | let transaction = Transaction( 95 | to: to, 96 | from: metamaskSDK.account, 97 | value: value 98 | ) 99 | 100 | let parameters: [Transaction] = [transaction] 101 | 102 | let transactionRequest = EthereumRequest( 103 | method: .ethSendTransaction, 104 | params: parameters // eth_sendTransaction rpc call expects an array parameters object 105 | ) 106 | 107 | showProgressView = true 108 | 109 | let transactionResult = isConnectWith 110 | ? await metamaskSDK.connectWith(transactionRequest) 111 | : await metamaskSDK.sendTransaction(from: metamaskSDK.account, to: to, value: value) 112 | 113 | showProgressView = false 114 | 115 | switch transactionResult { 116 | case let .success(value): 117 | result = value 118 | case let .failure(error): 119 | errorMessage = error.localizedDescription 120 | showError = true 121 | } 122 | } 123 | } 124 | 125 | struct Transaction: CodableData { 126 | let to: String 127 | let from: String 128 | let value: String 129 | let data: String? 130 | 131 | init(to: String, from: String, value: String, data: String? = nil) { 132 | self.to = to 133 | self.from = from 134 | self.value = value 135 | self.data = data 136 | } 137 | 138 | func socketRepresentation() -> NetworkData { 139 | [ 140 | "to": to, 141 | "from": from, 142 | "value": value, 143 | "data": data 144 | ] 145 | } 146 | } 147 | 148 | struct TransactionView_Previews: PreviewProvider { 149 | static var previews: some View { 150 | TransactionView() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import SwiftUI 8 | 9 | class ViewController: UIViewController { 10 | let connectView = ConnectView() 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | let childView = UIHostingController(rootView: connectView) 15 | addChild(childView) 16 | childView.view.frame = view.bounds 17 | view.addSubview(childView.view) 18 | childView.didMove(toParent: self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/metamask-ios-sdk/ViewExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExtension.swift 3 | // metamask-ios-sdk_Example 4 | // 5 | 6 | import SwiftUI 7 | 8 | extension View { 9 | func toast(isPresented: Binding, @ViewBuilder content: () -> ToastContent) -> some View { 10 | ZStack { 11 | self 12 | if isPresented.wrappedValue { 13 | ToastOverlay(content: content(), isPresented: isPresented) 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright ConsenSys Software Inc. 2022. All rights reserved. 2 | 3 | You acknowledge and agree that ConsenSys Software Inc. (“ConsenSys”) (or ConsenSys’s licensors) own all legal right, title and interest in and to the work, software, application, source code, documentation and any other documents in this repository (collectively, the “Program”), including any intellectual property rights which subsist in the Program (whether those rights happen to be registered or not, and wherever in the world those rights may exist), whether in source code or any other form. 4 | 5 | Subject to the limited license below, you may not (and you may not permit anyone else to) distribute, publish, copy, modify, merge, combine with another program, create derivative works of, reverse engineer, decompile or otherwise attempt to extract the source code of, the Program or any part thereof, except that you may contribute to this repository. 6 | 7 | You are granted a non-exclusive, non-transferable, non-sublicensable license to distribute, publish, copy, modify, merge, combine with another program or create derivative works of the Program (such resulting program, collectively, the “Resulting Program”) solely for Non-Commercial Use as long as you: 8 | 9 | 1. give prominent notice (“Notice”) with each copy of the Resulting Program that the Program is used in the Resulting Program and that the Program is the copyright of ConsenSys; and 10 | 2. subject the Resulting Program and any distribution, publication, copy, modification, merger therewith, combination with another program or derivative works thereof to the same Notice requirement and Non-Commercial Use restriction set forth herein. 11 | 12 | “Non-Commercial Use” means each use as described in clauses (1)-(3) below, as reasonably determined by ConsenSys in its sole discretion: 13 | 14 | 1. personal use for research, personal study, private entertainment, hobby projects or amateur pursuits, in each case without any anticipated commercial application; 15 | 2. use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization or government institution; or 16 | 3. the number of monthly active users of the Resulting Program across all versions thereof and platforms globally do not exceed 10,000 at any time. 17 | 18 | You will not use any trade mark, service mark, trade name, logo of ConsenSys or any other company or organization in a way that is likely or intended to cause confusion about the owner or authorized user of such marks, names or logos. 19 | 20 | If you have any questions, comments or interest in pursuing any other use cases, please reach out to us at metamask.license@consensys.net. 21 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "socket.io-client-swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/socketio/socket.io-client-swift", 7 | "state" : { 8 | "revision" : "175da8b5156f6b132436f0676cc84c2f6a766b6e", 9 | "version" : "16.1.0" 10 | } 11 | }, 12 | { 13 | "identity" : "starscream", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/daltoniam/Starscream", 16 | "state" : { 17 | "revision" : "ac6c0fc9da221873e01bd1a0d4818498a71eef33", 18 | "version" : "4.0.6" 19 | } 20 | } 21 | ], 22 | "version" : 2 23 | } 24 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "metamask-ios-sdk", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "metamask-ios-sdk", 15 | targets: ["metamask-ios-sdk"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(url: "https://github.com/socketio/socket.io-client-swift", .upToNextMajor(from: "16.1.0")) 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "metamask-ios-sdk", 27 | dependencies: [ 28 | .product(name: "SocketIO", package: "socket.io-client-swift"), 29 | "Ecies" 30 | ]), 31 | .binaryTarget( 32 | name: "Ecies", 33 | path: "Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework"), 34 | ], 35 | swiftLanguageVersions: [.version("5")] 36 | ) 37 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/Sources/metamask-ios-sdk/Assets/.gitkeep -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/API/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public enum Endpoint { 8 | public static var SERVER_URL = "https://metamask-sdk.api.cx.metamask.io/" 9 | 10 | case analytics 11 | 12 | public var url: String { 13 | switch self { 14 | case .analytics: 15 | return Endpoint.SERVER_URL.appending("evt") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/API/InfuraProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadOnlyRPCProvider.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public class ReadOnlyRPCProvider { 9 | let infuraAPIKey: String 10 | private let network: any Networking 11 | 12 | let rpcUrls: [String: String] 13 | let readonlyRPCMap: [String: String] 14 | 15 | public convenience init(infuraAPIKey: String? = nil, readonlyRPCMap: [String: String]? = nil) { 16 | self.init(infuraAPIKey: infuraAPIKey, readonlyRPCMap: readonlyRPCMap, network: Network()) 17 | } 18 | 19 | init(infuraAPIKey: String? = nil, readonlyRPCMap: [String: String]?, network: any Networking) { 20 | self.infuraAPIKey = infuraAPIKey ?? "" 21 | self.network = network 22 | self.readonlyRPCMap = readonlyRPCMap ?? [:] 23 | 24 | if let providedRPCMap = readonlyRPCMap { 25 | if let apiKey = infuraAPIKey { 26 | // Merge infuraReadonlyRPCMap with readonlyRPCMap, overriding infura's keys if they are present in readonlyRPCMap 27 | var mergedMap = ReadOnlyRPCProvider.infuraReadonlyRPCMap(apiKey) 28 | providedRPCMap.forEach { mergedMap[$0.key] = $0.value } 29 | self.rpcUrls = mergedMap 30 | } else { 31 | // Use only the provided readonlyRPCMap 32 | self.rpcUrls = providedRPCMap 33 | } 34 | } else if let apiKey = infuraAPIKey { 35 | // Use infuraReadonlyRPCMap as default 36 | self.rpcUrls = ReadOnlyRPCProvider.infuraReadonlyRPCMap(apiKey) 37 | } else { 38 | // Default to an empty map if neither are provided 39 | self.rpcUrls = [:] 40 | } 41 | } 42 | 43 | func supportsChain(_ chainId: String) -> Bool { 44 | return rpcUrls[chainId] != nil && (readonlyRPCMap[chainId] != nil || !infuraAPIKey.isEmpty) 45 | } 46 | 47 | static func infuraReadonlyRPCMap(_ infuraAPIKey: String) -> [String: String] { 48 | [ 49 | // ###### Ethereum ###### 50 | // Mainnet 51 | "0x1": "https://mainnet.infura.io/v3/\(infuraAPIKey)", 52 | // Sepolia 11155111 53 | "0x2a": "https://sepolia.infura.io/v3/\(infuraAPIKey)", 54 | // ###### Polygon ###### 55 | // Mainnet 56 | "0x89": "https://polygon-mainnet.infura.io/v3/\(infuraAPIKey)", 57 | // Mumbai 58 | "0x13881": "https://polygon-mumbai.infura.io/v3/\(infuraAPIKey)", 59 | // ###### Optimism ###### 60 | // Mainnet 61 | "0x45": "https://optimism-mainnet.infura.io/v3/\(infuraAPIKey)", 62 | // Goerli 63 | "0x1a4": "https://optimism-goerli.infura.io/v3/\(infuraAPIKey)", 64 | // ###### Arbitrum ###### 65 | // Mainnet 66 | "0xa4b1": "https://arbitrum-mainnet.infura.io/v3/\(infuraAPIKey)", 67 | // Goerli 68 | "0x66eed": "https://arbitrum-goerli.infura.io/v3/\(infuraAPIKey)", 69 | // ###### Palm ###### 70 | // Mainnet 71 | "0x2a15c308d": "https://palm-mainnet.infura.io/v3/\(infuraAPIKey)", 72 | // Testnet 73 | "0x2a15c3083": "https://palm-testnet.infura.io/v3/\(infuraAPIKey)", 74 | // ###### Avalanche C-Chain ###### 75 | // Mainnet 76 | "0xa86a": "https://avalanche-mainnet.infura.io/v3/\(infuraAPIKey)", 77 | // Fuji 78 | "0xa869": "https://avalanche-fuji.infura.io/v3/\(infuraAPIKey)", 79 | // ###### NEAR ###### 80 | // // Mainnet 81 | // "0x4e454152": "https://near-mainnet.infura.io/v3/\(infuraAPIKey)", 82 | // // Testnet 83 | // "0x4e454153": "https://near-testnet.infura.io/v3/\(infuraAPIKey)", 84 | // ###### Aurora ###### 85 | // Mainnet 86 | "0x4e454152": "https://aurora-mainnet.infura.io/v3/\(infuraAPIKey)", 87 | // Testnet 88 | "0x4e454153": "https://aurora-testnet.infura.io/v3/\(infuraAPIKey)", 89 | // ###### StarkNet ###### 90 | // Mainnet 91 | "0x534e5f4d41494e": "https://starknet-mainnet.infura.io/v3/\(infuraAPIKey)", 92 | // Goerli 93 | "0x534e5f474f45524c49": "https://starknet-goerli.infura.io/v3/\(infuraAPIKey)", 94 | // Goerli 2 95 | "0x534e5f474f45524c4932": "https://starknet-goerli2.infura.io/v3/\(infuraAPIKey)", 96 | // ###### Celo ###### 97 | // Mainnet 98 | "0xa4ec": "https://celo-mainnet.infura.io/v3/\(infuraAPIKey)", 99 | // Alfajores Testnet 100 | "0xaef3": "https://celo-alfajores.infura.io/v3/\(infuraAPIKey)" 101 | ] 102 | } 103 | 104 | func endpoint(for chainId: String) -> String? { 105 | rpcUrls[chainId] 106 | } 107 | 108 | public func sendRequest(_ request: any RPCRequest, 109 | params: Any = "", 110 | chainId: String, 111 | appMetadata: AppMetadata) async -> Any? { 112 | 113 | let params: [String: Any] = [ 114 | "method": request.method, 115 | "jsonrpc": "2.0", 116 | "id": request.id, 117 | "params": params 118 | ] 119 | 120 | guard let endpoint = endpoint(for: chainId) else { 121 | Logging.error("ReadOnlyRPCProvider:: Infura endpoint for chainId \(chainId) is not available") 122 | return nil 123 | } 124 | 125 | Logging.log("ReadOnlyRPCProvider:: Sending request \(request.method) on chain \(chainId) using endpoint \(endpoint) via Infura API") 126 | 127 | let devicePlatformInfo = DeviceInfo.platformDescription 128 | network.addHeaders([ 129 | "Metamask-Sdk-Info": "Sdk/iOS SdkVersion/\(SDKInfo.version) Platform/\(devicePlatformInfo) dApp/\(appMetadata.url) dAppTitle/\(appMetadata.name)" 130 | ] 131 | ) 132 | 133 | do { 134 | let response = try await network.post(params, endpoint: endpoint) 135 | let json: [String: Any] = try JSONSerialization.jsonObject( 136 | with: response, 137 | options: [] 138 | ) as? [String: Any] ?? [:] 139 | 140 | if let result = json["result"] { 141 | return result 142 | } 143 | 144 | Logging.error("ReadOnlyRPCProvider:: could not get result from response \(json)") 145 | if let error = json["error"] as? [String: Any] { 146 | return RequestError(from: error) 147 | } 148 | 149 | return nil 150 | } catch { 151 | Logging.error("ReadOnlyRPCProvider:: error: \(error.localizedDescription)") 152 | return RequestError(from: ["code": -1, "message": error.localizedDescription]) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/API/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // 4 | 5 | import SwiftUI 6 | 7 | public protocol Networking: ObservableObject { 8 | func addHeaders(_ headers: [String: String]) 9 | @discardableResult 10 | func post(_ parameters: [String: Any], endpoint: Endpoint) async throws -> Data 11 | func post(_ parameters: [String: Any], endpoint: String) async throws -> Data 12 | func fetch(_ Type: T.Type, endpoint: Endpoint) async throws -> T 13 | } 14 | 15 | public class Network: Networking { 16 | public init() {} 17 | 18 | private let queue = DispatchQueue(label: "headers.queue") 19 | 20 | private var additionalHeaders: [String: String] = [ 21 | "Accept": "application/json", 22 | "Content-Type": "application/json" 23 | ] 24 | 25 | func getAdditionalHeaders() -> [String: String] { 26 | return queue.sync { [weak self] in 27 | return self?.additionalHeaders ?? [:] 28 | } 29 | } 30 | 31 | func mergeHeaders(_ headers: [String: String]) { 32 | queue.sync { 33 | additionalHeaders.merge(headers) { (_, new) in new } 34 | } 35 | } 36 | 37 | public func fetch(_ Type: T.Type, endpoint: Endpoint) async throws -> T { 38 | guard let url = URL(string: endpoint.url) else { 39 | throw NetworkError.invalidUrl 40 | } 41 | 42 | let request = request(for: url) 43 | let (data, _) = try await URLSession.shared.data(for: request) 44 | let response = try JSONDecoder().decode(Type, from: data) 45 | return response 46 | } 47 | 48 | @discardableResult 49 | public func post(_ parameters: [String: Any], endpoint: Endpoint) async throws -> Data { 50 | try await post(parameters, endpoint: endpoint.url) 51 | } 52 | 53 | public func post(_ parameters: [String: Any], endpoint: String) async throws -> Data { 54 | guard let url = URL(string: endpoint) else { 55 | throw NetworkError.invalidUrl 56 | } 57 | 58 | var request = request(for: url) 59 | 60 | let payload = try JSONSerialization.data(withJSONObject: parameters, options: []) 61 | request.httpBody = payload 62 | request.httpMethod = "POST" 63 | 64 | let response = try await URLSession.shared.data(for: request) 65 | return response.0 66 | } 67 | 68 | public func addHeaders(_ headers: [String: String]) { 69 | mergeHeaders(headers) 70 | } 71 | 72 | private func request(for url: URL) -> URLRequest { 73 | var request = URLRequest(url: url) 74 | for (key, value) in getAdditionalHeaders() { 75 | request.addValue(value, forHTTPHeaderField: key) 76 | } 77 | 78 | return request 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/API/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkError.swift 3 | // 4 | 5 | import Foundation 6 | 7 | enum NetworkError: Error { 8 | case invalidUrl 9 | case invalidData 10 | } 11 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Analytics/Analytics.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Analytics.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol Tracking { 8 | var enableDebug: Bool { get set } 9 | func trackEvent(_ event: Event, parameters: [String: Any]) async 10 | } 11 | 12 | public class Analytics: Tracking { 13 | private let network: any Networking 14 | private var debug: Bool! 15 | 16 | public var enableDebug: Bool { 17 | get { debug } 18 | set { debug = newValue } 19 | } 20 | 21 | convenience init(debug: Bool) { 22 | self.init(network: Network(), debug: debug) 23 | } 24 | 25 | public init(network: any Networking, debug: Bool) { 26 | self.debug = debug 27 | self.network = network 28 | } 29 | 30 | public func trackEvent(_ event: Event, parameters: [String: Any]) async { 31 | if !debug { return } 32 | 33 | var params = parameters 34 | params["event"] = event.name 35 | 36 | do { 37 | try await network.post(params, endpoint: .analytics) 38 | } catch { 39 | Logging.error("tracking error: \(error.localizedDescription)") 40 | } 41 | } 42 | } 43 | 44 | extension Analytics { 45 | static let live = Dependencies.shared.tracker 46 | } 47 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Analytics/Event.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Event.swift 3 | // 4 | 5 | public enum Event: String { 6 | case sdkRpcRequest = "sdk_rpc_request" 7 | case sdkRpcRequestDone = "sdk_rpc_request_done" 8 | case connectionRequest = "sdk_connect_request_started" 9 | case reconnectionRequest = "sdk_reconnect_request_started" 10 | case connected = "sdk_connection_established" 11 | case connectionAuthorised = "sdk_connection_authorized" 12 | case connectionRejected = "sdk_connection_rejected" 13 | case disconnected = "sdk_disconnected" 14 | case connectionTerminated = "sdk_connection_terminated" 15 | 16 | var name: String { 17 | rawValue 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Backgroundtaskmanager/BackgroundTaskManager.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public enum BackgroundTaskManager { 4 | static var task: UIBackgroundTaskIdentifier = .init(rawValue: 0) 5 | 6 | public static func start() { 7 | task = UIApplication.shared.beginBackgroundTask(expirationHandler: { 8 | self.stop() 9 | }) 10 | } 11 | 12 | public static func stop() { 13 | UIApplication.shared.endBackgroundTask(task) 14 | task = UIBackgroundTaskIdentifier.invalid 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/CommClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommClient.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public typealias RequestJob = () -> Void 9 | 10 | public protocol CommClient { 11 | var channelId: String { get set } 12 | var appMetadata: AppMetadata? { get set } 13 | var sessionDuration: TimeInterval { get set } 14 | var onClientsTerminated: (() -> Void)? { get set } 15 | 16 | var trackEvent: ((Event, [String: Any]) -> Void)? { get set } 17 | var handleResponse: (([String: Any]) -> Void)? { get set } 18 | 19 | func connect(with request: String?) 20 | func disconnect() 21 | func clearSession() 22 | func requestAuthorisation() 23 | func addRequest(_ job: @escaping RequestJob) 24 | func sendMessage(_ message: T, encrypt: Bool, options: [String: String]) 25 | } 26 | 27 | public extension CommClient { 28 | func originatorInfo() -> RequestInfo { 29 | let originatorInfo = OriginatorInfo( 30 | title: appMetadata?.name, 31 | url: appMetadata?.url, 32 | icon: appMetadata?.iconUrl ?? appMetadata?.base64Icon, 33 | dappId: SDKInfo.bundleIdentifier, 34 | platform: SDKInfo.platform, 35 | apiVersion: appMetadata?.apiVersion ?? SDKInfo.version) 36 | 37 | return RequestInfo( 38 | type: "originator_info", 39 | originator: originatorInfo, 40 | originatorInfo: originatorInfo 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/CommClientFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommClientFactory.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public class CommClientFactory { 9 | func socketClient() -> CommClient { 10 | Dependencies.shared.socketClient 11 | } 12 | 13 | func deeplinkClient(dappScheme: String) -> CommClient { 14 | Dependencies.shared.deeplinkClient(dappScheme: dappScheme) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/CommLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommLayer.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | /** 9 | An enum representing the communication types supported for communication with MetaMask wallet 10 | **/ 11 | public enum Transport: CaseIterable, Identifiable, Hashable { 12 | /// Uses socket.io as a transport mechanism 13 | case socket 14 | /// Uses deeplinking as transport mechanism. Recommended. Requires setting URI scheme 15 | case deeplinking(dappScheme: String) 16 | 17 | public var id: String { 18 | switch self { 19 | case .socket: 20 | return "socket" 21 | case .deeplinking(let dappScheme): 22 | return "deeplinking_\(dappScheme)" 23 | } 24 | } 25 | 26 | public static var allCases: [Transport] { 27 | [.socket, .deeplinking(dappScheme: "")] 28 | } 29 | 30 | public var name: String { 31 | switch self { 32 | case .socket: return "Socket" 33 | case .deeplinking: return "Deeplinking" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/DeeplinkCommLayer/Deeplink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deeplink.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import Foundation 8 | 9 | public enum Deeplink: Equatable { 10 | case mmsdk(message: String, pubkey: String?, channelId: String?) 11 | case connect(pubkey: String?, channelId: String, request: String?) 12 | 13 | static let mmsdk = "mmsdk" 14 | static let connect = "connect" 15 | 16 | public static func == (lhs: Deeplink, rhs: Deeplink) -> Bool { 17 | switch (lhs, rhs) { 18 | case let (.mmsdk(messageLhs, pubkeyLhs, channelIdLhs), .mmsdk(messageRhs, pubkeyRhs, channelIdRhs)): 19 | return messageLhs == messageRhs && pubkeyLhs == pubkeyRhs && channelIdLhs == channelIdRhs 20 | case let (.connect(pubkeyLhs, channelIdLhs, requestLhs), .connect(pubkeyRhs, channelIdRhs, requestRhs)): 21 | return pubkeyLhs == pubkeyRhs && channelIdLhs == channelIdRhs && requestLhs == requestRhs 22 | default: 23 | return false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/DeeplinkCommLayer/DeeplinkClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeeplinkClient.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import Foundation 8 | 9 | public class DeeplinkClient: CommClient { 10 | 11 | private let session: SessionManager 12 | public var channelId: String = "" 13 | let dappScheme: String 14 | let urlOpener: URLOpener 15 | 16 | public var appMetadata: AppMetadata? 17 | public var trackEvent: ((Event, [String: Any]) -> Void)? 18 | public var handleResponse: (([String: Any]) -> Void)? 19 | public var onClientsTerminated: (() -> Void)? 20 | 21 | let keyExchange: KeyExchange 22 | let deeplinkManager: DeeplinkManager 23 | private var isConnecting = false 24 | 25 | public var sessionDuration: TimeInterval { 26 | get { 27 | session.sessionDuration 28 | } set { 29 | session.sessionDuration = newValue 30 | } 31 | } 32 | 33 | public var requestJobs: [RequestJob] = [] 34 | 35 | public init(session: SessionManager, 36 | keyExchange: KeyExchange, 37 | deeplinkManager: DeeplinkManager, 38 | dappScheme: String, 39 | urlOpener: URLOpener = DefaultURLOpener() 40 | ) { 41 | self.session = session 42 | self.keyExchange = keyExchange 43 | self.deeplinkManager = deeplinkManager 44 | self.dappScheme = dappScheme 45 | self.urlOpener = urlOpener 46 | setupClient() 47 | setupCallbacks() 48 | } 49 | 50 | private func setupCallbacks() { 51 | self.deeplinkManager.onReceiveMessage = handleMessage 52 | self.deeplinkManager.decryptMessage = keyExchange.decryptMessage 53 | } 54 | 55 | private func setupClient() { 56 | let sessionInfo = session.fetchSessionConfig() 57 | channelId = sessionInfo.0.sessionId 58 | } 59 | 60 | public func clearSession() { 61 | track(event: .disconnected) 62 | session.clear() 63 | setupClient() 64 | } 65 | 66 | public func requestAuthorisation() { 67 | 68 | } 69 | 70 | func sendMessage(_ message: String) { 71 | let deeplink = "metamask://\(message)" 72 | guard 73 | let urlString = deeplink.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), 74 | let url = URL(string: urlString) 75 | else { return } 76 | 77 | urlOpener.open(url) 78 | } 79 | 80 | public func handleUrl(_ url: URL) { 81 | deeplinkManager.handleUrl(url) 82 | } 83 | 84 | func sendMessage(_ deeplink: Deeplink, options: [String: String]) { 85 | switch deeplink { 86 | case .connect(_, let channelId, let request): 87 | let originatorInfo = originatorInfo().toJsonString()?.base64Encode() ?? "" 88 | var message = "connect?scheme=\(dappScheme)&channelId=\(channelId)&comm=deeplinking&originatorInfo=\(originatorInfo)" 89 | if let request = request { 90 | message.append("&request=\(request)") 91 | } 92 | sendMessage(message) 93 | case .mmsdk(let message, _, let channelId): 94 | let account = options["account"] ?? "" 95 | let chainId = options["chainId"] ?? "" 96 | let message = "mmsdk?scheme=\(dappScheme)&message=\(message)&channelId=\(channelId ?? "")&account=\(account)@\(chainId)" 97 | sendMessage(message) 98 | } 99 | } 100 | 101 | public func connect(with request: String? = nil) { 102 | track(event: .connectionRequest) 103 | isConnecting = true 104 | 105 | sendMessage(.connect( 106 | pubkey: nil, 107 | channelId: channelId, 108 | request: request 109 | ), options: [:]) 110 | } 111 | 112 | public func track(event: Event) { 113 | let parameters: [String: Any] = [ 114 | "id": channelId, 115 | "commLayer": "deeplinking", 116 | "sdkVersion": SDKInfo.version, 117 | "url": appMetadata?.url ?? "", 118 | "dappId": SDKInfo.bundleIdentifier ?? "N/A", 119 | "title": appMetadata?.name ?? "", 120 | "platform": SDKInfo.platform 121 | ] 122 | 123 | trackEvent?(event, parameters) 124 | } 125 | 126 | public func disconnect() { 127 | track(event: .disconnected) 128 | } 129 | 130 | public func terminateConnection() { 131 | track(event: .disconnected) 132 | } 133 | 134 | public func addRequest(_ job: @escaping RequestJob) { 135 | requestJobs.append(job) 136 | } 137 | 138 | public func runQueuedJobs() { 139 | while !requestJobs.isEmpty { 140 | let job = requestJobs.popLast() 141 | job?() 142 | } 143 | } 144 | 145 | public func sendMessage(_ message: T, encrypt: Bool, options: [String: String]) { 146 | guard let message = message as? String else { 147 | Logging.error("DeeplinkClient:: Expected message to be String, got \(type(of: message))") 148 | return 149 | } 150 | 151 | let base64Encoded = message.base64Encode() ?? "" 152 | 153 | let deeplink: Deeplink = .mmsdk( 154 | message: base64Encoded, 155 | pubkey: nil, 156 | channelId: channelId 157 | ) 158 | sendMessage(deeplink, options: options) 159 | } 160 | 161 | public func handleMessage(_ message: String) { 162 | do { 163 | guard let data = message.data(using: .utf8) else { 164 | Logging.error("DeeplinkClient:: Cannot convert message to data: \(message)") 165 | return 166 | } 167 | 168 | let json: [String: Any] = try JSONSerialization.jsonObject( 169 | with: data, 170 | options: [] 171 | ) 172 | as? [String: Any] ?? [:] 173 | 174 | if json["type"] as? String == "terminate" { 175 | disconnect() 176 | Logging.log("Connection terminated") 177 | } else if json["type"] as? String == "ready" { 178 | Logging.log("DeeplinkClient:: Connection is ready") 179 | runQueuedJobs() 180 | return 181 | } 182 | 183 | guard let data = json["data"] as? [String: Any] else { 184 | Logging.log("DeeplinkClient:: Ignoring response \(json)") 185 | return 186 | } 187 | if 188 | isConnecting, 189 | data["accounts"] != nil, 190 | data["chainId"] != nil { 191 | isConnecting = false 192 | track(event: .connected) 193 | } 194 | handleResponse?(data) 195 | } catch { 196 | Logging.error("DeeplinkClient:: Could not convert message to json. Message: \(message)\nError: \(error)") 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/DeeplinkCommLayer/DeeplinkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeeplinkManager.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import Foundation 8 | 9 | public class DeeplinkManager { 10 | public var onReceiveMessage: ((String) -> Void)? 11 | var decryptMessage: ((String) throws -> String?)? 12 | 13 | public init(onReceiveMessage: ( (String) -> Void)? = nil, decryptMessage: ( (String) -> String?)? = nil) { 14 | self.onReceiveMessage = onReceiveMessage 15 | self.decryptMessage = decryptMessage 16 | } 17 | 18 | public func handleUrl(_ url: URL) { 19 | handleUrl(url.absoluteString) 20 | } 21 | 22 | public func handleUrl(_ url: String) { 23 | let deeplink = getDeeplink(url) 24 | 25 | switch deeplink { 26 | case .mmsdk(let message, _, _): 27 | let base64Decoded = message.base64Decode() ?? "" 28 | 29 | onReceiveMessage?(base64Decoded) 30 | 31 | default: 32 | Logging.error("DeeplinkManager:: ignoring url \(url)") 33 | } 34 | } 35 | 36 | public func getDeeplink(_ link: String) -> Deeplink? { 37 | 38 | guard let url = URL(string: link) else { 39 | Logging.error("DeeplinkManager:: Deeplink has invalid url") 40 | return nil 41 | } 42 | 43 | guard url.scheme != nil else { 44 | Logging.error("DeeplinkManager:: Deeplink is missing scheme") 45 | return nil 46 | } 47 | 48 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { 49 | Logging.error("DeeplinkManager:: Deeplink missing components") 50 | return nil 51 | } 52 | 53 | guard let action = components.host else { 54 | Logging.error("DeeplinkManager:: Deeplink missing action") 55 | return nil 56 | } 57 | 58 | let pubkey = components.queryItems?.first(where: { $0.name == "pubkey" })?.value 59 | 60 | if action == Deeplink.connect { 61 | guard let channelId: String = components.queryItems?.first(where: { $0.name == "channelId" })?.value else { 62 | Logging.error("DeeplinkManager:: Connect step missing channelId") 63 | return nil 64 | } 65 | 66 | let request = components.queryItems?.first(where: { $0.name == "request" })?.value 67 | 68 | return .connect(pubkey: pubkey, channelId: channelId, request: request) 69 | 70 | } else if action == Deeplink.mmsdk { 71 | guard let message = components.queryItems?.first(where: { $0.name == "message" })?.value else { 72 | Logging.error("DeeplinkManager:: Deeplink missing message") 73 | return nil 74 | } 75 | 76 | let channelId = components.queryItems?.first(where: { $0.name == "channelId" })?.value 77 | 78 | return .mmsdk(message: message, pubkey: pubkey, channelId: channelId) 79 | } 80 | 81 | return nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/DeeplinkCommLayer/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String.swift 3 | // metamask-ios-sdk 4 | 5 | import Foundation 6 | 7 | extension String { 8 | func trimEscapingChars() -> Self { 9 | var unescapedString = replacingOccurrences(of: #"\""#, with: "\"") 10 | if unescapedString.hasPrefix("\"") && unescapedString.hasSuffix("\"") { 11 | unescapedString.removeFirst() 12 | unescapedString.removeLast() 13 | } 14 | return unescapedString 15 | } 16 | } 17 | 18 | func json(from value: Any) -> String? { 19 | // Recursive function to decode nested JSON strings 20 | func decodeNestedJson(_ value: Any) -> Any { 21 | if let arrayValue = value as? [Any] { 22 | // If it's an array, recursively decode each element 23 | return arrayValue.map { decodeNestedJson($0) } 24 | } else if let dictValue = value as? [String: Any] { 25 | // If it's a dictionary, recursively decode each value 26 | var decodedDict = [String: Any]() 27 | for (key, value) in dictValue { 28 | decodedDict[key] = decodeNestedJson(value) 29 | } 30 | return decodedDict 31 | } else { 32 | // If it's neither a string, array, nor dictionary, return the value as is 33 | return value 34 | } 35 | } 36 | 37 | // Decode any nested JSON strings recursively in the input dictionary 38 | let decodedJsonObject = decodeNestedJson(value) 39 | 40 | // Step 3: Convert the cleaned dictionary back to a JSON string 41 | guard let cleanedJsonData = try? JSONSerialization.data(withJSONObject: decodedJsonObject, options: []), 42 | let cleanedJsonString = String(data: cleanedJsonData, encoding: .utf8) else { 43 | Logging.error("Failed to serialize cleaned JSON dictionary") 44 | return nil 45 | } 46 | 47 | return cleanedJsonString 48 | } 49 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/DeeplinkCommLayer/URLOpener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLOpener.swift 3 | // 4 | 5 | import UIKit 6 | 7 | public protocol URLOpener { 8 | func open(_ url: URL) 9 | } 10 | 11 | public class DefaultURLOpener: URLOpener { 12 | public init() {} 13 | 14 | public func open(_ url: URL) { 15 | DispatchQueue.main.async { 16 | UIApplication.shared.open(url) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/SocketCommLayer/ClientEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientEvent.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public struct ClientEvent { 8 | public static var connected: String { 9 | "connection" 10 | } 11 | 12 | public static var disconnect: String { 13 | "disconnect" 14 | } 15 | 16 | public static var message: String { 17 | "message" 18 | } 19 | 20 | public static var terminate: String { 21 | "terminate" 22 | } 23 | 24 | public static var joinChannel: String { 25 | "join_channel" 26 | } 27 | 28 | public static func clientsConnected(on channel: String) -> String { 29 | "clients_connected".appending("-").appending(channel) 30 | } 31 | 32 | public static func clientDisconnected(on channel: String) -> String { 33 | "clients_disconnected".appending("-").appending(channel) 34 | } 35 | 36 | public static func message(on channelId: String) -> String { 37 | "message".appending("-").appending(channelId) 38 | } 39 | 40 | public static func config(on channelId: String) -> String { 41 | "config".appending("-").appending(channelId) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/SocketCommLayer/SocketChannel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketChannel.swift 3 | // 4 | 5 | import SocketIO 6 | import Foundation 7 | 8 | public class SocketChannel { 9 | public typealias ChannelData = SocketData 10 | public typealias EventType = SocketClientEvent 11 | 12 | public var networkUrl: String { 13 | get { 14 | _networkUrl 15 | } set { 16 | _networkUrl = newValue 17 | } 18 | } 19 | 20 | public var isConnected: Bool { 21 | socket.status == .connected 22 | } 23 | 24 | private var _networkUrl: String 25 | 26 | var socket: SocketProtocol! 27 | var socketManager: SocketManagerProtocol! 28 | 29 | public init(url: String = Endpoint.SERVER_URL) { 30 | _networkUrl = url 31 | configure(url: url) 32 | } 33 | 34 | private func configure(url: String) { 35 | guard let url = URL(string: url) else { 36 | Logging.error("Socket url is invalid") 37 | return 38 | } 39 | 40 | let options: SocketIOClientOption = .extraHeaders( 41 | [ 42 | "User-Agent": "SocketIOClient" 43 | ] 44 | ) 45 | 46 | socketManager = SocketManager( 47 | socketURL: url, 48 | config: [ 49 | .log(false), 50 | .forceWebsockets(true), 51 | options 52 | ] 53 | ) 54 | socket = socketManager.standardSocket 55 | } 56 | 57 | // MARK: Session 58 | 59 | public func connect() { 60 | socket.connect(withPayload: nil) 61 | } 62 | 63 | public func disconnect() { 64 | socket.disconnect() 65 | } 66 | 67 | public func tearDown() { 68 | socket.removeAllHandlers() 69 | } 70 | 71 | // MARK: Events 72 | public func on(_ event: SocketClientEvent, completion: @escaping ([Any]) -> Void) { 73 | socket.on(clientEvent: event, callback: { data, _ in 74 | DispatchQueue.main.async { 75 | completion(data) 76 | } 77 | }) 78 | } 79 | 80 | public func on(_ event: String, completion: @escaping ([Any]) -> Void) { 81 | socket.on(event, callback: { data, _ in 82 | DispatchQueue.main.async { 83 | completion(data) 84 | } 85 | }) 86 | } 87 | 88 | public func emit(_ event: String, _ item: CodableData) { 89 | socket.emit(event, item, completion: nil) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/SocketCommLayer/SocketClientProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketProtocol.swift 3 | // 4 | 5 | import Foundation 6 | import SocketIO 7 | 8 | protocol SocketProtocol { 9 | var status: SocketIOStatus { get } 10 | func connect(withPayload payload: [String: Any]?) 11 | func disconnect() 12 | func emit(_ event: String, _ items: SocketData..., completion: (() -> Void)?) 13 | @discardableResult 14 | func on(clientEvent event: SocketClientEvent, callback: @escaping ([Any], SocketAckEmitter) -> Void) -> UUID 15 | @discardableResult 16 | func on(_ event: String, callback: @escaping ([Any], SocketAckEmitter) -> Void) -> UUID 17 | func removeAllHandlers() 18 | } 19 | 20 | protocol SocketManagerProtocol { 21 | var standardSocket: SocketProtocol { get } 22 | } 23 | 24 | extension SocketIOClient: SocketProtocol { } 25 | 26 | extension SocketManager: SocketManagerProtocol { 27 | var standardSocket: SocketProtocol { 28 | self.defaultSocket as SocketProtocol 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/CommunicationLayer/SocketCommLayer/SocketMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SocketMessage.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | enum DecodingError: Error { 9 | case invalidMessage 10 | } 11 | 12 | public struct SocketMessage: CodableData, Mappable { 13 | public let id: String 14 | public var ackId: String? 15 | public let message: T 16 | public var clientType: String = "dapp" 17 | 18 | public init(id: String, message: T, ackId: String? = nil, clientType: String = "dapp") { 19 | self.id = id 20 | self.message = message 21 | self.ackId = ackId 22 | self.clientType = clientType 23 | } 24 | 25 | // Custom initializer for decoding 26 | public init(from decoder: Decoder) throws { 27 | let container = try decoder.container(keyedBy: CodingKeys.self) 28 | id = try container.decode(String.self, forKey: .id) 29 | ackId = try container.decodeIfPresent(String.self, forKey: .ackId) 30 | message = try container.decode(T.self, forKey: .message) 31 | clientType = try container.decodeIfPresent(String.self, forKey: .clientType) ?? "dapp" 32 | } 33 | 34 | // Custom method for encoding 35 | public func encode(to encoder: Encoder) throws { 36 | var container = encoder.container(keyedBy: CodingKeys.self) 37 | try container.encode(id, forKey: .id) 38 | try container.encode(ackId, forKey: .ackId) 39 | try container.encode(message, forKey: .message) 40 | try container.encode(clientType, forKey: .clientType) 41 | } 42 | 43 | private enum CodingKeys: String, CodingKey { 44 | case id 45 | case ackId 46 | case message 47 | case clientType 48 | } 49 | 50 | public func socketRepresentation() -> NetworkData { 51 | if let ack = ackId { 52 | [ 53 | "id": id, 54 | "ackId": ack, 55 | "clientType": clientType, 56 | "message": try? (message as? CodableData)?.socketRepresentation() 57 | ] 58 | } else { 59 | [ 60 | "id": id, 61 | "clientType": clientType, 62 | "message": try? (message as? CodableData)?.socketRepresentation() 63 | ] 64 | } 65 | } 66 | 67 | func toDictionary() -> [String: Any]? { 68 | let encoder = JSONEncoder() 69 | do { 70 | let jsonData = try encoder.encode(self) 71 | guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { 72 | print("Error converting JSON data to dictionary") 73 | Logging.error("Message:: Error converting JSON data to dictionary") 74 | return nil 75 | } 76 | return jsonObject 77 | } catch { 78 | print("Error encoding JSON: \(error)") 79 | Logging.error("Message:: Error encoding JSON: \(error)") 80 | return nil 81 | } 82 | } 83 | 84 | public static func message(from message: [String: Any]) throws -> SocketMessage { 85 | do { 86 | let json = try JSONSerialization.data(withJSONObject: message) 87 | let message = try JSONDecoder().decode(SocketMessage.self, from: json) 88 | return message 89 | } catch { 90 | Logging.error("Message \(message) could not be decoded: \(error.localizedDescription)") 91 | throw DecodingError.invalidMessage 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Crypto/Crypto.swift: -------------------------------------------------------------------------------- 1 | import Ecies 2 | import Foundation 3 | 4 | public protocol Crypto { 5 | /// Generates keypair and returns the asymmetric private key 6 | /// - Returns: Asymmetric private key 7 | static func generatePrivateKey() -> String 8 | 9 | /// Computes public key from given private key 10 | /// - Parameter privateKey: Sender's private key 11 | /// - Returns: Public key 12 | static func publicKey(from privateKey: String) throws -> String 13 | 14 | /// Encrypts plain text using provided public key 15 | /// - Parameters: 16 | /// - message: Plain text to encrypt 17 | /// - publicKey: Sender public key 18 | /// - Returns: Encrypted text 19 | static func encrypt(_ message: String, publicKey: String) throws -> String 20 | 21 | /// Decrypts base64 encoded cipher text to plain text using provided private key 22 | /// - Parameters: 23 | /// - message: base64 encoded cipher text to decrypt 24 | /// - privateKey: Receiever's private key 25 | /// - Returns: Decryted plain text 26 | static func decrypt(_ message: String, privateKey: String) throws -> String 27 | } 28 | 29 | public enum CryptoError: Error { 30 | case encryptionFailure 31 | case decryptionFailure 32 | case publicKeyGenerationFailure 33 | } 34 | 35 | /// Encryption module using ECIES encryption standard 36 | public enum Ecies: Crypto { 37 | public static func generatePrivateKey() -> String { 38 | String(cString: ecies_generate_secret_key()) 39 | } 40 | 41 | public static func publicKey(from privateKey: String) throws -> String { 42 | let privateKey: NSString = privateKey as NSString 43 | let privateKeyBytes = UnsafeMutablePointer(mutating: privateKey.utf8String) 44 | guard let pubKeyCString = ecies_public_key_from(privateKeyBytes) else { 45 | throw CryptoError.publicKeyGenerationFailure 46 | } 47 | return String(cString: pubKeyCString) 48 | } 49 | 50 | public static func encrypt(_ message: String, publicKey: String) throws -> String { 51 | let message: NSString = message as NSString 52 | let publicKey: NSString = publicKey as NSString 53 | let messageBytes = UnsafeMutablePointer(mutating: message.utf8String) 54 | let publicKeyBytes = UnsafeMutablePointer(mutating: publicKey.utf8String) 55 | 56 | guard let encryptedText = ecies_encrypt(publicKeyBytes, messageBytes) else { 57 | throw CryptoError.encryptionFailure 58 | } 59 | return String(cString: encryptedText) 60 | } 61 | 62 | public static func decrypt(_ message: String, privateKey: String) throws -> String { 63 | let message: NSString = message as NSString 64 | let privateKey: NSString = privateKey as NSString 65 | let messageBytes = UnsafeMutablePointer(mutating: message.utf8String) 66 | let privateKeyBytes = UnsafeMutablePointer(mutating: privateKey.utf8String) 67 | guard let decryptedText = ecies_decrypt(privateKeyBytes, messageBytes) else { 68 | throw CryptoError.decryptionFailure 69 | } 70 | return String(cString: decryptedText) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Crypto/Encoding.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension String { 4 | // Encode a string to base64 5 | func base64Encode() -> String? { 6 | guard let data = data(using: .utf8) else { return nil } 7 | return data.base64EncodedString() 8 | } 9 | 10 | // Decode a base64 string to original string 11 | func base64Decode() -> String? { 12 | guard let data = Data(base64Encoded: self) else { return nil } 13 | return String(data: data, encoding: .utf8) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Crypto/KeyExchange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyExchange.swift 3 | // 4 | 5 | import SocketIO 6 | import Foundation 7 | 8 | public enum KeyExchangeType: String, Mappable { 9 | case start = "key_handshake_start" 10 | case ack = "key_handshake_ACK" 11 | case syn = "key_handshake_SYN" 12 | case synack = "key_handshake_SYNACK" 13 | 14 | public init(from decoder: Decoder) throws { 15 | let container = try decoder.singleValueContainer() 16 | let status = try? container.decode(String.self) 17 | switch status { 18 | case "key_handshake_start": self = .start 19 | case "key_handshake_ACK": self = .ack 20 | case "key_handshake_SYN": self = .syn 21 | case "key_handshake_SYNACK": self = .synack 22 | default: 23 | self = .ack 24 | } 25 | } 26 | } 27 | 28 | public enum KeyExchangeError: Error { 29 | case keysNotExchanged 30 | case encodingError 31 | } 32 | 33 | public struct KeyExchangeMessage: CodableData, Mappable { 34 | public let type: KeyExchangeType 35 | public let pubkey: String? 36 | public var v: Int? 37 | public var clientType: String? = "dapp" 38 | 39 | public init(type: KeyExchangeType, pubkey: String?, v: Int? = 2, clientType: String? = "dapp") { 40 | self.type = type 41 | self.pubkey = pubkey 42 | self.clientType = clientType 43 | self.v = v 44 | } 45 | 46 | // Custom initializer for decoding 47 | public init(from decoder: Decoder) throws { 48 | let container = try decoder.container(keyedBy: CodingKeys.self) 49 | type = try container.decode(KeyExchangeType.self, forKey: .type) 50 | pubkey = try container.decodeIfPresent(String.self, forKey: .pubkey) 51 | v = try container.decodeIfPresent(Int.self, forKey: .v) ?? 2 52 | clientType = try container.decodeIfPresent(String.self, forKey: .clientType) ?? "dapp" 53 | } 54 | 55 | // Custom method for encoding 56 | public func encode(to encoder: Encoder) throws { 57 | var container = encoder.container(keyedBy: CodingKeys.self) 58 | try container.encode(type, forKey: .type) 59 | try container.encode(pubkey, forKey: .pubkey) 60 | try container.encode(v, forKey: .v) 61 | try container.encode(clientType, forKey: .clientType) 62 | } 63 | 64 | private enum CodingKeys: String, CodingKey { 65 | case type 66 | case pubkey 67 | case v 68 | case clientType 69 | } 70 | 71 | public func socketRepresentation() -> NetworkData { 72 | ["type": type.rawValue, "pubkey": pubkey, "v": v, "clientType": clientType] 73 | } 74 | } 75 | 76 | /* 77 | A module for handling key exchange between client and server 78 | The key exchange sequence is defined as: 79 | syn -> synack -> ack 80 | */ 81 | 82 | public class KeyExchange { 83 | private var privateKey: String 84 | public var pubkey: String 85 | public private(set) var theirPublicKey: String? 86 | 87 | private let storage: SecureStore 88 | private let encyption: Crypto.Type 89 | var keysExchanged: Bool = false 90 | var isKeysExchangedViaV2Protocol: Bool = false 91 | private let privateKeyStorageKey = "MM_SDK_PRIV_KEY" 92 | private let theirPubliKeyStorageKey = "MM_SDK_THEIR_PUB_KEY" 93 | 94 | public init(encryption: Crypto.Type = Ecies.self, storage: SecureStore) { 95 | self.storage = storage 96 | self.encyption = encryption 97 | 98 | if let storedPrivateKey = storage.string(for: privateKeyStorageKey) { 99 | Logging.log("KeyExchange:: using stored private key") 100 | self.privateKey = storedPrivateKey 101 | 102 | if let theirPubKey = storage.string(for: theirPubliKeyStorageKey) { 103 | self.theirPublicKey = theirPubKey 104 | 105 | // wallet already has keys 106 | keysExchanged = true 107 | isKeysExchangedViaV2Protocol = true 108 | } 109 | } else { 110 | Logging.log("KeyExchange:: generating new private key") 111 | privateKey = encyption.generatePrivateKey() 112 | } 113 | 114 | do { 115 | pubkey = try encyption.publicKey(from: privateKey) 116 | } catch { 117 | pubkey = "" 118 | } 119 | } 120 | 121 | public func reset() { 122 | keysExchanged = false 123 | theirPublicKey = nil 124 | privateKey = "" 125 | isKeysExchangedViaV2Protocol = false 126 | 127 | storage.deleteData(for: privateKeyStorageKey) 128 | storage.deleteData(for: theirPubliKeyStorageKey) 129 | privateKey = encyption.generatePrivateKey() 130 | 131 | do { 132 | pubkey = try encyption.publicKey(from: privateKey) 133 | } catch { 134 | pubkey = "" 135 | } 136 | } 137 | 138 | public func nextMessage(_ message: KeyExchangeMessage) -> KeyExchangeMessage? { 139 | if message.type == .start { 140 | keysExchanged = false 141 | } 142 | 143 | if 144 | let publicKey = message.pubkey, 145 | !publicKey.isEmpty { 146 | setTheirPublicKey(publicKey) 147 | 148 | if message.v == 2 { 149 | self.storage.save(string: privateKey, key: privateKeyStorageKey) 150 | self.storage.save(string: publicKey, key: theirPubliKeyStorageKey) 151 | isKeysExchangedViaV2Protocol = true 152 | } 153 | } 154 | 155 | guard let nextStep = nextStep(message.type) else { 156 | return nil 157 | } 158 | 159 | return KeyExchangeMessage( 160 | type: nextStep, 161 | pubkey: pubkey 162 | ) 163 | } 164 | 165 | public func nextStep(_ step: KeyExchangeType) -> KeyExchangeType? { 166 | switch step { 167 | case .start: return .syn 168 | case .syn: return .synack 169 | case .synack: return .ack 170 | case .ack: return nil 171 | } 172 | } 173 | 174 | public func message(type: KeyExchangeType) -> KeyExchangeMessage { 175 | KeyExchangeMessage( 176 | type: type, 177 | pubkey: pubkey 178 | ) 179 | } 180 | 181 | public func setTheirPublicKey(_ publicKey: String?) { 182 | guard let theirPubKey = publicKey else { return } 183 | 184 | theirPublicKey = theirPubKey 185 | keysExchanged = true 186 | storage.save(string: theirPubKey, key: theirPubliKeyStorageKey) 187 | storage.save(string: privateKey, key: privateKeyStorageKey) 188 | } 189 | 190 | public static func isHandshakeRestartMessage(_ message: [String: Any]) -> Bool { 191 | guard 192 | let message = message["message"] as? [String: Any], 193 | let type = message["type"] as? String, 194 | let exchangeType = KeyExchangeType(rawValue: type), 195 | exchangeType == .start 196 | else { return false } 197 | return true 198 | } 199 | 200 | public func encryptMessage(_ message: T) throws -> String { 201 | guard let theirPublicKey = theirPublicKey else { 202 | throw KeyExchangeError.keysNotExchanged 203 | } 204 | 205 | guard let encodedData = try? JSONEncoder().encode(message) else { 206 | throw KeyExchangeError.encodingError 207 | } 208 | 209 | guard let jsonString = String( 210 | data: encodedData, 211 | encoding: .utf8 212 | ) else { 213 | throw KeyExchangeError.encodingError 214 | } 215 | 216 | return try encyption.encrypt( 217 | jsonString, 218 | publicKey: theirPublicKey 219 | ) 220 | } 221 | 222 | public func encrypt(_ message: String) throws -> String { 223 | guard let theirPublicKey = theirPublicKey else { 224 | throw KeyExchangeError.keysNotExchanged 225 | } 226 | 227 | return try encyption.encrypt( 228 | message, 229 | publicKey: theirPublicKey 230 | ) 231 | } 232 | 233 | public func decryptMessage(_ message: String) throws -> String { 234 | guard theirPublicKey != nil else { 235 | throw KeyExchangeError.keysNotExchanged 236 | } 237 | 238 | return try encyption.decrypt( 239 | message, 240 | privateKey: privateKey 241 | ).trimEscapingChars() 242 | } 243 | } 244 | 245 | extension KeyExchange { 246 | static let live = Dependencies.shared.keyExchange 247 | } 248 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/DeviceInfo/DeviceInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceInfo.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import UIKit 7 | import Foundation 8 | 9 | public struct DeviceInfo { 10 | public static let platformDescription = "\(UIDevice.current.name) \(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" 11 | } 12 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/AppMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dapp.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public struct AppMetadata { 8 | public let name: String 9 | public let url: String 10 | public let iconUrl: String? 11 | public let base64Icon: String? 12 | public let apiVersion: String? 13 | 14 | var platform: String = "ios" 15 | 16 | public init(name: String, 17 | url: String, 18 | iconUrl: String? = nil, 19 | base64Icon: String? = nil, 20 | apiVersion: String? = nil 21 | ) { 22 | self.name = name 23 | self.url = url 24 | self.iconUrl = iconUrl 25 | self.apiVersion = apiVersion 26 | self.base64Icon = base64Icon 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/ErrorType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorType.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public enum ErrorType: Int { 9 | // MARK: Ethereum Provider 10 | 11 | case userRejectedRequest = 4001 // Ethereum Provider User Rejected Request 12 | case unauthorisedRequest = 4100 // Ethereum Provider User Rejected Request 13 | case unsupportedMethod = 4200 // Ethereum Provider Unsupported Method 14 | case disconnected = 4900 // Ethereum Provider Not Connected 15 | case chainDisconnected = 4901 // Ethereum Provider Chain Not Connected 16 | case unrecognizedChainId = 4902 // Unrecognized chain ID. Try adding the chain using wallet_addEthereumChain first 17 | 18 | // MARK: Ethereum RPC 19 | 20 | case invalidInput = -32000 // JSON RPC 2.0 Server error 21 | case transactionRejected = -32003 // Ethereum JSON RPC Transaction Rejected 22 | case invalidRequest = -32600 // JSON RPC 2.0 Invalid Request 23 | case invalidMethodParameters = -32602 // JSON RPC 2.0 Invalid Parameters 24 | case serverError = -32603 // Could be one of many outcomes 25 | case parseError = -32700 // JSON RPC 2.0 Parse error 26 | case unknownError = -1 // check RequestError.code instead 27 | 28 | static func isServerError(_ code: Int) -> Bool { 29 | code < -32000 && code >= -32099 30 | } 31 | 32 | var message: String { 33 | switch self { 34 | case .userRejectedRequest: 35 | return "User rejected the request" 36 | case .unauthorisedRequest: 37 | return "User rejected the request" 38 | case .unsupportedMethod: 39 | return "Unsupported method" 40 | case .disconnected: 41 | return "Not connected" 42 | case .chainDisconnected: 43 | return "Chain not connected" 44 | case .unrecognizedChainId: 45 | return "Unrecognized chain ID. Try adding the chain using addEthereumChain first" 46 | case .invalidInput: 47 | return "JSON RPC server error" 48 | case .transactionRejected: 49 | return "Transaction rejected" 50 | case .invalidRequest: 51 | return "Invalid request" 52 | case .invalidMethodParameters: 53 | return "Invalid method parameters" 54 | case .serverError: 55 | return "Server error" 56 | case .parseError: 57 | return "Parse error" 58 | case .unknownError: 59 | return "The request failed" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/EthereumMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EthereumMethod.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public enum EthereumMethod: String, CaseIterable, CodableData { 9 | case ethSign = "eth_sign" 10 | case web3Sha = "web3_sha3" 11 | case ethCall = "eth_call" 12 | case ethChainId = "eth_chainId" 13 | case ethGetCode = "eth_getCode" 14 | case ethAccounts = "eth_accounts" 15 | case ethGasPrice = "eth_gasPrice" 16 | case personalSign = "personal_sign" 17 | case ethGetBalance = "eth_getBalance" 18 | case watchAsset = "wallet_watchAsset" 19 | case ethBlockNumber = "eth_blockNumber" 20 | case ethEstimateGas = "eth_estimateGas" 21 | case ethGetStorageAt = "eth_getStorageAt" 22 | case ethSignTypedData = "eth_signTypedData" 23 | case ethGetBlockByHash = "eth_getBlockByHash" 24 | case web3ClientVersion = "web3_clientVersion" 25 | case ethRequestAccounts = "eth_requestAccounts" 26 | case ethSendTransaction = "eth_sendTransaction" 27 | case ethSignTypedDataV3 = "eth_signTypedData_v3" 28 | case ethSignTypedDataV4 = "eth_signTypedData_v4" 29 | case addEthereumChain = "wallet_addEthereumChain" 30 | case metamaskBatch = "metamask_batch" 31 | case metamaskOpen = "metamask_open" 32 | case personalEcRecover = "personal_ecRecover" 33 | case walletRevokePermissions = "wallet_revokePermissions" 34 | case walletRequestPermissions = "wallet_requestPermissions" 35 | case walletGetPermissions = "wallet_getPermissions" 36 | case metamaskConnectWith = "metamask_connectwith" 37 | case metaMaskChainChanged = "metamask_chainChanged" 38 | case ethSendRawTransaction = "eth_sendRawTransaction" 39 | case switchEthereumChain = "wallet_switchEthereumChain" 40 | case ethGetTransactionCount = "eth_getTransactionCount" 41 | case metaMaskConnectSign = "metamask_connectSign" 42 | case metaMaskAccountsChanged = "metamask_accountsChanged" 43 | case ethGetTransactionByHash = "eth_getTransactionByHash" 44 | case ethGetTransactionReceipt = "eth_getTransactionReceipt" 45 | case getMetamaskProviderState = "metamask_getProviderState" 46 | case ethGetBlockTransactionCountByHash = "eth_getBlockTransactionCountByHash" 47 | case ethGetBlockTransactionCountByNumber = "eth_getBlockTransactionCountByNumber" 48 | case unknownMethod = "unknown" 49 | 50 | static func requiresAuthorisation(_ method: EthereumMethod) -> Bool { 51 | let methods: [EthereumMethod] = [ 52 | .ethSign, 53 | .watchAsset, 54 | .metamaskOpen, 55 | .personalEcRecover, 56 | .walletRequestPermissions, 57 | .walletRevokePermissions, 58 | .walletGetPermissions, 59 | .personalSign, 60 | .metamaskBatch, 61 | .metaMaskConnectSign, 62 | .metamaskConnectWith, 63 | .ethSignTypedData, 64 | .ethRequestAccounts, 65 | .ethSendTransaction, 66 | .ethSignTypedDataV3, 67 | .ethSignTypedDataV4, 68 | .addEthereumChain, 69 | .switchEthereumChain 70 | ] 71 | 72 | return methods.contains(method) 73 | } 74 | 75 | static func isReadOnly(_ method: EthereumMethod) -> Bool { 76 | !requiresAuthorisation(method) 77 | } 78 | 79 | static func isResultMethod(_ method: EthereumMethod) -> Bool { 80 | let resultMethods: [EthereumMethod] = [ 81 | .ethSign, 82 | .watchAsset, 83 | .ethChainId, 84 | .personalSign, 85 | .metamaskBatch, 86 | .walletRevokePermissions, 87 | .walletGetPermissions, 88 | .walletRequestPermissions, 89 | .metaMaskConnectSign, 90 | .metamaskConnectWith, 91 | .ethSignTypedData, 92 | .ethRequestAccounts, 93 | .ethSendTransaction, 94 | .ethSignTypedDataV3, 95 | .ethSignTypedDataV4, 96 | .addEthereumChain, 97 | .switchEthereumChain, 98 | .getMetamaskProviderState 99 | ] 100 | 101 | return resultMethods.contains(method) 102 | } 103 | 104 | static func isConnectMethod(_ method: EthereumMethod) -> Bool { 105 | let connectMethods: [EthereumMethod] = [ 106 | .metaMaskConnectSign, 107 | .metamaskConnectWith 108 | ] 109 | return connectMethods.contains(method) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/EthereumRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EthereumRequest.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public struct EthereumRequest: RPCRequest { 8 | public var id: String 9 | public let method: String 10 | public var params: T 11 | 12 | public var methodType: EthereumMethod { 13 | EthereumMethod(rawValue: method) ?? .unknownMethod 14 | } 15 | 16 | public init(id: String = TimestampGenerator.timestamp(), 17 | method: String, 18 | params: T = "") { 19 | self.id = id 20 | self.method = method 21 | self.params = params 22 | } 23 | 24 | public init(id: String = TimestampGenerator.timestamp(), 25 | method: EthereumMethod, 26 | params: T = "") { 27 | self.id = id 28 | self.method = method.rawValue 29 | self.params = params 30 | } 31 | 32 | public func socketRepresentation() -> NetworkData { 33 | [ 34 | "id": id, 35 | "method": method, 36 | "parameters": try? params.socketRepresentation() 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/EthereumWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EthereumWrapper.swift 3 | // metamask-ios-sdk 4 | 5 | import Foundation 6 | 7 | class EthereumWrapper { 8 | var ethereum: Ethereum? 9 | static let shared = EthereumWrapper() 10 | } 11 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/RPCRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RPCRequest.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | import SocketIO 8 | 9 | public protocol RPCRequest: CodableData, Mappable { 10 | var id: String { get } 11 | var method: String { get } 12 | associatedtype ParameterType: CodableData 13 | var params: ParameterType { get } 14 | var methodType: EthereumMethod { get } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/RequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestError.swift 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | 8 | // MARK: - RequestError 9 | 10 | public struct RequestError: Codable, Error { 11 | public let code: Int 12 | public let message: String 13 | 14 | public init(from info: [String: Any]) { 15 | code = info["code"] as? Int ?? -1 16 | if let msg = info["message"] as? String ?? ErrorType(rawValue: code)?.message { 17 | message = msg 18 | } else if ErrorType.isServerError(code) { 19 | message = ErrorType.serverError.message 20 | } else { 21 | message = "Something went wrong" 22 | } 23 | } 24 | 25 | public var localizedDescription: String { 26 | message 27 | } 28 | 29 | public static var genericError: RequestError { 30 | RequestError(from: [ 31 | "code": -100, 32 | "message": "Something went wrong" 33 | ]) 34 | } 35 | 36 | public static var connectError: RequestError { 37 | RequestError(from: [ 38 | "code": -101, 39 | "message": "Not connected. Please connect first" 40 | ]) 41 | } 42 | 43 | public static var invalidUrlError: RequestError { 44 | RequestError(from: [ 45 | "code": -102, 46 | "message": "Please use a valid url in AppMetaData" 47 | ]) 48 | } 49 | 50 | public static var invalidTitleError: RequestError { 51 | RequestError(from: [ 52 | "code": -103, 53 | "message": "Please use a valid name in AppMetaData" 54 | ]) 55 | } 56 | 57 | public static var invalidBatchRequestError: RequestError { 58 | RequestError(from: [ 59 | "code": -104, 60 | "message": "Something went wrong, check that your requests are valid" 61 | ]) 62 | } 63 | 64 | public static var responseError: RequestError { 65 | RequestError(from: [ 66 | "code": -105, 67 | "message": "Unexpected response" 68 | ]) 69 | } 70 | 71 | static func failWithError(_ error: RequestError) -> EthereumPublisher { 72 | let passthroughSubject = PassthroughSubject() 73 | let publisher: EthereumPublisher = passthroughSubject 74 | .receive(on: DispatchQueue.main) 75 | .eraseToAnyPublisher() 76 | passthroughSubject.send(completion: .failure(error)) 77 | return publisher 78 | } 79 | } 80 | 81 | public extension RequestError { 82 | var codeType: ErrorType { 83 | guard let errorType = ErrorType(rawValue: code) else { 84 | return ErrorType.isServerError(code) ? .serverError : .unknownError 85 | } 86 | return errorType 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/ResponseMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResponseMethod.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | enum ResponseMethod: String { 9 | case ethSign = "eth_sign" 10 | case ethChainId = "eth_chainId" 11 | case personalSign = "personal_sign" 12 | case watchAsset = "wallet_watchAsset" 13 | case signTypedData = "eth_signTypedData" 14 | case requestAccounts = "eth_requestAccounts" 15 | case signTransaction = "eth_signTransaction" 16 | case sendTransaction = "eth_sendTransaction" 17 | case signTypedDataV3 = "eth_signTypedData_v3" 18 | case signTypedDataV4 = "eth_signTypedData_v4" 19 | case addEthereumChain = "wallet_addEthereumChain" 20 | case switchEthereumChain = "wallet_switchEthereumChain" 21 | case getMetamaskProviderState = "metamask_getProviderState" 22 | } 23 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/SubmitRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubmitRequest.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Combine 7 | import Foundation 8 | 9 | struct SubmittedRequest { 10 | let method: String 11 | private let requestSubject = PassthroughSubject() 12 | 13 | var publisher: EthereumPublisher? { 14 | requestSubject 15 | .receive(on: DispatchQueue.main) 16 | .eraseToAnyPublisher() 17 | } 18 | 19 | func send(_ value: Any) { 20 | requestSubject.send(value) 21 | } 22 | 23 | func error(_ err: RequestError) { 24 | requestSubject.send(completion: .failure(err)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Ethereum/TimestampGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimestampGenerator.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct TimestampGenerator { 9 | public static func timestamp() -> String { 10 | let currentDate = Date() 11 | let salt = Int64(arc4random_uniform(100)) + 1 12 | let time = Int64(currentDate.timeIntervalSince1970 * 1000) 13 | let uniqueTime = salt + time 14 | return String(uniqueTime) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Extensions/NSRecursiveLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRecursiveLock.swift 3 | // 4 | 5 | import Foundation 6 | 7 | extension NSRecursiveLock { 8 | @inlinable @discardableResult 9 | func sync(_ work: () -> Value) -> Value { 10 | lock() 11 | defer { unlock() } 12 | return work() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Extensions/Notification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public extension Notification.Name { 8 | static let MetaMaskAccountChanged = Notification.Name("MetaMaskAccountChanged") 9 | static let MetaMaskChainIdChanged = Notification.Name("MetaMaskChainChanged") 10 | } 11 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Logger/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // 4 | 5 | import OSLog 6 | import Foundation 7 | 8 | public class Logging { 9 | public static func log(_ message: String) { 10 | Logger().log("mmsdk| \(message)") 11 | } 12 | 13 | public static func error(_ error: String, file: String = #file, function: String = #function, line: Int = #line) { 14 | Logger().log( 15 | level: .error, 16 | "\n============\nmmsdk| Error: \(error)\nFunc: \(function)\nFile: \(fileName(from: file))\nLine: \(line)\n============\n" 17 | ) 18 | } 19 | 20 | public static func error(_ error: Error, file: String = #file, function: String = #function, line: Int = #line) { 21 | Logger().log(level: .error, "\n============\nmmsdk| Error \nFunc: \(function)\nFile: \(fileName(from: file))\nLine: \(line)\nError: \(error.localizedDescription)\n============\n") 22 | } 23 | 24 | private static func fileName(from path: String) -> String { 25 | path.components(separatedBy: "/").last ?? "" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/AddChainParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddChainParameters.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct AddChainParameters: CodableData { 9 | public let chainId: String 10 | public let chainName: String 11 | public let rpcUrls: [String] 12 | public let iconUrls: [String]? 13 | public let blockExplorerUrls: [String]? 14 | public let nativeCurrency: NativeCurrency 15 | 16 | public init(chainId: String, chainName: String, rpcUrls: [String], iconUrls: [String]?, blockExplorerUrls: [String]?, nativeCurrency: NativeCurrency) { 17 | self.chainId = chainId 18 | self.chainName = chainName 19 | self.rpcUrls = rpcUrls 20 | self.iconUrls = iconUrls 21 | self.blockExplorerUrls = blockExplorerUrls 22 | self.nativeCurrency = nativeCurrency 23 | } 24 | 25 | public func socketRepresentation() -> NetworkData { 26 | [ 27 | "chainId": chainId, 28 | "chainName": chainName, 29 | "rpcUrls": rpcUrls, 30 | "iconUrls": iconUrls ?? [], 31 | "blockExplorerUrls": blockExplorerUrls ?? [], 32 | "nativeCurrency": nativeCurrency.socketRepresentation() 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/Mappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mappable.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public protocol Mappable: Codable { } 9 | 10 | public extension Mappable { 11 | func toDictionary() -> [String: Any]? { 12 | let encoder = JSONEncoder() 13 | do { 14 | let jsonData = try encoder.encode(self) 15 | guard let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] else { 16 | Logging.error("Mappable:: Error converting JSON data to dictionary") 17 | return nil 18 | } 19 | return jsonObject 20 | } catch { 21 | print("Error encoding JSON: \(error)") 22 | Logging.error("Mappable:: Error encoding JSON: \(error)") 23 | return nil 24 | } 25 | } 26 | 27 | func toJsonString() -> String? { 28 | let encoder = JSONEncoder() 29 | do { 30 | let jsonData = try encoder.encode(self) 31 | return String(data: jsonData, encoding: .utf8) 32 | } catch { 33 | Logging.error("Error encoding JSON: \(error)") 34 | return nil 35 | } 36 | } 37 | } 38 | 39 | extension String: Mappable {} 40 | extension Dictionary: Mappable where Key == String, Value: Codable {} 41 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/NativeCurrency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NativeCurrency.swift 3 | // metamask-ios-sdk 4 | 5 | import Foundation 6 | 7 | public struct NativeCurrency: CodableData { 8 | public let name: String? 9 | public let symbol: String 10 | public let decimals: Int 11 | 12 | public init(name: String?, symbol: String, decimals: Int) { 13 | self.name = name 14 | self.symbol = symbol 15 | self.decimals = decimals 16 | } 17 | 18 | public func socketRepresentation() -> NetworkData { 19 | [ 20 | "name": name ?? "", 21 | "symbol": symbol, 22 | "decimals": decimals 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/OriginatorInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OriginatorInfo.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct OriginatorInfo: CodableData, Mappable { 9 | public let title: String? 10 | public let url: String? 11 | public let icon: String? 12 | public let dappId: String? 13 | public let platform: String? 14 | public let apiVersion: String? 15 | 16 | public func socketRepresentation() -> NetworkData { 17 | [ 18 | "title": title, 19 | "url": url, 20 | "icon": icon, 21 | "dappId": dappId, 22 | "platform": platform, 23 | "apiVersion": apiVersion 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/RequestInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestInfo.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct RequestInfo: CodableData, Mappable { 9 | public let type: String 10 | public let originator: OriginatorInfo 11 | public let originatorInfo: OriginatorInfo 12 | 13 | public func socketRepresentation() -> NetworkData { 14 | ["type": type, 15 | "originator": originator.socketRepresentation(), // Backward compatibility with MetaMask mobile 16 | "originatorInfo": originatorInfo.socketRepresentation()] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/SignContract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignContract.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct SignContract: Mappable { 9 | let id: String 10 | let method: String 11 | let params: [SignContractParameter] 12 | } 13 | 14 | public struct SignContractParameter: Mappable { 15 | let domain: Domain 16 | let message: SignMessage 17 | let primaryType: String 18 | let types: ParameterTypes 19 | } 20 | 21 | public struct Domain: Mappable { 22 | let chainId: String 23 | let name: String 24 | let verifyingContract: String 25 | let version: String 26 | } 27 | 28 | public struct SignMessage: Mappable { 29 | let contents: String 30 | let from: Person 31 | let to: Person 32 | } 33 | 34 | public struct Person: Mappable { 35 | let name: String 36 | let wallet: String 37 | } 38 | 39 | public struct ParameterTypes: Mappable { 40 | let EIP712Domain: [ParameterType] 41 | let Mail: [ParameterType] 42 | let Person: [ParameterType] 43 | } 44 | 45 | public struct ParameterType: Mappable { 46 | let name: String 47 | let type: String 48 | } 49 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Models/Typealiases.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typealiases.swift 3 | // 4 | 5 | import SocketIO 6 | import Foundation 7 | 8 | public typealias NetworkData = SocketData 9 | public typealias RequestTask = Task 10 | public typealias CodableData = Codable & SocketData 11 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Persistence/SecureStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureStore.swift 3 | // metamask-ios-sdk 4 | // 5 | import Foundation 6 | 7 | public protocol SecureStore { 8 | func string(for key: String) -> String? 9 | 10 | func data(for key: String) -> Data? 11 | 12 | @discardableResult 13 | func deleteData(for key: String) -> Bool 14 | 15 | @discardableResult 16 | func deleteAll() -> Bool 17 | 18 | @discardableResult 19 | func save(string: String, key: String) -> Bool 20 | 21 | @discardableResult 22 | func save(data: Data, key: String) -> Bool 23 | 24 | func model(for key: String) -> T? 25 | } 26 | 27 | public struct Keychain: SecureStore { 28 | private let service: String 29 | 30 | public init(service: String) { 31 | self.service = service 32 | } 33 | 34 | public func string(for key: String) -> String? { 35 | guard 36 | let data = data(for: key), 37 | let string = String(data: data, encoding: .utf8) 38 | else { return nil } 39 | return string 40 | } 41 | 42 | public func deleteData(for key: String) -> Bool { 43 | let request = deletionRequestForKey(key) 44 | let status: OSStatus = SecItemDelete(request) 45 | return status == errSecSuccess 46 | } 47 | 48 | public func deleteAll() -> Bool { 49 | let request = deletionRequestForAll() 50 | let status: OSStatus = SecItemDelete(request) 51 | return status == errSecSuccess 52 | } 53 | 54 | public func save(string: String, key: String) -> Bool { 55 | guard let data = string.data(using: .utf8) else { return false } 56 | return save(data: data, key: key) 57 | } 58 | 59 | @discardableResult 60 | public func save(data: Data, key: String) -> Bool { 61 | guard let attributes = attributes(for: data, key: key) else { return false } 62 | 63 | let status: OSStatus = SecItemAdd(attributes, nil) 64 | 65 | switch status { 66 | case noErr: 67 | return true 68 | case errSecDuplicateItem: 69 | guard deleteData(for: key) else { return false } 70 | return save(data: data, key: key) 71 | default: 72 | return false 73 | } 74 | } 75 | 76 | public func model(for key: String) -> T? { 77 | guard 78 | let data = data(for: key), 79 | let model = try? JSONDecoder().decode(T.self, from: data) 80 | else { return nil } 81 | 82 | return model 83 | } 84 | 85 | // MARK: Helper functions 86 | 87 | public func data(for key: String) -> Data? { 88 | let request = requestForKey(key) 89 | var dataTypeRef: CFTypeRef? 90 | let status: OSStatus = SecItemCopyMatching(request, &dataTypeRef) 91 | 92 | switch status { 93 | case errSecSuccess: 94 | return dataTypeRef as? Data 95 | default: 96 | return nil 97 | } 98 | } 99 | 100 | private func requestForKey(_ key: String) -> CFDictionary { 101 | [ 102 | kSecReturnData: true, 103 | kSecAttrAccount: key, 104 | kSecAttrService: service, 105 | kSecMatchLimit: kSecMatchLimitOne, 106 | kSecClass: kSecClassGenericPassword 107 | ] as CFDictionary 108 | } 109 | 110 | private func deletionRequestForKey(_ key: String) -> CFDictionary { 111 | [ 112 | kSecAttrAccount: key, 113 | kSecAttrService: service, 114 | kSecClass: kSecClassGenericPassword 115 | ] as CFDictionary 116 | } 117 | 118 | private func deletionRequestForAll() -> CFDictionary { 119 | [ 120 | kSecAttrService: service, 121 | kSecClass: kSecClassGenericPassword 122 | ] as CFDictionary 123 | } 124 | 125 | private func attributes(for data: Data, key: String) -> CFDictionary? { 126 | [ 127 | kSecValueData: data, 128 | kSecAttrAccount: key, 129 | kSecAttrService: service, 130 | kSecClass: kSecClassGenericPassword, 131 | kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 132 | ] as CFDictionary 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Persistence/SessionConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionConfig.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public class SessionConfig: Codable, Equatable { 9 | public static func == (lhs: SessionConfig, rhs: SessionConfig) -> Bool { 10 | lhs.sessionId == rhs.sessionId && lhs.expiry == rhs.expiry 11 | } 12 | 13 | public let sessionId: String 14 | public let expiry: Date 15 | 16 | public var isValid: Bool { 17 | expiry > Date() 18 | } 19 | 20 | public init(sessionId: String, expiry: Date) { 21 | self.sessionId = sessionId 22 | self.expiry = expiry 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/Persistence/SessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionManager.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public class SessionManager { 9 | private let store: SecureStore 10 | private let SESSION_KEY = "session_id" 11 | private let DEFAULT_SESSION_DURATION: TimeInterval = 24 * 7 * 3600 12 | 13 | public var sessionDuration: TimeInterval 14 | 15 | public init(store: SecureStore, 16 | sessionDuration: TimeInterval) { 17 | self.store = store 18 | self.sessionDuration = sessionDuration 19 | } 20 | 21 | public func fetchCurrentSessionConfig() -> SessionConfig? { 22 | let config: SessionConfig? = store.model(for: SESSION_KEY) 23 | return config 24 | } 25 | 26 | public func createNewSessionConfig() { 27 | // update session expiry date 28 | let config = SessionConfig(sessionId: UUID().uuidString.lowercased(), 29 | expiry: Date(timeIntervalSinceNow: sessionDuration)) 30 | if !config.isValid { 31 | sessionDuration = DEFAULT_SESSION_DURATION 32 | createNewSessionConfig() 33 | } 34 | // persist session config 35 | if let configData = try? JSONEncoder().encode(config) { 36 | store.save(data: configData, key: SESSION_KEY) 37 | } 38 | } 39 | 40 | public func fetchSessionConfig() -> (SessionConfig, Bool) { 41 | 42 | if let config = fetchCurrentSessionConfig(), config.isValid { 43 | return (config, true) 44 | } else { 45 | // purge any existing session info 46 | store.deleteData(for: SESSION_KEY) 47 | createNewSessionConfig() 48 | let config = fetchSessionConfig().0 49 | return (config, false) 50 | } 51 | } 52 | 53 | public func clear() { 54 | store.deleteAll() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/SDK/Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dependencies.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public final class Dependencies { 9 | public static let shared = Dependencies() 10 | 11 | public lazy var network: any Networking = Network() 12 | public lazy var tracker: Tracking = Analytics(network: network, debug: true) 13 | public lazy var store: SecureStore = Keychain(service: SDKInfo.bundleIdentifier ?? UUID().uuidString) 14 | public lazy var sessionManager: SessionManager = SessionManager(store: store, sessionDuration: 24 * 3600 * 30) 15 | 16 | public lazy var commClientFactory: CommClientFactory = CommClientFactory() 17 | 18 | public func ethereum(transport: Transport, sdkOptions: SDKOptions?) -> Ethereum { 19 | Ethereum.shared( 20 | transport: transport, 21 | store: store, 22 | commClientFactory: commClientFactory, 23 | readOnlyRPCProvider: ReadOnlyRPCProvider(infuraAPIKey: sdkOptions?.infuraAPIKey, readonlyRPCMap: sdkOptions?.readonlyRPCMap) 24 | ) { event, parameters in 25 | self.trackEvent(event, parameters: parameters) 26 | }.updateTransportLayer(transport) 27 | } 28 | 29 | public lazy var keyExchange: KeyExchange = KeyExchange(storage: store) 30 | 31 | public lazy var deeplinkManager: DeeplinkManager = DeeplinkManager() 32 | 33 | public lazy var socketClient: CommClient = SocketClient( 34 | session: sessionManager, 35 | channel: SocketChannel(), 36 | keyExchange: keyExchange, 37 | urlOpener: DefaultURLOpener(), 38 | trackEvent: { event, parameters in 39 | self.trackEvent(event, parameters: parameters) 40 | } 41 | ) 42 | 43 | public func deeplinkClient(dappScheme: String) -> DeeplinkClient { 44 | DeeplinkClient( 45 | session: sessionManager, 46 | keyExchange: keyExchange, 47 | deeplinkManager: deeplinkManager, 48 | dappScheme: dappScheme) 49 | } 50 | 51 | public func trackEvent(_ event: Event, parameters: [String: Any] = [:]) { 52 | Task { 53 | await self.tracker.trackEvent(event, parameters: parameters) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/SDK/MetaMaskSDK.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetaMaskSDK.swift 3 | // 4 | 5 | import SwiftUI 6 | import UIKit 7 | import Combine 8 | import Foundation 9 | 10 | class SDKWrapper { 11 | var sdk: MetaMaskSDK? 12 | static let shared = SDKWrapper() 13 | } 14 | 15 | public class MetaMaskSDK: ObservableObject { 16 | private var tracker: Tracking = Analytics.live 17 | private var ethereum: Ethereum! 18 | 19 | /// The active/selected MetaMask account chain 20 | @Published public var chainId: String = "" 21 | /// Indicated whether connected to MetaMask 22 | @Published public var connected: Bool = false 23 | 24 | /// The active/selected MetaMask account address 25 | @Published public var account: String = "" 26 | 27 | public static var sharedInstance: MetaMaskSDK? = SDKWrapper.shared.sdk 28 | 29 | /// In debug mode we track three events: connection request, connected, disconnected, otherwise no tracking 30 | public var enableDebug: Bool = true { 31 | didSet { 32 | tracker.enableDebug = enableDebug 33 | } 34 | } 35 | 36 | public var transport: Transport 37 | 38 | public var networkUrl: String { 39 | get { 40 | (ethereum.commClient as? SocketClient)?.networkUrl ?? "" 41 | } set { 42 | (ethereum.commClient as? SocketClient)?.networkUrl = newValue 43 | } 44 | } 45 | 46 | public var useDeeplinks: Bool = false { 47 | didSet { 48 | (ethereum.commClient as? SocketClient)?.useDeeplinks = useDeeplinks 49 | } 50 | } 51 | 52 | public var sessionDuration: TimeInterval { 53 | get { 54 | ethereum.commClient.sessionDuration 55 | } set { 56 | ethereum.commClient.sessionDuration = newValue 57 | } 58 | } 59 | 60 | private init(appMetadata: AppMetadata, transport: Transport, enableDebug: Bool, sdkOptions: SDKOptions?) { 61 | self.ethereum = Dependencies.shared.ethereum(transport: transport, sdkOptions: sdkOptions) 62 | self.transport = transport 63 | self.ethereum.delegate = self 64 | self.ethereum.updateMetadata(appMetadata) 65 | self.tracker.enableDebug = enableDebug 66 | self.account = ethereum.account 67 | self.chainId = ethereum.chainId 68 | setupAppLifeCycleObservers() 69 | } 70 | 71 | public var isMetaMaskInstalled: Bool { 72 | guard let url = URL(string: "metamask://") else { 73 | return false 74 | } 75 | return UIApplication.shared.canOpenURL(url) 76 | } 77 | 78 | public func handleUrl(_ url: URL) { 79 | (ethereum.commClient as? DeeplinkClient)?.handleUrl(url) 80 | } 81 | 82 | public static func shared(_ appMetadata: AppMetadata, 83 | transport: Transport, 84 | enableDebug: Bool = true, 85 | sdkOptions: SDKOptions?) -> MetaMaskSDK { 86 | guard let sdk = SDKWrapper.shared.sdk else { 87 | let metamaskSdk = MetaMaskSDK( 88 | appMetadata: appMetadata, 89 | transport: transport, 90 | enableDebug: enableDebug, 91 | sdkOptions: sdkOptions) 92 | SDKWrapper.shared.sdk = metamaskSdk 93 | return metamaskSdk 94 | } 95 | return sdk 96 | } 97 | 98 | public func updateTransportLayer(_ transport: Transport) { 99 | self.ethereum.updateTransportLayer(transport) 100 | } 101 | } 102 | 103 | public extension MetaMaskSDK { 104 | func connect() async -> Result<[String], RequestError> { 105 | await ethereum.connect() 106 | } 107 | 108 | func connectAndSign(message: String) async -> Result { 109 | await ethereum.connectAndSign(message: message) 110 | } 111 | 112 | func connectWith(_ request: EthereumRequest) async -> Result { 113 | await ethereum.connectWith(request) 114 | } 115 | 116 | func disconnect() { 117 | ethereum.disconnect() 118 | } 119 | 120 | func clearSession() { 121 | ethereum.clearSession() 122 | connected = false 123 | } 124 | 125 | func terminateConnection() { 126 | ethereum.terminateConnection() 127 | } 128 | 129 | func request(_ request: EthereumRequest) async -> Result { 130 | await ethereum.request(request) 131 | } 132 | 133 | func batchRequest(_ requests: [EthereumRequest]) async -> Result<[String], RequestError> { 134 | await ethereum.batchRequest(requests) 135 | } 136 | 137 | func getChainId() async -> Result { 138 | await ethereum.getChainId() 139 | } 140 | 141 | func getEthAccounts() async -> Result<[String], RequestError> { 142 | await ethereum.getEthAccounts() 143 | } 144 | 145 | func getEthGasPrice() async -> Result { 146 | await ethereum.getEthGasPrice() 147 | } 148 | 149 | func getEthBalance(address: String, block: String) async -> Result { 150 | await ethereum.getEthBalance(address: address, block: block) 151 | } 152 | 153 | func getEthBlockNumber() async -> Result { 154 | await ethereum.getEthBlockNumber() 155 | } 156 | 157 | func getEthEstimateGas() async -> Result { 158 | await ethereum.getEthEstimateGas() 159 | } 160 | 161 | func getWeb3ClientVersion() async -> Result { 162 | await ethereum.getWeb3ClientVersion() 163 | } 164 | 165 | func personalSign(message: String, address: String) async -> Result { 166 | await ethereum.personalSign(message: message, address: address) 167 | } 168 | 169 | func signTypedDataV4(typedData: String, address: String) async -> Result { 170 | await ethereum.signTypedDataV4(typedData: typedData, address: address) 171 | } 172 | 173 | func sendTransaction(from: String, to: String, value: String) async -> Result { 174 | await ethereum.sendTransaction(from: from, to: to, value: value) 175 | } 176 | 177 | func sendRawTransaction(signedTransaction: String) async -> Result { 178 | await ethereum.sendRawTransaction(signedTransaction: signedTransaction) 179 | } 180 | 181 | func getBlockTransactionCountByNumber(blockNumber: String) async -> Result { 182 | await ethereum.getBlockTransactionCountByNumber(blockNumber: blockNumber) 183 | } 184 | 185 | func getBlockTransactionCountByHash(blockHash: String) async -> Result { 186 | await ethereum.getBlockTransactionCountByHash(blockHash: blockHash) 187 | } 188 | 189 | func getTransactionCount(address: String, tagOrblockNumber: String) async -> Result { 190 | await ethereum.getTransactionCount(address: address, tagOrblockNumber: tagOrblockNumber) 191 | } 192 | 193 | func addEthereumChain(chainId: String, 194 | chainName: String, 195 | rpcUrls: [String], 196 | iconUrls: [String]?, 197 | blockExplorerUrls: [String]?, 198 | nativeCurrency: NativeCurrency) async -> Result { 199 | await ethereum.addEthereumChain( 200 | chainId: chainId, 201 | chainName: chainName, 202 | rpcUrls: rpcUrls, 203 | iconUrls: iconUrls, 204 | blockExplorerUrls: blockExplorerUrls, 205 | nativeCurrency: nativeCurrency 206 | ) 207 | } 208 | 209 | func switchEthereumChain(chainId: String) async -> Result { 210 | await ethereum.switchEthereumChain(chainId: chainId) 211 | } 212 | } 213 | 214 | extension MetaMaskSDK: EthereumEventsDelegate { 215 | func chainIdChanged(_ chainId: String) { 216 | self.chainId = chainId 217 | NotificationCenter.default.post(name: .MetaMaskChainIdChanged, object: nil, userInfo: ["chainId": chainId]) 218 | } 219 | 220 | func accountChanged(_ account: String) { 221 | self.account = account 222 | connected = true 223 | NotificationCenter.default.post(name: .MetaMaskAccountChanged, object: nil, userInfo: ["account": account]) 224 | } 225 | } 226 | 227 | private extension MetaMaskSDK { 228 | func setupAppLifeCycleObservers() { 229 | NotificationCenter.default.addObserver( 230 | self, 231 | selector: #selector(startBackgroundTask), 232 | name: UIApplication.willResignActiveNotification, 233 | object: nil 234 | ) 235 | 236 | NotificationCenter.default.addObserver( 237 | self, 238 | selector: #selector(stopBackgroundTask), 239 | name: UIApplication.didBecomeActiveNotification, 240 | object: nil 241 | ) 242 | } 243 | 244 | @objc 245 | func startBackgroundTask() { 246 | BackgroundTaskManager.start() 247 | } 248 | 249 | @objc 250 | func stopBackgroundTask() { 251 | BackgroundTaskManager.stop() 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/SDK/SDKInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDKInfo.swift 3 | // 4 | 5 | import UIKit 6 | import Foundation 7 | 8 | public enum SDKInfo { 9 | /// Bundle with SDK plist 10 | public static var sdkBundle: [String: Any] { 11 | Bundle(for: MetaMaskSDK.self).infoDictionary ?? [:] 12 | } 13 | 14 | /// The version number of the SDK e.g `1.0.0` 15 | public static var version: String { 16 | sdkBundle["CFBundleShortVersionString"] as? String ?? "" 17 | } 18 | 19 | /// The bundle identifier of the dapp 20 | public static var bundleIdentifier: String? { 21 | Bundle.main.bundleIdentifier 22 | } 23 | 24 | /// The platform OS on which the SDK is run e.g `ios, ipados` 25 | public static var platform: String { 26 | UIDevice.current.systemName.lowercased() 27 | } 28 | 29 | // Checks if Dapp is configured for Deeplink communication layer 30 | public static func isConfiguredForURLScheme(_ scheme: String) -> Bool { 31 | guard let urlTypes = sdkBundle["CFBundleURLTypes"] as? [AnyObject], 32 | let urlTypeDictionary = urlTypes.first as? [String: AnyObject], 33 | let urlSchemes = urlTypeDictionary["CFBundleURLSchemes"] as? [String] 34 | else { return false } 35 | return urlSchemes.contains(scheme) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Classes/SDK/SDKOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SDKOptions.swift 3 | // metamask-ios-sdk 4 | // 5 | 6 | import Foundation 7 | 8 | public struct SDKOptions { 9 | public let infuraAPIKey: String 10 | public let readonlyRPCMap: [String: String] 11 | 12 | public init(infuraAPIKey: String, readonlyRPCMap: [String: String] = [:]) { 13 | self.infuraAPIKey = infuraAPIKey 14 | self.readonlyRPCMap = readonlyRPCMap 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AvailableLibraries 6 | 7 | 8 | BinaryPath 9 | libecies.a 10 | HeadersPath 11 | Headers 12 | LibraryIdentifier 13 | ios-arm64 14 | LibraryPath 15 | libecies.a 16 | SupportedArchitectures 17 | 18 | arm64 19 | 20 | SupportedPlatform 21 | ios 22 | 23 | 24 | BinaryPath 25 | libecies.a 26 | HeadersPath 27 | Headers 28 | LibraryIdentifier 29 | ios-arm64-simulator 30 | LibraryPath 31 | libecies.a 32 | SupportedArchitectures 33 | 34 | arm64 35 | 36 | SupportedPlatform 37 | ios 38 | SupportedPlatformVariant 39 | simulator 40 | 41 | 42 | CFBundlePackageType 43 | XFWK 44 | XCFrameworkFormatVersion 45 | 1.0 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/ecies.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | const char *ecies_generate_secret_key(void); 7 | 8 | const char *ecies_public_key_from(const char *secret_key_ptr); 9 | 10 | const char *ecies_encrypt(const char *public_key_ptr, const char *message_ptr); 11 | 12 | const char *ecies_decrypt(const char *secret_key_ptr, const char *message_ptr); 13 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64-simulator/Headers/module.modulemap: -------------------------------------------------------------------------------- 1 | module Ecies { 2 | header "ecies.h" 3 | export * 4 | } -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64-simulator/libecies.a -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64/Headers/ecies.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | const char *ecies_generate_secret_key(void); 7 | 8 | const char *ecies_public_key_from(const char *secret_key_ptr); 9 | 10 | const char *ecies_encrypt(const char *public_key_ptr, const char *message_ptr); 11 | 12 | const char *ecies_decrypt(const char *secret_key_ptr, const char *message_ptr); 13 | -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64/Headers/module.modulemap: -------------------------------------------------------------------------------- 1 | module Ecies { 2 | header "ecies.h" 3 | export * 4 | } -------------------------------------------------------------------------------- /Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64/libecies.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/metamask-ios-sdk/924d91bb3e98a5383c3082d6d5ba3ddac9e1c565/Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework/ios-arm64/libecies.a -------------------------------------------------------------------------------- /metamask-ios-sdk.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'metamask-ios-sdk' 3 | s.version = '0.8.10' 4 | s.summary = 'Enable users to easily connect with their MetaMask Mobile wallet.' 5 | s.swift_version = '5.5' 6 | 7 | s.description = <<-DESC 8 | The iOS MetaMask SDK enables native iOS apps to interact with the Ethereum blockchain via the MetaMask Mobile wallet. 9 | DESC 10 | 11 | s.homepage = 'https://github.com/MetaMask/metamask-ios-sdk' 12 | s.license = { :type => 'Copyright ConsenSys Software Inc. 2022. All rights reserved.', :file => 'LICENSE' } 13 | s.author = { 'MetaMask' => 'sdk@metamask.io' } 14 | s.source = { :git => 'https://github.com/MetaMask/metamask-ios-sdk.git', :tag => s.version.to_s } 15 | 16 | s.ios.deployment_target = '15.0' 17 | s.source_files = 'Sources/metamask-ios-sdk/Classes/**/*' 18 | s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 19 | s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 20 | 21 | s.vendored_frameworks = 'Sources/metamask-ios-sdk/Frameworks/Ecies.xcframework' 22 | s.dependency 'Starscream', '4.0.6' 23 | s.dependency 'Socket.IO-Client-Swift', '~> 16.1.0' 24 | end 25 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Unique sonar data by project 2 | sonar.projectKey=metamask-ios-sdk 3 | sonar.organization=metamask 4 | 5 | # Source 6 | sonar.sources=Sources/metamask-ios-sdk/Classes 7 | sonar.exclusions=Exammple/** 8 | 9 | # Tests 10 | # sonar.tests=src,test 11 | # sonar.test.inclusions=**/*.test.js,**/*.test.ts,**/*.test.tsx 12 | # sonar.javascript.lcov.reportPaths=coverage/coverage/lcov.info 13 | 14 | # Block PRs with quality gate failures 15 | sonar.qualitygate.wait=false 16 | --------------------------------------------------------------------------------