├── .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 |
--------------------------------------------------------------------------------