├── .github
├── CODEOWNERS
└── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ ├── config.yml
│ └── feature_request.md
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetSelector.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── kotlinc.xml
├── migrations.xml
├── misc.xml
├── other.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── metamask
│ │ └── dapp
│ │ └── ExampleInstrumentedTest.kt
│ ├── debug
│ ├── app_icon-playstore.png
│ └── res
│ │ ├── drawable
│ │ └── app_icon_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── app_icon.xml
│ │ └── app_icon_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-mdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ └── values
│ │ └── app_icon_background.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── metamask
│ │ │ └── dapp
│ │ │ ├── AppModule.kt
│ │ │ ├── AppTopBar.kt
│ │ │ ├── BatchSignMessageScreen.kt
│ │ │ ├── ConnectScreen.kt
│ │ │ ├── DappActionsScreen.kt
│ │ │ ├── DappButton.kt
│ │ │ ├── DappLabel.kt
│ │ │ ├── DappScreen.kt
│ │ │ ├── EthereumFlowViewModel.kt
│ │ │ ├── EthereumViewModel.kt
│ │ │ ├── Heading.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MetaMaskDappApplication.kt
│ │ │ ├── ReadOnlyCallsScreen.kt
│ │ │ ├── ScreenViewModel.kt
│ │ │ ├── SendTransactionScreen.kt
│ │ │ ├── Setup.kt
│ │ │ ├── SignMessageScreen.kt
│ │ │ ├── SwitchChainScreen.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── stackoverflow.png
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ ├── release
│ ├── app_icon-playstore.png
│ └── res
│ │ ├── drawable
│ │ └── app_icon_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── app_icon.xml
│ │ └── app_icon_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-mdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── app_icon.png
│ │ └── app_icon_round.png
│ │ └── values
│ │ └── app_icon_background.xml
│ └── test
│ └── java
│ └── com
│ └── metamask
│ └── dapp
│ └── ExampleUnitTest.kt
├── build.gradle
├── ecies
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── io
│ │ └── metamask
│ │ └── ecies
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── io
│ │ │ └── metamask
│ │ │ └── ecies
│ │ │ └── Ecies.kt
│ └── jniLibs
│ │ ├── arm64-v8a
│ │ └── libecies.so
│ │ ├── armeabi-v7a
│ │ └── libecies.so
│ │ ├── x86
│ │ └── libecies.so
│ │ └── x86_64
│ │ └── libecies.so
│ └── test
│ └── java
│ └── io
│ └── metamask
│ └── ecies
│ └── ExampleUnitTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── metamask-android-sdk
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── io
│ │ └── metamask
│ │ └── androidsdk
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ │ └── io
│ │ │ └── metamask
│ │ │ └── nativesdk
│ │ │ ├── IMessegeService.aidl
│ │ │ └── IMessegeServiceCallback.aidl
│ ├── java
│ │ └── io
│ │ │ └── metamask
│ │ │ └── androidsdk
│ │ │ ├── Analytics.kt
│ │ │ ├── AnyRequest.kt
│ │ │ ├── ClientMessageServiceCallback.kt
│ │ │ ├── ClientServiceConnection.kt
│ │ │ ├── CommunicationClient.kt
│ │ │ ├── CommunicationClientModule.kt
│ │ │ ├── CommunicationClientModuleInterface.kt
│ │ │ ├── Constants.kt
│ │ │ ├── Crypto.kt
│ │ │ ├── DappMetadata.kt
│ │ │ ├── DeviceInfo.kt
│ │ │ ├── Encryption.kt
│ │ │ ├── ErrorType.kt
│ │ │ ├── Ethereum.kt
│ │ │ ├── EthereumEventCallback.kt
│ │ │ ├── EthereumFlow.kt
│ │ │ ├── EthereumMethod.kt
│ │ │ ├── EthereumRequest.kt
│ │ │ ├── EthereumState.kt
│ │ │ ├── HttpClient.kt
│ │ │ ├── InfuraProvider.kt
│ │ │ ├── KeyExchange.kt
│ │ │ ├── KeyExchangeMessageType.kt
│ │ │ ├── KeyStorage.kt
│ │ │ ├── Logger.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── Message.kt
│ │ │ ├── MessageType.kt
│ │ │ ├── NativeCurrency.kt
│ │ │ ├── Network.kt
│ │ │ ├── OriginatorInfo.kt
│ │ │ ├── ReadOnlyRPCProvider.kt
│ │ │ ├── RequestError.kt
│ │ │ ├── RequestInfo.kt
│ │ │ ├── Result.kt
│ │ │ ├── RpcRequest.kt
│ │ │ ├── SDKInfo.kt
│ │ │ ├── SDKOptions.kt
│ │ │ ├── SecureStorage.kt
│ │ │ ├── SessionConfig.kt
│ │ │ ├── SessionManager.kt
│ │ │ ├── SubmittedRequest.kt
│ │ │ └── TimeStampGenerator.kt
│ └── res
│ │ ├── layout
│ │ └── activity_main.xml
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── io
│ └── metamask
│ └── androidsdk
│ ├── CommunicationClientTests.kt
│ ├── CryptoTests.kt
│ ├── EthereumTests.kt
│ ├── KeyExchangeTests.kt
│ ├── KeyStorageTests.kt
│ ├── MockClientMessageServiceCallback.kt
│ ├── MockClientServiceConnection.kt
│ ├── MockCommunicationClientModule.kt
│ ├── MockCrypto.kt
│ ├── MockEthereumEventCallback.kt
│ ├── MockKeyStorage.kt
│ ├── MockReadOnlyRPCProvider.kt
│ ├── MockTracker.kt
│ ├── RSAEncryption.kt
│ ├── SessionConfigTests.kt
│ ├── SessionManagerTests.kt
│ └── TestLogger.kt
├── nativesdk
├── .gitignore
├── README.md
├── build.gradle.kts
├── consumer-rules.pro
├── illustrations
│ └── architecture.png
├── libs
│ └── ecies.aar
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── io
│ │ └── metamask
│ │ └── nativesdk
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── aidl
│ │ └── io
│ │ │ └── metamask
│ │ │ └── nativesdk
│ │ │ ├── IMessegeService.aidl
│ │ │ └── IMessegeServiceCallback.aidl
│ └── java
│ │ └── io
│ │ └── metamask
│ │ └── nativesdk
│ │ ├── Analytics.kt
│ │ ├── CommunicationClient.kt
│ │ ├── ConnectionStatusManager.kt
│ │ ├── Constants.kt
│ │ ├── Crypto.kt
│ │ ├── Endpoint.kt
│ │ ├── Event.kt
│ │ ├── EventType.kt
│ │ ├── HttpClient.kt
│ │ ├── KeyExchange.kt
│ │ ├── KeyExchangeMessageType.kt
│ │ ├── Logger.kt
│ │ ├── MessageService.kt
│ │ ├── MessageType.kt
│ │ ├── MetaMaskConnectionStatusCallback.kt
│ │ ├── NativeSDKPackage.kt
│ │ └── SessionManager.kt
│ └── test
│ └── java
│ └── io
│ └── metamask
│ └── nativesdk
│ └── ExampleUnitTest.kt
├── scripts
├── ecies-publish-module.gradle
├── metamask-android-sdk-publish-module.gradle
└── publish-auth.gradle
└── settings.gradle
/.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
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a bug report for the MetaMask Android SDK
3 | title: "[Bug]: "
4 | labels: ["template: bug"]
5 | body:
6 | - type: textarea
7 | attributes:
8 | label: Provide environment information
9 | description: "Give us a general overview of the environment where the issue is happening"
10 | validations:
11 | required: true
12 | - type: input
13 | attributes:
14 | label: MetaMask Android SDK Version
15 | description: "Please specify the exact MetaMask SDK version"
16 | validations:
17 | required: true
18 | - type: input
19 | attributes:
20 | label: MetaMask Mobile app Version
21 | description: "Please specify the exact MetaMask Mobile app version"
22 | validations:
23 | required: true
24 | - type: input
25 | attributes:
26 | label: Android Version
27 | description: "Please specify the exact version. For example: Android 12.1"
28 | validations:
29 | required: true
30 | - type: textarea
31 | attributes:
32 | label: Describe the Bug
33 | description: A clear and concise description of what the bug is.
34 | validations:
35 | required: true
36 | - type: textarea
37 | attributes:
38 | label: Expected Behavior
39 | description: A clear and concise description of what you expected to happen.
40 | validations:
41 | required: true
42 | - type: input
43 | attributes:
44 | label: Link to reproduction - Issues with a link to complete (but minimal) reproduction code will be addressed faster
45 | description: A link to a GitHub repository 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.
46 | validations:
47 | required: false
48 | - type: textarea
49 | attributes:
50 | label: To Reproduce
51 | 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.
52 | validations:
53 | required: true
54 | - type: markdown
55 | attributes:
56 | value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear.
57 |
--------------------------------------------------------------------------------
/.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/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: enhancement
6 | assignees: ""
7 | ---
8 |
9 | # Feature Request
10 |
11 |
14 |
15 | ## Why it is needed
16 |
17 |
20 |
21 | ## Possible implementation
22 |
23 |
26 |
27 | ### Code sample
28 |
29 |
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | MetaMaskAndroidSDKClient
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
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 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'dagger.hilt.android.plugin'
6 | }
7 |
8 | android {
9 | namespace 'com.metamask.dapp'
10 | compileSdk 33
11 |
12 | defaultConfig {
13 | applicationId "com.metamask.dapp"
14 | minSdk 23
15 | targetSdk 33
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_11
27 | targetCompatibility JavaVersion.VERSION_11
28 | }
29 | kotlinOptions {
30 | jvmTarget = "11"
31 | }
32 | kapt {
33 | javacOptions {
34 | // Set Java version for KAPT explicitly
35 | option("-source", "11")
36 | option("-target", "11")
37 | }
38 | }
39 | buildFeatures {
40 | compose true
41 | }
42 | composeOptions {
43 | kotlinCompilerExtensionVersion '1.4.0'
44 | }
45 | packagingOptions {
46 | resources {
47 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
48 | }
49 | }
50 | }
51 |
52 | kotlin {
53 | jvmToolchain(11)
54 | }
55 |
56 | dependencies {
57 | implementation project(':metamask-android-sdk')
58 | implementation 'androidx.compose.material:material:1.0.4'
59 | implementation 'com.google.android.material:material:1.9.0'
60 | implementation "androidx.navigation:navigation-compose:2.5.0"
61 | implementation 'androidx.compose.runtime:runtime-livedata:1.4.3'
62 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
63 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
64 | implementation "com.google.dagger:hilt-android:$hilt_version"
65 | implementation 'androidx.navigation:navigation-runtime-ktx:2.6.0'
66 | kapt "com.google.dagger:hilt-compiler:$hilt_version"
67 | implementation 'androidx.core:core-ktx:1.7.0'
68 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
69 | implementation 'androidx.activity:activity-compose:1.3.1'
70 |
71 |
72 | implementation "androidx.compose.ui:ui:$compose_version"
73 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
74 | implementation 'androidx.compose.material3:material3:1.0.0'
75 | testImplementation 'junit:junit:4.13.2'
76 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
77 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
78 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
79 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
80 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
81 | }
82 |
83 | kapt {
84 | correctErrorTypes true
85 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/metamask/dapp/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.metamask.dapp", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/debug/app_icon-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/app_icon-playstore.png
--------------------------------------------------------------------------------
/app/src/debug/res/drawable/app_icon_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-anydpi-v26/app_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-anydpi-v26/app_icon_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-hdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-hdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-hdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-hdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-mdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-mdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-mdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-mdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xxhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xxhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xxxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xxxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/debug/res/mipmap-xxxhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/debug/res/mipmap-xxxhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/debug/res/values/app_icon_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import android.content.Context
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import io.metamask.androidsdk.*
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 |
15 | internal object AppModule {
16 | @Provides
17 | fun provideDappMetadata(): DappMetadata {
18 | return DappMetadata("Droiddapp", "https://www.droiddapp.io", iconUrl = "https://cdn.sstatic.net/Sites/stackoverflow/Img/apple-touch-icon.png")
19 | }
20 |
21 | @Provides
22 | fun provideLogger(): Logger {
23 | return DefaultLogger
24 | }
25 |
26 | @Provides // Add SDKOptions(infuraAPIKey="supply_your_key_here") to Ethereum constructor for read-only calls
27 | fun provideEthereum(@ApplicationContext context: Context, dappMetadata: DappMetadata, logger: Logger): Ethereum {
28 | return Ethereum(context, dappMetadata, null, logger)
29 | }
30 |
31 | @Provides
32 | fun provideEthereumFlow(ethereum: Ethereum): EthereumFlow {
33 | return EthereumFlow(ethereum)
34 | }
35 | }
36 |
37 | @Module
38 | @InstallIn(SingletonComponent::class)
39 | abstract class EthereumModule {
40 | @Binds
41 | abstract fun bindEthereum(ethereumflow: EthereumFlow): EthereumFlowWrapper
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/AppTopBar.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp.com.metamask.dapp
2 |
3 | import androidx.navigation.NavController
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.ArrowBack
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.Composable
8 |
9 | @OptIn(ExperimentalMaterial3Api::class)
10 | @Composable
11 | fun AppTopBar(navController: NavController) {
12 | TopAppBar(
13 | title = { Text("") },
14 | navigationIcon = {
15 | IconButton(onClick = { navController.popBackStack() }) {
16 | Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back")
17 | }
18 | }
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/BatchSignMessageScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.text.BasicTextField
6 | import androidx.compose.material3.Surface
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.text.TextStyle
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import androidx.navigation.compose.rememberNavController
17 | import com.metamask.dapp.com.metamask.dapp.AppTopBar
18 | import io.metamask.androidsdk.EthereumState
19 | import io.metamask.androidsdk.Result
20 | import kotlinx.coroutines.launch
21 |
22 | @Composable
23 | fun BatchSignMessageScreen(
24 | navController: NavController,
25 | ethereumState: EthereumState,
26 | batchSign: suspend (messages: List, address: String) -> Result
27 | ) {
28 | var signResult by remember { mutableStateOf("") }
29 | var errorMessage by remember { mutableStateOf(null) }
30 |
31 | val transactionData = "{\"data\":\"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675\",\"from\": \"0x0000000000000000000000000000000000000000\",\"gas\": \"0x76c0\",\"gasPrice\": \"0x9184e72a000\",\"to\": \"0xd46e8dd67c5d32be8058bb8eb970870f07244567\",\"value\": \"0x9184e72a\"}"
32 | val helloWorld = "Hello, world, signing in!"
33 | val byeWorld = "Last message to sign!"
34 | val batchSignMessages: List = listOf(helloWorld, transactionData, byeWorld)
35 | val coroutineScope = rememberCoroutineScope()
36 |
37 | Surface {
38 | AppTopBar(navController)
39 |
40 | Column(
41 | modifier = Modifier
42 | .fillMaxSize()
43 | .padding(horizontal = 36.dp),
44 | horizontalAlignment = Alignment.CenterHorizontally
45 | ) {
46 | Heading("Sign Message")
47 |
48 | Spacer(modifier = Modifier.weight(1f))
49 |
50 | BasicTextField(
51 | value = batchSignMessages.joinToString("\n\n=================================\n\n"),
52 | textStyle = TextStyle(color = if (isSystemInDarkTheme()) { Color.White} else { Color.Black}),
53 | onValueChange = { },
54 | modifier = Modifier.padding(bottom = 36.dp)
55 | )
56 |
57 | DappButton(buttonText = stringResource(R.string.batch_sign)) {
58 | coroutineScope.launch {
59 | when (val result = batchSign(batchSignMessages, ethereumState.selectedAddress)) {
60 | is Result.Success.Items -> {
61 | errorMessage = null
62 | signResult = result.value.joinToString("\n=================================\n")
63 | }
64 | is Result.Error -> {
65 | errorMessage = result.error.message
66 | }
67 | else -> {}
68 | }
69 | }
70 | }
71 |
72 | Spacer(modifier = Modifier.height(8.dp))
73 |
74 | DappLabel(
75 | text = errorMessage ?: signResult,
76 | color = if (errorMessage != null) { Color.Red } else { Color.Unspecified },
77 | modifier = Modifier.padding(bottom = 36.dp)
78 | )
79 |
80 | Spacer(modifier = Modifier.height(24.dp))
81 | }
82 | }
83 | }
84 |
85 | @Preview
86 | @Composable
87 | fun PreviewBatchSignMessage() {
88 | BatchSignMessageScreen(
89 | rememberNavController(),
90 | ethereumState = EthereumState("", "", ""),
91 | batchSign = { _, _ -> Result.Success.Items(listOf()) }
92 | )
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ConnectScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.Surface
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import androidx.compose.ui.unit.dp
12 | import io.metamask.androidsdk.EthereumState
13 | import io.metamask.androidsdk.Result
14 | import kotlinx.coroutines.launch
15 |
16 | @Composable
17 | fun ConnectScreen(
18 | ethereumState: EthereumState,
19 | connect: suspend () -> Result,
20 | connectSign: () -> Unit,
21 | connectWith: () -> Unit,
22 | disconnect: () -> Unit,
23 | clearSession: () -> Unit) {
24 |
25 | val bottomMargin = 24.dp
26 | val connected = ethereumState.selectedAddress.isNotEmpty()
27 |
28 | var errorMessage by remember { mutableStateOf(null) }
29 | val coroutineScope = rememberCoroutineScope()
30 |
31 | Surface {
32 | Column(
33 | modifier = Modifier
34 | .fillMaxSize()
35 | .padding(horizontal = 36.dp),
36 | horizontalAlignment = Alignment.CenterHorizontally
37 | ) {
38 | Heading("MetaMask SDK Dapp")
39 |
40 | Spacer(modifier = Modifier.weight(1f))
41 |
42 | // Connect button
43 | if (connected) {
44 | DappButton(buttonText = stringResource(R.string.disconnect)) {
45 | disconnect()
46 | }
47 | } else {
48 | DappButton(buttonText = stringResource(R.string.connect)) {
49 | coroutineScope.launch {
50 | errorMessage = when(val result = connect()) {
51 | is Result.Error -> {
52 | result.error.message
53 | }
54 | else -> { null }
55 | }
56 | }
57 | }
58 |
59 | Spacer(modifier = Modifier.height(12.dp))
60 |
61 | // Connect and sign button
62 | DappButton(buttonText = stringResource(R.string.connect_sign)) {
63 | connectSign()
64 | }
65 |
66 | Spacer(modifier = Modifier.height(12.dp))
67 |
68 | // Connect with button
69 | DappButton(buttonText = stringResource(R.string.connect_with)) {
70 | connectWith()
71 | }
72 | }
73 |
74 | Spacer(modifier = Modifier.height(4.dp))
75 |
76 | DappLabel(
77 | text = errorMessage ?: ethereumState.selectedAddress,
78 | color = if (errorMessage != null) { Color.Red } else { Color.Unspecified },
79 | modifier = Modifier.padding(bottom = 12.dp)
80 | )
81 |
82 | // Clear session button
83 | DappButton(buttonText = stringResource(R.string.clear_session)) {
84 | clearSession()
85 | errorMessage = null
86 | }
87 |
88 | Spacer(modifier = Modifier.height(4.dp))
89 |
90 | DappLabel(
91 | text = ethereumState.sessionId,
92 | color = Color.Unspecified,
93 | modifier = Modifier.padding(bottom = bottomMargin)
94 | )
95 | }
96 | }
97 | }
98 |
99 | @Preview
100 | @Composable
101 | fun PreviewConnectClearButtons() {
102 | ConnectScreen(
103 | ethereumState = EthereumState("", "", ""),
104 | connect = { -> Result.Success.Item("")},
105 | connectSign = { },
106 | connectWith = { },
107 | disconnect = { },
108 | clearSession = { }
109 | )
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/DappActionsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import androidx.compose.ui.unit.dp
12 | import androidx.navigation.NavController
13 | import androidx.navigation.compose.rememberNavController
14 | import com.metamask.dapp.com.metamask.dapp.AppTopBar
15 | import io.metamask.androidsdk.EthereumState
16 |
17 | @Composable
18 | fun DappActionsScreen(
19 | navController: NavController,
20 | ethereumState: EthereumState,
21 | onSignMessage: () -> Unit,
22 | onChainedSign: () -> Unit,
23 | onSendTransaction: () -> Unit,
24 | onSwitchChain: () -> Unit,
25 | onReadOnlyCalls: () -> Unit
26 | ) {
27 | Surface {
28 | AppTopBar(navController)
29 |
30 | Column(
31 | modifier = Modifier
32 | .fillMaxSize()
33 | .padding(horizontal = 36.dp),
34 | horizontalAlignment = Alignment.CenterHorizontally
35 | ) {
36 | Heading("Dapp Actions")
37 |
38 | DappLabel(
39 | heading = "Account:",
40 | text = ethereumState.selectedAddress,
41 | color = Color.Unspecified,
42 | modifier = Modifier.padding(bottom = 36.dp)
43 | )
44 |
45 | DappLabel(
46 | heading = "ChainId:",
47 | text = ethereumState.chainId,
48 | color = Color.Unspecified,
49 | modifier = Modifier.padding(bottom = 36.dp)
50 | )
51 |
52 | Spacer(modifier = Modifier.weight(1f))
53 |
54 | // Sign message button
55 | DappButton(buttonText = stringResource(R.string.sign)) {
56 | onSignMessage()
57 | }
58 |
59 | Spacer(modifier = Modifier.height(12.dp))
60 |
61 | // Chained signing button
62 | DappButton(buttonText = stringResource(R.string.batch_sign)) {
63 | onChainedSign()
64 | }
65 |
66 | Spacer(modifier = Modifier.height(12.dp))
67 |
68 | // Send transaction button
69 | DappButton(buttonText = stringResource(R.string.send)) {
70 | onSendTransaction()
71 | }
72 |
73 | Spacer(modifier = Modifier.height(12.dp))
74 |
75 | // Switch chain button
76 | DappButton(buttonText = stringResource(R.string.switch_chain)) {
77 | onSwitchChain()
78 | }
79 |
80 | Spacer(modifier = Modifier.height(12.dp))
81 |
82 | // Read only RPC calls
83 | DappButton(buttonText = stringResource(R.string.read_only_calls)) {
84 | onReadOnlyCalls()
85 | }
86 |
87 | Spacer(modifier = Modifier.height(24.dp))
88 | }
89 | }
90 | }
91 |
92 |
93 |
94 | @Preview
95 | @Composable
96 | fun PreviewDappActions() {
97 | DappActionsScreen(rememberNavController(), EthereumState("","",""), {}, {}, {}, {}, {})
98 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/DappButton.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.fillMaxWidth
4 | import androidx.compose.foundation.layout.height
5 | import androidx.compose.material3.Button
6 | import androidx.compose.material3.ButtonDefaults
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.unit.Dp
14 | import androidx.compose.ui.unit.dp
15 | import androidx.compose.ui.unit.sp
16 |
17 | @Composable
18 | fun DappButton(
19 | buttonText: String,
20 | buttonHeight: Dp = 48.dp,
21 | buttonBackgroundColor: Color = Color(40, 124, 204),
22 | buttonTextColor: Color = Color.White,
23 | onClick: () -> Unit
24 | ) {
25 | Button(
26 | onClick = onClick,
27 | elevation = null, colors = ButtonDefaults.buttonColors(
28 | containerColor = buttonBackgroundColor,
29 | contentColor = buttonTextColor
30 | ), modifier = Modifier
31 | .height(buttonHeight)
32 | .fillMaxWidth()
33 | ) {
34 | Text(
35 | text = buttonText,
36 | fontSize = 16.sp,
37 | fontWeight = FontWeight.SemiBold,
38 | color = Color.White,
39 | )
40 | }
41 | }
42 |
43 | @Preview
44 | @Composable
45 | fun PreviewDappButton() {
46 | DappButton(
47 | buttonText = "Connect",
48 | onClick = {}
49 | )
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/DappLabel.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import androidx.compose.ui.unit.TextUnit
10 | import androidx.compose.ui.unit.dp
11 | import androidx.compose.ui.unit.sp
12 |
13 | @Composable
14 | fun DappLabel(
15 | heading: String = "",
16 | text: String,
17 | color: Color = Color.White,
18 | fontSize: TextUnit = 14.sp,
19 | modifier: Modifier = Modifier.padding(bottom = 12.dp)
20 | ) {
21 | if (heading.isNotEmpty()) {
22 | Text(
23 | text = heading,
24 | color = color,
25 | fontSize = 18.sp,
26 | modifier = Modifier.padding(bottom = 5.dp)
27 | )
28 | }
29 | Text(
30 | text = text,
31 | color = color,
32 | fontSize = fontSize,
33 | modifier = modifier
34 | )
35 | }
36 |
37 | @Preview
38 | @Composable
39 | fun PreviewDappLabel() {
40 | DappLabel(text = "Connect")
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/DappScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | enum class DappScreen {
4 | CONNECT,
5 | ACTIONS,
6 | BATCH_SIGN,
7 | SIGN_MESSAGE,
8 | CONNECT_SIGN_MESSAGE,
9 | CONNECT_WITH,
10 | SEND_TRANSACTION,
11 | SWITCH_CHAIN,
12 | READ_ONLY_CALLS
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/EthereumFlowViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import io.metamask.androidsdk.*
6 | import kotlinx.coroutines.flow.Flow
7 | import javax.inject.Inject
8 |
9 | @HiltViewModel
10 | class EthereumFlowViewModel @Inject constructor(
11 | private val ethereum: EthereumFlowWrapper
12 | ): ViewModel() {
13 | val ethereumFlow: Flow get() = ethereum.ethereumState
14 |
15 | suspend fun connect() : Result {
16 | return ethereum.connect()
17 | }
18 |
19 | suspend fun connectWith(request: EthereumRequest) : Result {
20 | return ethereum.connectWith(request)
21 | }
22 |
23 | suspend fun connectSign(message: String) : Result {
24 | return ethereum.connectSign(message)
25 | }
26 |
27 | suspend fun connectWithSendTransaction(amount: String,
28 | from: String,
29 | to: String) : Result {
30 | val params: MutableMap = mutableMapOf(
31 | "from" to from,
32 | "to" to to,
33 | "amount" to amount
34 | )
35 |
36 | val transactionRequest = EthereumRequest(
37 | method = EthereumMethod.ETH_SEND_TRANSACTION.value,
38 | params = listOf(params)
39 | )
40 |
41 | return connectWith(transactionRequest)
42 | }
43 |
44 | suspend fun sendRequestBatch(requests: List) : Result {
45 | return ethereum.sendRequestBatch(requests)
46 | }
47 |
48 | suspend fun sendRequest(request: EthereumRequest) : Result {
49 | return ethereum.sendRequest(request)
50 | }
51 |
52 | suspend fun sendBatchSigningRequest(
53 | messages: List,
54 | address: String) : Result {
55 | val requestBatch: MutableList = mutableListOf()
56 |
57 | for (message in messages) {
58 | val params: List = listOf(address, message)
59 | val ethereumRequest = EthereumRequest(
60 | method = EthereumMethod.PERSONAL_SIGN.value,
61 | params = params
62 | )
63 | requestBatch.add(ethereumRequest)
64 | }
65 |
66 | return ethereum.sendRequestBatch(requestBatch)
67 | }
68 |
69 | suspend fun signMessage(
70 | message: String,
71 | address: String,
72 | ) : Result {
73 | return ethereum.ethSignTypedDataV4(typedData = message, address)
74 | }
75 |
76 | suspend fun getBalance(address: String, block: String = "latest") : Result {
77 | return ethereum.getEthBalance(address, block)
78 | }
79 |
80 | suspend fun gasPrice() : Result {
81 | return ethereum.getEthGasPrice()
82 | }
83 |
84 | suspend fun web3ClientVersion() : Result {
85 | return ethereum.getWeb3ClientVersion()
86 | }
87 |
88 | suspend fun sendTransaction(
89 | amount: String,
90 | from: String,
91 | to: String,
92 | ) : Result {
93 | return ethereum.sendTransaction(from = from, to = to, value = amount)
94 | }
95 |
96 | suspend fun switchChain(chainId: String) : SwitchChainResult {
97 | return when (val result = ethereum.switchEthereumChain(chainId)) {
98 | is Result.Success -> {
99 | SwitchChainResult.Success("Successfully switched to ${Network.chainNameFor(chainId)} ($chainId)")
100 | }
101 | is Result.Error -> {
102 | if (result.error.code == ErrorType.UNRECOGNIZED_CHAIN_ID.code || result.error.code == ErrorType.SERVER_ERROR.code) {
103 | val message = "${Network.chainNameFor(chainId)} ($chainId) has not been added to your MetaMask wallet. Add chain?"
104 | SwitchChainResult.Error(result.error.code, message)
105 | } else {
106 | SwitchChainResult.Error(result.error.code,"Add chain error: ${result.error.message}")
107 | }
108 | }
109 | }
110 | }
111 |
112 | suspend fun addEthereumChain(chainId: String) : SwitchChainResult {
113 | return when (val result = ethereum.addEthereumChain(
114 | chainId = chainId,
115 | chainName = Network.chainNameFor(chainId),
116 | rpcUrls = Network.rpcUrls(Network.fromChainId(chainId)),
117 | iconUrls = listOf(),
118 | blockExplorerUrls = null,
119 | nativeCurrency = NativeCurrency(name = Network.chainNameFor(chainId), symbol = Network.symbol(chainId), decimals = 18)
120 | )) {
121 | is Result.Error -> {
122 | SwitchChainResult.Error(result.error.code,"Add chain error: ${result.error.message}")
123 | }
124 | is Result.Success -> {
125 | if (chainId == ethereum.chainId) {
126 | SwitchChainResult.Success("Successfully switched to ${Network.chainNameFor(chainId)} ($chainId)")
127 | } else {
128 | SwitchChainResult.Success("Successfully added ${Network.chainNameFor(chainId)} ($chainId)")
129 | }
130 | }
131 | }
132 | }
133 |
134 | fun disconnect(clearSession: Boolean = false) {
135 | ethereum.disconnect(clearSession)
136 | }
137 | }
138 |
139 | sealed class SwitchChainResult {
140 | data class Success(val value: String) : SwitchChainResult()
141 | data class Error(val error: Int, val message: String): SwitchChainResult()
142 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/Heading.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.tooling.preview.Preview
9 | import androidx.compose.ui.unit.dp
10 |
11 | @Composable
12 | fun Heading(title: String) {
13 | Text(
14 | text = title,
15 | style = MaterialTheme.typography.headlineSmall,
16 | modifier = Modifier
17 | .padding(16.dp)
18 |
19 | )
20 | }
21 |
22 | @Preview
23 | @Composable
24 | fun PreviewHeading() {
25 | Heading(title = "Connect Dapp")
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.material3.*
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.tooling.preview.Preview
12 | import com.metamask.dapp.ui.theme.MetaMaskAndroidSDKClientTheme
13 | import dagger.hilt.android.AndroidEntryPoint
14 |
15 | @AndroidEntryPoint
16 | class MainActivity : ComponentActivity() {
17 |
18 | private val ethereumViewModel: EthereumFlowViewModel by viewModels()
19 | private val screenViewModel: ScreenViewModel by viewModels()
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 |
24 | setContent {
25 | MetaMaskAndroidSDKClientTheme {
26 | // A surface container using the 'background' color from the theme
27 | Surface(
28 | modifier = Modifier.fillMaxSize(),
29 | color = MaterialTheme.colorScheme.background
30 | ) {
31 | Setup(ethereumViewModel, screenViewModel)
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
38 | @Composable
39 | fun Greeting(name: String) {
40 | Text(text = "Hello $name!")
41 | }
42 |
43 | @Preview(showBackground = true)
44 | @Composable
45 | fun DefaultPreview() {
46 | MetaMaskAndroidSDKClientTheme {
47 | Greeting("Android")
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/MetaMaskDappApplication.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class MetaMaskDappApplication : Application() {}
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ReadOnlyCallsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material3.*
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.Alignment
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import androidx.compose.ui.unit.dp
12 | import androidx.navigation.NavController
13 | import androidx.navigation.compose.rememberNavController
14 | import com.metamask.dapp.com.metamask.dapp.AppTopBar
15 | import io.metamask.androidsdk.EthereumState
16 | import io.metamask.androidsdk.Result
17 | import kotlinx.coroutines.launch
18 |
19 | @Composable
20 | fun ReadOnlyCallsScreen(
21 | navController: NavController,
22 | ethereumState: EthereumState,
23 | getBalance: suspend (address: String) -> Result,
24 | getGasPrice: suspend () -> Result,
25 | getWeb3ClientVersion: suspend () -> Result
26 | ) {
27 | var selectedAddress by remember { mutableStateOf("") }
28 | var balance by remember { mutableStateOf("") }
29 | var gasPrice by remember { mutableStateOf("") }
30 | var web3ClientVersion by remember { mutableStateOf("") }
31 | var getBalanceErrorMessage by remember { mutableStateOf(null) }
32 | var getGasPriceErrorMessage by remember { mutableStateOf(null) }
33 | var getWeb3VersionErrorMessage by remember { mutableStateOf(null) }
34 | val coroutineScope = rememberCoroutineScope()
35 |
36 | LaunchedEffect(ethereumState.selectedAddress) {
37 | selectedAddress = ethereumState.selectedAddress
38 | }
39 |
40 | Surface {
41 | AppTopBar(navController)
42 |
43 | Column(
44 | modifier = Modifier
45 | .fillMaxSize()
46 | .padding(horizontal = 36.dp),
47 | horizontalAlignment = Alignment.CenterHorizontally
48 | ) {
49 | Heading("Read-Only RPCs")
50 |
51 | Spacer(modifier = Modifier.weight(1f))
52 |
53 | // Get balance
54 | DappButton(buttonText = stringResource(R.string.get_balance)) {
55 | coroutineScope.launch {
56 | when(val result = getBalance(selectedAddress)) {
57 | is Result.Success.Item -> {
58 | balance = result.value
59 | getBalanceErrorMessage = null
60 | }
61 | is Result.Error -> {
62 | getBalanceErrorMessage = result.error.message
63 | }
64 | else -> {}
65 | }
66 | }
67 | }
68 |
69 | Spacer(modifier = Modifier.height(8.dp))
70 |
71 | DappLabel(
72 | text = getBalanceErrorMessage ?: balance,
73 | color = if (getBalanceErrorMessage != null) { Color.Red } else { Color.Unspecified },
74 | modifier = Modifier.padding(bottom = 12.dp)
75 | )
76 |
77 | // Get gas price
78 | DappButton(buttonText = stringResource(R.string.get_gas_price)) {
79 | coroutineScope.launch {
80 | when(val result = getGasPrice()) {
81 | is Result.Success.Item -> {
82 | gasPrice = result.value
83 | getGasPriceErrorMessage = null
84 | }
85 | is Result.Error -> {
86 | getGasPriceErrorMessage = result.error.message
87 | }
88 | else -> {}
89 | }
90 | }
91 | }
92 |
93 | Spacer(modifier = Modifier.height(8.dp))
94 |
95 | DappLabel(
96 | text = getGasPriceErrorMessage ?: gasPrice,
97 | color = if (getGasPriceErrorMessage != null) { Color.Red } else { Color.Unspecified },
98 | modifier = Modifier.padding(bottom = 12.dp)
99 | )
100 |
101 | // Get Web3 client version
102 | DappButton(buttonText = stringResource(R.string.get_web3_client_version)) {
103 | coroutineScope.launch {
104 | when(val result = getWeb3ClientVersion()) {
105 | is Result.Success.Item -> {
106 | web3ClientVersion = result.value
107 | getWeb3VersionErrorMessage = null
108 | }
109 | is Result.Error -> {
110 | getWeb3VersionErrorMessage = result.error.message
111 | }
112 | else -> {}
113 | }
114 | }
115 | }
116 |
117 | Spacer(modifier = Modifier.height(8.dp))
118 |
119 | DappLabel(
120 | text = getWeb3VersionErrorMessage ?: web3ClientVersion,
121 | color = if (getWeb3VersionErrorMessage != null) { Color.Red } else { Color.Unspecified },
122 | modifier = Modifier.padding(bottom = 12.dp)
123 | )
124 | }
125 | }
126 | }
127 |
128 | @Preview
129 | @Composable
130 | fun PreviewReadOnlyCallsScreen() {
131 | ReadOnlyCallsScreen(
132 | rememberNavController(),
133 | ethereumState = EthereumState("", "", ""),
134 | getBalance = {_ -> Result.Success.Item("")},
135 | getGasPrice = { -> Result.Success.Item("")},
136 | getWeb3ClientVersion = { -> Result.Success.Item("")}
137 | )
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import dagger.hilt.android.lifecycle.HiltViewModel
7 | import io.metamask.androidsdk.DefaultLogger
8 | import io.metamask.androidsdk.Logger
9 | import javax.inject.Inject
10 |
11 | @HiltViewModel
12 | class ScreenViewModel @Inject constructor(): ViewModel() {
13 | private val _currentScreen = mutableStateOf(DappScreen.CONNECT)
14 | val currentScreen: State = _currentScreen
15 |
16 | fun setScreen(screen: DappScreen) {
17 | _currentScreen.value = screen
18 | DefaultLogger.log("Navigating to $screen")
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/SignMessageScreen.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.text.BasicTextField
6 | import androidx.compose.material3.*
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.text.TextStyle
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import androidx.navigation.compose.rememberNavController
17 | import com.metamask.dapp.com.metamask.dapp.AppTopBar
18 | import io.metamask.androidsdk.EthereumState
19 | import io.metamask.androidsdk.Result
20 | import kotlinx.coroutines.launch
21 |
22 | @Composable
23 | fun SignMessageScreen(
24 | navController: NavController,
25 | ethereumState: EthereumState,
26 | isConnectSign: Boolean = false,
27 | connectSignMessage: suspend (message: String) -> Result,
28 | signMessage: suspend (message: String, address: String) -> Result
29 | ) {
30 | fun signMessage(chainId: String): String {
31 | return if(isConnectSign) {
32 | "He will win who knows when to fight and when not to fight. He will win who knows how to handle both superior and inferior forces."
33 | } else {
34 | "{\"domain\":{\"chainId\":\"$chainId\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Busa!\",\"from\":{\"name\":\"Kinno\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Busa\",\"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[]\"}]}}"
35 | }
36 | }
37 |
38 | var message = signMessage(ethereumState.chainId)
39 | var signResult by remember { mutableStateOf("") }
40 | var errorMessage by remember { mutableStateOf(null) }
41 | val coroutineScope = rememberCoroutineScope()
42 |
43 | LaunchedEffect(ethereumState.chainId) {
44 | message = signMessage(ethereumState.chainId)
45 | }
46 |
47 | Surface {
48 | AppTopBar(navController)
49 |
50 | Column(
51 | modifier = Modifier
52 | .fillMaxSize()
53 | .padding(horizontal = 36.dp),
54 | horizontalAlignment = Alignment.CenterHorizontally
55 | ) {
56 | Heading(if (isConnectSign) { "Connect & Sign Message" } else { "Sign Message"})
57 |
58 | Spacer(modifier = Modifier.weight(1f))
59 |
60 | BasicTextField(
61 | value = message,
62 | textStyle = TextStyle(color = if (isSystemInDarkTheme()) { Color.White} else { Color.Black}),
63 | onValueChange = {
64 | message = it
65 | },
66 | modifier = Modifier.padding(bottom = 36.dp)
67 | )
68 |
69 | if (isConnectSign) {
70 | DappButton(buttonText = stringResource(R.string.connect_sign)) {
71 | coroutineScope.launch {
72 | val result = connectSignMessage(message)
73 | errorMessage = when (result) {
74 | is Result.Success -> {
75 | null
76 | }
77 | is Result.Error -> {
78 | result.error.message
79 | }
80 | }
81 | }
82 | }
83 | } else {
84 | DappButton(buttonText = stringResource(R.string.sign)) {
85 | coroutineScope.launch {
86 | when (val result = signMessage(message, ethereumState.selectedAddress)) {
87 | is Result.Success.Item -> {
88 | errorMessage = null
89 | signResult = result.value
90 | }
91 | is Result.Error -> {
92 | errorMessage = result.error.message
93 | }
94 | else -> {}
95 | }
96 | }
97 | }
98 | }
99 |
100 | Spacer(modifier = Modifier.height(8.dp))
101 |
102 | DappLabel(
103 | text = errorMessage ?: signResult,
104 | color = if (errorMessage != null) { Color.Red } else { Color.Unspecified },
105 | modifier = Modifier.padding(bottom = 36.dp)
106 | )
107 |
108 | Spacer(modifier = Modifier.height(24.dp))
109 | }
110 | }
111 | }
112 |
113 | @Preview
114 | @Composable
115 | fun PreviewSignMessage() {
116 | SignMessageScreen(
117 | rememberNavController(),
118 | ethereumState = EthereumState("", "", ""),
119 | false,
120 | signMessage = { _, _ -> Result.Success.Item("") },
121 | connectSignMessage = { _ -> Result.Success.Item("") }
122 | )
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.ViewCompat
17 | import androidx.core.view.WindowCompat
18 |
19 | private val DarkColorScheme = darkColorScheme(
20 | primary = Purple80,
21 | secondary = PurpleGrey80,
22 | tertiary = Pink80
23 | )
24 |
25 | private val LightColorScheme = lightColorScheme(
26 | primary = Purple40,
27 | secondary = PurpleGrey40,
28 | tertiary = Pink40
29 |
30 | /* Other default colors to override
31 | background = Color(0xFFFFFBFE),
32 | surface = Color(0xFFFFFBFE),
33 | onPrimary = Color.White,
34 | onSecondary = Color.White,
35 | onTertiary = Color.White,
36 | onBackground = Color(0xFF1C1B1F),
37 | onSurface = Color(0xFF1C1B1F),
38 | */
39 | )
40 |
41 | @Composable
42 | fun MetaMaskAndroidSDKClientTheme(
43 | darkTheme: Boolean = isSystemInDarkTheme(),
44 | // Dynamic color is available on Android 12+
45 | dynamicColor: Boolean = true,
46 | content: @Composable () -> Unit
47 | ) {
48 | val colorScheme = when {
49 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
50 | val context = LocalContext.current
51 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
52 | }
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | val context = LocalContext.current
58 | val window = (LocalContext.current as? Activity)?.window
59 | if (!view.isInEditMode) {
60 | SideEffect {
61 | (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
62 | if (context is Activity) {
63 | context.window.statusBarColor = colorScheme.primary.toArgb()
64 | val insetsController = WindowCompat.getInsetsController(context.window, view)
65 | insetsController?.isAppearanceLightStatusBars = darkTheme
66 | }
67 | //ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
68 | }
69 | }
70 |
71 | MaterialTheme(
72 | colorScheme = colorScheme,
73 | typography = Typography,
74 | content = content
75 | )
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/metamask/dapp/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/stackoverflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/drawable/stackoverflow.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MetaMaskAndroidSDKClient
3 | Connection result:
4 | SessionId:
5 | Sign result:
6 | Transaction result:
7 | Second Fragment
8 | Dapp Actions
9 | Connect
10 | Disconnect
11 | Clear Session
12 | Sign Message
13 | Batch Signing
14 | Send Transaction
15 | Connect with Send Transaction
16 | Switch Chain
17 | Connect and Sign
18 | Connect With Request
19 | Add Chain
20 | Get Balance
21 | Get Gas Price
22 | Get Web3 Client Version
23 | Read Only Calls
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/release/app_icon-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/app_icon-playstore.png
--------------------------------------------------------------------------------
/app/src/release/res/drawable/app_icon_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-anydpi-v26/app_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-anydpi-v26/app_icon_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-hdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-hdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-hdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-hdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-mdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-mdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-mdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-mdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xxhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxxhdpi/app_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xxxhdpi/app_icon.png
--------------------------------------------------------------------------------
/app/src/release/res/mipmap-xxxhdpi/app_icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/app/src/release/res/mipmap-xxxhdpi/app_icon_round.png
--------------------------------------------------------------------------------
/app/src/release/res/values/app_icon_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/app/src/test/java/com/metamask/dapp/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.metamask.dapp
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.4.0'
4 | hilt_version = '2.43.2'
5 | }
6 |
7 | dependencies {
8 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
9 | classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
10 | }
11 | }
12 | plugins {
13 | id 'com.android.application' version '7.3.1' apply false
14 | id 'com.android.library' version '7.3.1' apply false
15 | id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
16 | id 'com.google.dagger.hilt.android' version "$hilt_version" apply false
17 | id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' apply false
18 | id 'org.jetbrains.dokka' version '1.9.0'
19 | }
20 |
21 | apply plugin: 'io.github.gradle-nexus.publish-plugin'
22 | apply from: "${rootDir}/scripts/publish-auth.gradle"
--------------------------------------------------------------------------------
/ecies/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/ecies/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'io.metamask.ecies'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 | minSdk 23
12 | targetSdk 33
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_11
26 | targetCompatibility JavaVersion.VERSION_11
27 | }
28 | kotlinOptions {
29 | jvmTarget = '11'
30 | }
31 |
32 | sourceSets {
33 | main {
34 | jniLibs.srcDirs = ['src/main/jniLibs']
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation 'androidx.core:core-ktx:1.9.0'
41 | implementation 'androidx.appcompat:appcompat:1.6.1'
42 | implementation 'com.google.android.material:material:1.9.0'
43 | testImplementation 'junit:junit:4.13.2'
44 | androidTestImplementation 'androidx.test.ext:junit:1.1.5'
45 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
46 | }
47 |
48 | ext {
49 | PUBLISH_GROUP_ID = 'io.metamask.ecies'
50 | PUBLISH_VERSION = '1.0.1'
51 | PUBLISH_ARTIFACT_ID = 'ecies'
52 | }
53 |
54 | apply plugin: 'maven-publish'
55 | apply plugin: 'signing'
56 | apply plugin: 'org.jetbrains.dokka'
57 | apply from: "${rootProject.projectDir}/scripts/ecies-publish-module.gradle"
--------------------------------------------------------------------------------
/ecies/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/ecies/consumer-rules.pro
--------------------------------------------------------------------------------
/ecies/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keepclasseswithmembernames,includedescriptorclasses class * {
24 | native ;
25 | }
--------------------------------------------------------------------------------
/ecies/src/androidTest/java/io/metamask/ecies/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.ecies
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("io.metamask.ecies.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/ecies/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ecies/src/main/java/io/metamask/ecies/Ecies.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.ecies
2 |
3 | import android.util.Log
4 |
5 | public class Ecies {
6 | companion object {
7 | const val TAG = "ECIES"
8 | // Load the Rust library.
9 | init {
10 | try {
11 | System.loadLibrary("ecies")
12 | Log.d(TAG, "Ecies loaded successfully!")
13 | } catch (e: UnsatisfiedLinkError) {
14 | Log.d(TAG, "Ecies could not be loaded: ${e.message}")
15 | }
16 | }
17 |
18 | // Rust FFI function signatures.
19 | @JvmStatic external fun generateSecretKey(): String
20 | @JvmStatic external fun derivePublicKeyFrom(secret: String): String
21 | @JvmStatic external fun encryptMessage(public: String, message: String): String
22 | @JvmStatic external fun decryptMessage(secret: String, message: String): String
23 | }
24 |
25 | // Kotlin functions that wrap Rust FFI functions.
26 | public fun privateKey(): String {
27 | return generateSecretKey()
28 | }
29 |
30 | public fun publicKeyFrom(secretKey: String): String {
31 | return derivePublicKeyFrom(secretKey)
32 | }
33 |
34 | public fun encrypt(publicKey: String, message: String): String {
35 | return encryptMessage(publicKey, message)
36 | }
37 |
38 | public fun decrypt(secretKey: String, message: String): String {
39 | return decryptMessage(secretKey, message)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ecies/src/main/jniLibs/arm64-v8a/libecies.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/ecies/src/main/jniLibs/arm64-v8a/libecies.so
--------------------------------------------------------------------------------
/ecies/src/main/jniLibs/armeabi-v7a/libecies.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/ecies/src/main/jniLibs/armeabi-v7a/libecies.so
--------------------------------------------------------------------------------
/ecies/src/main/jniLibs/x86/libecies.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/ecies/src/main/jniLibs/x86/libecies.so
--------------------------------------------------------------------------------
/ecies/src/main/jniLibs/x86_64/libecies.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/ecies/src/main/jniLibs/x86_64/libecies.so
--------------------------------------------------------------------------------
/ecies/src/test/java/io/metamask/ecies/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.ecies
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. For more details, visit
11 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
12 | # org.gradle.parallel=true
13 | android.disableAutomaticComponentCreation=true
14 | android.nonTransitiveRClass=true
15 | android.useAndroidX=true
16 | kotlin.code.style=official
17 | org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Aug 05 09:12:01 SAST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/metamask-android-sdk/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/metamask-android-sdk/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.0'
5 | id 'kotlin-kapt'
6 | id 'maven-publish'
7 | }
8 |
9 | android {
10 | namespace 'io.metamask.androidsdk'
11 | compileSdk 33
12 |
13 | defaultConfig {
14 | minSdk 23
15 | targetSdk 33
16 |
17 | ext.versionCode = 1
18 | ext.versionName = "0.6.6"
19 |
20 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
21 | consumerProguardFiles 'consumer-rules.pro'
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_11
26 | targetCompatibility JavaVersion.VERSION_11
27 | }
28 | kotlinOptions {
29 | jvmTarget = "11"
30 | }
31 | kapt {
32 | javacOptions {
33 | // Set Java version for KAPT explicitly
34 | option("-source", "11")
35 | option("-target", "11")
36 | }
37 | }
38 | ndkVersion '25.2.9519653'
39 | buildToolsVersion '30.0.3'
40 | }
41 |
42 | kotlin {
43 | jvmToolchain(11)
44 | }
45 |
46 | dependencies {
47 | implementation 'io.metamask.ecies:ecies:1.0.1'
48 | implementation 'com.squareup.okhttp3:okhttp:4.9.2'
49 | implementation 'androidx.security:security-crypto:1.1.0-alpha06'
50 | implementation 'com.google.code.gson:gson:2.9.0'
51 | implementation 'androidx.core:core-ktx:1.9.0'
52 | implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
53 | implementation 'androidx.appcompat:appcompat:1.6.1'
54 | implementation 'com.google.android.material:material:1.9.0'
55 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
56 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
57 |
58 | testImplementation 'junit:junit:4.13.2'
59 | testImplementation 'org.robolectric:robolectric:4.10.3'
60 | testImplementation 'org.mockito:mockito-core:4.0.0'
61 | testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0'
62 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
63 |
64 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
66 | }
67 |
68 | ext {
69 | PUBLISH_GROUP_ID = 'io.metamask.androidsdk'
70 | PUBLISH_VERSION = '0.6.6'
71 | PUBLISH_ARTIFACT_ID = 'metamask-android-sdk'
72 | }
73 |
74 | apply plugin: 'maven-publish'
75 | apply plugin: 'signing'
76 | apply plugin: 'org.jetbrains.dokka'
77 | apply from: "${rootProject.projectDir}/scripts/metamask-android-sdk-publish-module.gradle"
--------------------------------------------------------------------------------
/metamask-android-sdk/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/metamask-android-sdk/consumer-rules.pro
--------------------------------------------------------------------------------
/metamask-android-sdk/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/metamask-android-sdk/src/androidTest/java/io/metamask/androidsdk/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("io.metamask.androidsdk.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl:
--------------------------------------------------------------------------------
1 | // IMessegeService.aidl
2 | package io.metamask.nativesdk;
3 | import io.metamask.nativesdk.IMessegeServiceCallback;
4 |
5 | interface IMessegeService {
6 | void registerCallback(in IMessegeServiceCallback callback);
7 | void sendMessage(inout Bundle message);
8 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl:
--------------------------------------------------------------------------------
1 | // IMessegeServiceCallback.aidl
2 | package io.metamask.nativesdk;
3 |
4 | // Declare any non-default types here with import statements
5 |
6 | interface IMessegeServiceCallback {
7 | void onMessageReceived(inout Bundle response);
8 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Analytics.kt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | package io.metamask.androidsdk
5 |
6 |
7 | enum class Event(val value: String) {
8 | SDK_RPC_REQUEST("sdk_rpc_request"),
9 | SDK_RPC_REQUEST_DONE("sdk_rpc_request_done"),
10 | SDK_CONNECTION_REQUEST_STARTED("sdk_connect_request_started"),
11 | SDK_CONNECTION_ESTABLISHED("sdk_connection_established"),
12 | SDK_CONNECTION_AUTHORIZED("sdk_connection_authorized"),
13 | SDK_CONNECTION_REJECTED("sdk_connection_rejected"),
14 | SDK_CONNECTION_FAILED("sdk_connection_failed"),
15 | SDK_DISCONNECTED("sdk_disconnected")
16 | }
17 |
18 | interface Tracker {
19 | var enableDebug: Boolean
20 | fun trackEvent(event: Event, params: MutableMap)
21 | }
22 |
23 | class Endpoints {
24 | companion object {
25 | private const val BASE_URL = "https://metamask-sdk.api.cx.metamask.io"
26 | const val ANALYTICS = "$BASE_URL/evt"
27 | }
28 | }
29 |
30 | internal class Analytics(override var enableDebug: Boolean = true) : Tracker {
31 |
32 | private val httpClient: HttpClient = HttpClient()
33 |
34 | override fun trackEvent(event: Event, params: MutableMap) {
35 | if (!enableDebug) { return }
36 |
37 | params["event"] = event.value
38 | httpClient.newCall(Endpoints.ANALYTICS, params)
39 | }
40 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/AnyRequest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class AnyRequest(
4 | override var id: String = TimeStampGenerator.timestamp(),
5 | override val method: String,
6 | override val params: Any?
7 | ) : RpcRequest()
8 |
9 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientMessageServiceCallback.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.os.Bundle
4 | import io.metamask.nativesdk.IMessegeServiceCallback
5 |
6 | open class ClientMessageServiceCallback(
7 | var onMessage: ((Bundle) -> Unit)? = null
8 | ) : IMessegeServiceCallback.Stub() {
9 | override fun onMessageReceived(bundle: Bundle) {
10 | onMessage?.invoke(bundle)
11 | }
12 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ClientServiceConnection.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.content.ComponentName
4 | import android.content.ServiceConnection
5 | import android.os.Bundle
6 | import android.os.IBinder
7 | import io.metamask.nativesdk.IMessegeService
8 | import io.metamask.nativesdk.IMessegeServiceCallback
9 |
10 | open class ClientServiceConnection(
11 | var onConnected: (() -> Unit)? = null,
12 | var onDisconnected: ((ComponentName?) -> Unit)? = null,
13 | var onBindingDied: ((ComponentName?) -> Unit)? = null,
14 | var onNullBinding: ((ComponentName?) -> Unit)? = null
15 | ) : ServiceConnection {
16 | private var messageService: IMessegeService? = null
17 |
18 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
19 | messageService = IMessegeService.Stub.asInterface(service)
20 | onConnected?.invoke()
21 | }
22 |
23 | override fun onServiceDisconnected(name: ComponentName?) {
24 | onDisconnected?.invoke(name)
25 | }
26 |
27 | override fun onBindingDied(name: ComponentName?) {
28 | onBindingDied?.invoke(name)
29 | }
30 |
31 | override fun onNullBinding(name: ComponentName?) {
32 | onNullBinding?.invoke(name)
33 | }
34 |
35 | open fun registerCallback(callback: IMessegeServiceCallback) {
36 | messageService?.registerCallback(callback)
37 | }
38 |
39 | open fun sendMessage(bundle: Bundle) {
40 | messageService?.sendMessage(bundle)
41 | }
42 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModule.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.content.Context
4 |
5 | open class CommunicationClientModule(private val context: Context): CommunicationClientModuleInterface {
6 | override fun provideKeyStorage(): SecureStorage {
7 | return KeyStorage(context)
8 | }
9 |
10 | override fun provideSessionManager(keyStorage: SecureStorage): SessionManager {
11 | return SessionManager(keyStorage)
12 | }
13 |
14 | override fun provideKeyExchange(): KeyExchange {
15 | return KeyExchange()
16 | }
17 |
18 | override fun provideLogger(): Logger {
19 | return DefaultLogger
20 | }
21 |
22 | override fun provideTracker(): Tracker {
23 | return Analytics()
24 | }
25 |
26 | override fun provideClientServiceConnection(): ClientServiceConnection {
27 | return ClientServiceConnection()
28 | }
29 |
30 | override fun provideClientMessageServiceCallback(): ClientMessageServiceCallback {
31 | return ClientMessageServiceCallback()
32 | }
33 |
34 | override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient {
35 | val keyStorage = provideKeyStorage()
36 | val sessionManager = provideSessionManager(keyStorage)
37 | val keyExchange = provideKeyExchange()
38 | val serviceConnection = provideClientServiceConnection()
39 | val messageServiceCallback = provideClientMessageServiceCallback()
40 | val logger = provideLogger()
41 | val tracker = provideTracker()
42 |
43 | return CommunicationClient(
44 | context,
45 | callback,
46 | sessionManager,
47 | keyExchange,
48 | serviceConnection,
49 | messageServiceCallback,
50 | tracker,
51 | logger)
52 | }
53 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/CommunicationClientModuleInterface.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | interface CommunicationClientModuleInterface {
4 | fun provideKeyStorage(): SecureStorage
5 | fun provideSessionManager(keyStorage: SecureStorage): SessionManager
6 | fun provideKeyExchange(): KeyExchange
7 | fun provideLogger(): Logger
8 | fun provideTracker(): Tracker
9 | fun provideClientServiceConnection(): ClientServiceConnection
10 | fun provideClientMessageServiceCallback(): ClientMessageServiceCallback
11 | fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient
12 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Constants.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | const val TAG = "MM_ANDROID_SDK"
4 | const val MESSAGE = "message"
5 | const val KEY_EXCHANGE = "key_exchange"
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Crypto.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import io.metamask.ecies.Ecies
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.launch
7 |
8 | class Crypto : Encryption {
9 | private var ecies: Ecies? = null
10 | override var onInitialized: () -> Unit = {}
11 | private val coroutineScope = CoroutineScope(Dispatchers.IO)
12 |
13 | init {
14 | coroutineScope.launch {
15 | ecies = Ecies()
16 | onInitialized()
17 | }
18 | }
19 |
20 | override fun generatePrivateKey(): String {
21 | return ecies?.privateKey() ?: ""
22 | }
23 |
24 | override fun publicKey(privateKey: String): String {
25 | return ecies?.publicKeyFrom(privateKey) ?: ""
26 | }
27 |
28 | override fun encrypt(publicKey: String, message: String): String {
29 | return ecies?.encrypt(publicKey, message) ?: ""
30 | }
31 |
32 | override fun decrypt(privateKey: String, message: String): String {
33 | return ecies?.decrypt(privateKey, message) ?: ""
34 | }
35 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DappMetadata.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 | import kotlinx.serialization.Serializable
3 | import java.net.MalformedURLException
4 | import java.net.URL
5 |
6 | @Serializable
7 | data class DappMetadata(
8 | val name: String,
9 | val url: String,
10 | val iconUrl: String? = null,
11 | val base64Icon: String? = null
12 | ) {
13 | fun hasValidUrl(): Boolean {
14 | return try {
15 | val url = URL(url)
16 | url.protocol != null && url.host != null
17 | } catch (e: MalformedURLException) {
18 | false
19 | }
20 | }
21 |
22 | fun hasValidName(): Boolean {
23 | return name.isNotEmpty()
24 | }
25 |
26 | val validationError: RequestError?
27 | get() {
28 | if (!hasValidUrl()) {
29 | return RequestError(-101, "Please use a valid Dapp url")
30 | }
31 | if (!hasValidName()) {
32 | return RequestError(-102, "Please use a valid Dapp name")
33 | }
34 | return null
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/DeviceInfo.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.os.Build
4 |
5 | object DeviceInfo {
6 | val platformDescription = "${Build.MANUFACTURER} ${Build.MODEL} ${Build.VERSION.RELEASE}"
7 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Encryption.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | interface Encryption {
4 | var onInitialized: () -> Unit
5 | fun generatePrivateKey(): String
6 | fun publicKey(privateKey: String): String
7 | fun encrypt(publicKey: String, message: String): String
8 | fun decrypt(privateKey: String, message: String): String
9 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ErrorType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | enum class ErrorType(val code: Int) {
4 | // Ethereum Provider
5 | USER_REJECTED_REQUEST(4001), // Ethereum Provider User Rejected Request
6 | UNAUTHORISED_REQUEST(4100), // Ethereum Provider User Rejected Request
7 | UNSUPPORTED_METHOD(4200), // Ethereum Provider Unsupported Method
8 | DISCONNECTED(4900), // Ethereum Provider Not Connected
9 | CHAIN_DISCONNECTED(4901), // Ethereum Provider Chain Not Connected
10 | UNRECOGNIZED_CHAIN_ID(4902), // Unrecognized chain ID. Try adding the chain using wallet_ADD_ETHEREUM_CHAIN first
11 |
12 | // Ethereum RPC
13 | INVALID_INPUT(-32000), // JSON RPC 2.0 Server error
14 | TRANSACTION_REJECTED(-32003), // Ethereum JSON RPC Transaction Rejected
15 | INVALID_REQUEST(-32600), // JSON RPC 2.0 Invalid Request
16 | INVALID_METHOD_PARAMETERS(-32602), // JSON RPC 2.0 Invalid Parameters
17 | SERVER_ERROR(-32603), // Could be one of many outcomes
18 | PARSE_ERROR(-32700), // JSON RPC 2.0 Parse error
19 | UNKNOWN_ERROR(-1); // Check RequestError.code instead
20 |
21 | companion object {
22 | fun message(code: Int): String {
23 |
24 | return when(values().firstOrNull { it.code == code }) {
25 | USER_REJECTED_REQUEST -> "User rejected request"
26 | UNAUTHORISED_REQUEST -> "Request not authorised"
27 | UNSUPPORTED_METHOD -> "Ethereum provider unsupported method"
28 | DISCONNECTED -> "Ethereum provider not connected"
29 | CHAIN_DISCONNECTED -> "Ethereum provider chain not connected"
30 | UNRECOGNIZED_CHAIN_ID -> "Unrecognized chain ID. Try adding the chain using ADD_ETHEREUM_CHAIN first"
31 | INVALID_INPUT -> "JSON RPC 2.0 Server error"
32 | TRANSACTION_REJECTED -> "Ethereum transaction rejected"
33 | INVALID_METHOD_PARAMETERS -> "JSON RPC 2.0 invalid parameters"
34 | INVALID_REQUEST -> "Invalid request"
35 | SERVER_ERROR -> "Server error"
36 | PARSE_ERROR -> "Parse error"
37 | else -> "The request failed"
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/EthereumEventCallback.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | interface EthereumEventCallback {
4 | fun updateAccount(account: String)
5 | fun updateChainId(newChainId: String)
6 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/EthereumMethod.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | enum class EthereumMethod(val value: String) {
4 | ETH_SIGN("eth_sign"),
5 | WEB3_SHA("web3_sha3"),
6 | ETH_CALL("eth_call"),
7 | ETH_CHAIN_ID("eth_chainId"),
8 | ETH_GET_CODE("eth_getCode"),
9 | ETH_ACCOUNTS("eth_accounts"),
10 | ETH_GAS_PRICE("eth_gasPrice"),
11 | PERSONAL_SIGN("personal_sign"),
12 | ETH_GET_BALANCE("eth_getBalance"),
13 | WATCH_ASSET("wallet_watchAsset"),
14 | ETH_BLOCK_NUMBER("eth_blockNumber"),
15 | ETH_ESTIMATE_GAS("eth_estimateGas"),
16 | ETH_GET_STORAGE_AT("eth_getStorageAt"),
17 | ETH_SIGN_TYPED_DATA("eth_signTypedData"),
18 | ETH_GET_BLOCK_BY_HASH("eth_getBlockByHash"),
19 | WEB3_CLIENT_VERSION("web3_clientVersion"),
20 | ETH_REQUEST_ACCOUNTS("eth_requestAccounts"),
21 | ETH_SIGN_TRANSACTION("eth_signTransaction"),
22 | ETH_SEND_TRANSACTION("eth_sendTransaction"),
23 | ETH_SIGN_TYPED_DATA_V3("eth_signTypedData_v3"),
24 | ETH_SIGN_TYPED_DATA_V4("eth_signTypedData_v4"),
25 | ADD_ETHEREUM_CHAIN("wallet_addEthereumChain"),
26 | METAMASK_BATCH("metamask_batch"),
27 | METAMASK_OPEN("metamask_open"),
28 | PERSONAL_EC_RECOVER("personal_ecRecover"),
29 | WALLET_REVOKE_PERMISSIONS("wallet_revokePermissions"),
30 | WALLET_REQUEST_PERMISSIONS("wallet_requestPermissions"),
31 | WALLET_GET_PERMISSIONS("wallet_getPermissions"),
32 | METAMASK_CONNECT_WITH("metamask_connectwith"),
33 | METAMASK_CONNECT_SIGN("metamask_connectSign"),
34 | METAMASK_CHAIN_CHANGED("metamask_chainChanged"),
35 | ETH_SEND_RAW_TRANSACTION("eth_sendRawTransaction"),
36 | SWITCH_ETHEREUM_CHAIN("wallet_switchEthereumChain"),
37 | ETH_GET_TRANSACTION_COUNT("eth_getTransactionCount"),
38 | METAMASK_ACCOUNTS_CHANGED("metamask_accountsChanged"),
39 | ETH_GET_TRANSACTION_BY_HASH("eth_getTransactionByHash"),
40 | ETH_GET_TRANSACTION_RECEIPT("eth_getTransactionReceipt"),
41 | GET_METAMASK_PROVIDER_STATE("metamask_getProviderState"),
42 | ETH_GET_BLOCK_TRANSACTION_COUNT_BY_HASH("eth_getBlockTransactionCountByHash"),
43 | ETH_GET_BLOCK_TRANSACTION_COUNT_BY_NUMBER("eth_getBlockTransactionCountByNumber"),
44 | UNKNOWN("unknown");
45 |
46 | companion object {
47 | fun hasMethod(method: String): Boolean {
48 | return enumValues()
49 | .toList()
50 | .map { it.value }
51 | .contains(method)
52 | }
53 |
54 | fun requiresAuthorisation(method: String): Boolean {
55 | val authorisationMethods: List = listOf(
56 | ETH_SIGN, WATCH_ASSET, PERSONAL_SIGN, METAMASK_BATCH, WALLET_GET_PERMISSIONS, WALLET_REVOKE_PERMISSIONS,
57 | ADD_ETHEREUM_CHAIN, SWITCH_ETHEREUM_CHAIN, METAMASK_CONNECT_WITH, WALLET_REQUEST_PERMISSIONS,
58 | ETH_SEND_TRANSACTION, ETH_SIGN_TRANSACTION, ETH_REQUEST_ACCOUNTS, METAMASK_CONNECT_SIGN, PERSONAL_EC_RECOVER,
59 | ETH_SIGN_TYPED_DATA, ETH_SIGN_TYPED_DATA_V3, ETH_SIGN_TYPED_DATA_V4, ETH_ACCOUNTS, METAMASK_OPEN
60 | ).map { it.value }
61 |
62 | return authorisationMethods.contains(method)
63 | }
64 |
65 | fun isReadOnly(method: String): Boolean {
66 | return !requiresAuthorisation(method)
67 | }
68 |
69 | fun isResultMethod(method: String): Boolean {
70 | val resultMethods: List = listOf(
71 | ETH_SIGN, ETH_CHAIN_ID, PERSONAL_SIGN, METAMASK_CONNECT_WITH, WALLET_GET_PERMISSIONS,
72 | ADD_ETHEREUM_CHAIN, SWITCH_ETHEREUM_CHAIN, METAMASK_BATCH, WALLET_REQUEST_PERMISSIONS,
73 | ETH_SIGN_TRANSACTION, ETH_SEND_TRANSACTION, METAMASK_CONNECT_SIGN, WALLET_REVOKE_PERMISSIONS,
74 | WATCH_ASSET, ETH_REQUEST_ACCOUNTS, GET_METAMASK_PROVIDER_STATE,ETH_ACCOUNTS,
75 | ETH_SIGN_TYPED_DATA, ETH_SIGN_TYPED_DATA_V3, ETH_SIGN_TYPED_DATA_V4,
76 | ).map { it.value }
77 | return resultMethods.contains(method)
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/EthereumRequest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class EthereumRequest(
4 | override var id: String = TimeStampGenerator.timestamp(),
5 | override val method: String,
6 | override val params: Any? = null
7 | ) : RpcRequest()
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/EthereumState.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class EthereumState(
4 | val chainId: String,
5 | val sessionId: String,
6 | val selectedAddress: String
7 | )
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/HttpClient.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.util.Log
4 | import okhttp3.*
5 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
6 | import okhttp3.RequestBody.Companion.toRequestBody
7 | import org.json.JSONObject
8 | import java.io.IOException
9 |
10 | internal class HttpClient(private val logger: Logger = DefaultLogger) {
11 | private val client = OkHttpClient()
12 | private var additionalHeaders: Headers = Headers.headersOf("Accept", "application/json", "Content-Type", "application/json")
13 |
14 | fun addHeaders(headers: Map) {
15 | additionalHeaders = additionalHeaders.newBuilder().apply {
16 | headers.forEach { (key, value) ->
17 | add(key, value)
18 | }
19 | }.build()
20 | }
21 |
22 | fun newCall(baseUrl: String, parameters: Map? = null, callback: ((String?, IOException?) -> Unit)? = null) {
23 | val params: Map = parameters ?: mapOf()
24 | val json = JSONObject(params).toString()
25 |
26 | val requestBody = json.toRequestBody("application/json".toMediaTypeOrNull())
27 | val request = Request.Builder()
28 | .url(baseUrl)
29 | .headers(additionalHeaders)
30 | .post(requestBody)
31 | .build()
32 |
33 | client.newCall(request).enqueue(object: Callback {
34 | override fun onFailure(call: Call, e: IOException) {
35 | logger.error("HttpClient: error ${e.message}")
36 | if (callback != null) {
37 | callback(null, e)
38 | }
39 | }
40 |
41 | override fun onResponse(call: Call, response: Response) {
42 | response.use {
43 | // Handle the response asynchronously
44 | if (callback != null) {
45 | callback(it.body?.string(), null)
46 | }
47 | }
48 | }
49 | })
50 | }
51 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/InfuraProvider.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import org.json.JSONObject
4 |
5 | open class InfuraProvider(private val infuraAPIKey: String?, readonlyRPCMap: Map?, private val logger: Logger = DefaultLogger) {
6 | val rpcUrls: Map = when {
7 | readonlyRPCMap != null && infuraAPIKey != null -> {
8 | // Merge infuraReadonlyRPCMap with readonlyRPCMap, overriding infura's keys if they are present in readonlyRPCMap
9 | val mergedMap = infuraReadonlyRPCMap(infuraAPIKey).toMutableMap()
10 | mergedMap.putAll(readonlyRPCMap)
11 | mergedMap
12 | }
13 | readonlyRPCMap != null -> readonlyRPCMap
14 | infuraAPIKey != null -> infuraReadonlyRPCMap(infuraAPIKey)
15 | else -> emptyMap()
16 | }
17 |
18 | fun supportsChain(chainId: String): Boolean {
19 | return !rpcUrls[chainId].isNullOrEmpty()
20 | }
21 |
22 | fun infuraReadonlyRPCMap(infuraAPIKey: String) : Map {
23 | return mapOf(
24 | // ###### Ethereum ######
25 | // Mainnet
26 | "0x1" to "https://mainnet.infura.io/v3/${infuraAPIKey}",
27 |
28 | // Sepolia 11155111
29 | "0x2a" to "https://sepolia.infura.io/v3/${infuraAPIKey}",
30 |
31 | // ###### Linear ######
32 | // Mainnet
33 | "0xe708" to "https://linea-mainnet.infura.io/v3/${infuraAPIKey}",
34 | // Goerli Testnet
35 | "0xe704" to "https://linea-goerli.infura.io/v3/${infuraAPIKey}",
36 |
37 | // ###### Polygon ######
38 | // Mainnet
39 | "0x89" to "https://polygon-mainnet.infura.io/v3/${infuraAPIKey}",
40 | // Mumbai
41 | "0x13881" to "https://polygon-mumbai.infura.io/v3/${infuraAPIKey}",
42 | // ###### Optimism ######
43 | // Mainnet
44 | "0x45" to "https://optimism-mainnet.infura.io/v3/${infuraAPIKey}",
45 | // Goerli
46 | "0x1a4" to "https://optimism-goerli.infura.io/v3/${infuraAPIKey}",
47 | // ###### Arbitrum ######
48 | // Mainnet
49 | "0xa4b1" to "https://arbitrum-mainnet.infura.io/v3/${infuraAPIKey}",
50 | // Goerli
51 | "0x66eed" to "https://arbitrum-goerli.infura.io/v3/${infuraAPIKey}",
52 | // ###### Palm ######
53 | // Mainnet
54 | "0x2a15c308d" to "https://palm-mainnet.infura.io/v3/${infuraAPIKey}",
55 | // Testnet
56 | "0x2a15c3083" to "https://palm-testnet.infura.io/v3/${infuraAPIKey}",
57 | // ###### Avalanche C-Chain ######
58 | // Mainnet
59 | "0xa86a" to "https://avalanche-mainnet.infura.io/v3/${infuraAPIKey}",
60 | // Fuji
61 | "0xa869" to "https://avalanche-fuji.infura.io/v3/${infuraAPIKey}",
62 | // ###### NEAR ######
63 | // // Mainnet
64 | // "0x4e454152" to "https://near-mainnet.infura.io/v3/${infuraAPIKey}",
65 | // // Testnet
66 | // "0x4e454153" to "https://near-testnet.infura.io/v3/${infuraAPIKey}",
67 | // ###### Aurora ######
68 | // Mainnet
69 | "0x4e454152" to "https://aurora-mainnet.infura.io/v3/${infuraAPIKey}",
70 | // Testnet
71 | "0x4e454153" to "https://aurora-testnet.infura.io/v3/${infuraAPIKey}",
72 | // ###### StarkNet ######
73 | // Mainnet
74 | "0x534e5f4d41494e" to "https://starknet-mainnet.infura.io/v3/${infuraAPIKey}",
75 | // Goerli
76 | "0x534e5f474f45524c49" to "https://starknet-goerli.infura.io/v3/${infuraAPIKey}",
77 | // Goerli 2
78 | "0x534e5f474f45524c4932" to "https://starknet-goerli2.infura.io/v3/${infuraAPIKey}",
79 | // ###### Celo ######
80 | // Mainnet
81 | "0xa4ec" to "https://celo-mainnet.infura.io/v3/${infuraAPIKey}",
82 | // Alfajores Testnet
83 | "0xaef3" to "https://celo-alfajores.infura.io/v3/${infuraAPIKey}",
84 | )
85 | }
86 |
87 | open fun makeRequest(request: RpcRequest, chainId: String, dappMetadata: DappMetadata, callback: ((Result) -> Unit)?) {
88 | val httpClient = HttpClient()
89 |
90 | val devicePlatformInfo = DeviceInfo.platformDescription
91 | val headers = mapOf(
92 | "Metamask-Sdk-Info" to "Sdk/Android SdkVersion/${SDKInfo.VERSION} Platform/$devicePlatformInfo dApp/${dappMetadata.url} dAppTitle/${dappMetadata.name}"
93 | )
94 | httpClient.addHeaders(headers)
95 |
96 | val params: MutableMap = mutableMapOf()
97 | params["method"] = request.method
98 | params["jsonrpc"] = "2.0"
99 | params["id"] = request.id
100 | params["params"] = request.params ?: listOf()
101 |
102 | httpClient.newCall("${rpcUrls[chainId]}", parameters = params) { response, ioException ->
103 | if (response != null) {
104 | logger.log("InfuraProvider:: response $response")
105 | try {
106 | val result = JSONObject(response).optString("result") ?: ""
107 | callback?.invoke(Result.Success.Item(result))
108 | } catch (e: Exception) {
109 | logger.error("InfuraProvider:: error: ${e.message}")
110 | callback?.invoke(Result.Error(RequestError(-1, response)))
111 | }
112 | } else if (ioException != null) {
113 | callback?.invoke(Result.Success.Item(ioException.message ?: ""))
114 | }
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchange.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import io.metamask.androidsdk.KeyExchangeMessageType.*
4 |
5 | data class KeyExchangeMessage(
6 | val type: String,
7 | val publicKey: String?
8 | )
9 |
10 | class KeyExchange(private val crypto: Encryption = Crypto(), private val logger: Logger = DefaultLogger) {
11 | companion object {
12 | const val TYPE = "type"
13 | const val PUBLIC_KEY = "public_key"
14 | }
15 |
16 | private var privateKey: String? = null
17 | var publicKey: String? = null
18 | private var theirPublicKey: String? = null
19 | private var isKeysExchanged = false
20 |
21 | init {
22 | reset()
23 | crypto.onInitialized = {
24 | reset()
25 | }
26 | }
27 |
28 | private fun setIsKeysExchanged(newValue: Boolean) {
29 | synchronized(this) {
30 | isKeysExchanged = newValue
31 | }
32 | }
33 |
34 | fun keysExchanged(): Boolean {
35 | synchronized(this) {
36 | return isKeysExchanged
37 | }
38 | }
39 |
40 | fun reset() {
41 | privateKey = crypto.generatePrivateKey()
42 | privateKey?.let {
43 | publicKey = crypto.publicKey(it)
44 | }
45 | setIsKeysExchanged(false)
46 | theirPublicKey = null
47 | }
48 |
49 | fun encrypt(message: String): String {
50 | val key: String = theirPublicKey ?: throw NullPointerException("theirPublicKey is null")
51 | return crypto.encrypt(key, message)
52 | }
53 |
54 | fun decrypt(message: String): String {
55 | val key: String = privateKey ?: throw NullPointerException("privateKey is null")
56 | return crypto.decrypt(key, message)
57 | }
58 |
59 | fun complete() {
60 | logger.log("KeyExchange:: Key exchange complete")
61 | setIsKeysExchanged(true)
62 | }
63 |
64 | fun nextKeyExchangeMessage(current: KeyExchangeMessage): KeyExchangeMessage? {
65 | current.publicKey?.let {
66 | theirPublicKey = it
67 | }
68 |
69 | return when(current.type) {
70 | KEY_HANDSHAKE_START.name -> KeyExchangeMessage(KEY_HANDSHAKE_SYN.name, publicKey)
71 | KEY_HANDSHAKE_SYN.name -> KeyExchangeMessage(KEY_HANDSHAKE_SYNACK.name, publicKey)
72 | KEY_HANDSHAKE_SYNACK.name -> KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, publicKey)
73 | KEY_HANDSHAKE_ACK.name -> null
74 | else -> null
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyExchangeMessageType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | enum class KeyExchangeMessageType {
8 | @SerialName("none")
9 | NONE,
10 | @SerialName("key_handshake_start")
11 | KEY_HANDSHAKE_START,
12 | @SerialName("key_handshake_check")
13 | KEY_HANDSHAKE_CHECK,
14 | @SerialName("key_exchange_SYN")
15 | KEY_HANDSHAKE_SYN,
16 | @SerialName("key_exchange_SYNACK")
17 | KEY_HANDSHAKE_SYNACK,
18 | @SerialName("key_exchange_ACK")
19 | KEY_HANDSHAKE_ACK
20 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/KeyStorage.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.content.Context
4 | import android.security.keystore.KeyGenParameterSpec
5 | import android.security.keystore.KeyProperties
6 | import android.util.Base64
7 | import android.util.Base64.decode
8 | import android.util.Base64.encodeToString
9 | import kotlinx.coroutines.*
10 | import java.security.KeyStore
11 | import javax.crypto.KeyGenerator
12 | import javax.crypto.SecretKey
13 |
14 | class KeyStorage(private val context: Context, private val logger: Logger = DefaultLogger): SecureStorage {
15 |
16 | private val keyStoreAlias = context.packageName
17 | private val androidKeyStore = "AndroidKeyStore"
18 |
19 | private lateinit var keyStore: KeyStore
20 | private lateinit var secretKey: SecretKey
21 | private val coroutineScope = CoroutineScope(Dispatchers.IO)
22 |
23 | init {
24 | loadSecretKey()
25 | }
26 |
27 | private val secretKeyEntry: KeyStore.SecretKeyEntry? get() {
28 | return try {
29 | keyStore.getEntry(keyStoreAlias, null) as? KeyStore.SecretKeyEntry
30 | } catch(e: Exception) {
31 | logger.error("KeyStorage: ${e.message}")
32 | null
33 | }
34 | }
35 |
36 | private fun encodedValue(value: String): String {
37 | val bytes = (value + keyStoreAlias).toByteArray()
38 | val base64 = encodeToString(bytes, Base64.DEFAULT)
39 | return base64.replace('/', '_').replace('=', '-').lowercase()
40 | }
41 |
42 | override fun loadSecretKey() {
43 | keyStore = KeyStore.getInstance(androidKeyStore)
44 | keyStore.load(null)
45 | secretKey = secretKeyEntry?.secretKey ?: generateSecretKey()
46 | }
47 |
48 | private fun generateSecretKey(): SecretKey {
49 | val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, androidKeyStore)
50 | val keyGenParameterSpec = KeyGenParameterSpec.Builder(
51 | keyStoreAlias,
52 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
53 | )
54 | .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
55 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
56 | .build()
57 |
58 | keyGenerator.init(keyGenParameterSpec)
59 | return keyGenerator.generateKey()
60 | }
61 |
62 | override fun clear(file: String) {
63 | val encodedFileName = encodedValue(file)
64 |
65 | coroutineScope.launch {
66 | context.getSharedPreferences(
67 | encodedFileName,
68 | Context.MODE_PRIVATE)
69 | .edit()
70 | .clear()
71 | .apply()
72 | }
73 | }
74 |
75 | override fun clearValue(key: String, file: String) {
76 | val encodedKey = encodedValue(key)
77 | val encodedFileName = encodedValue(file)
78 |
79 | coroutineScope.launch {
80 | context.getSharedPreferences(
81 | encodedFileName,
82 | Context.MODE_PRIVATE)
83 | .edit()
84 | .putString(encodedKey, null)
85 | .apply()
86 | }
87 | }
88 |
89 | override fun putValue(value: String, key: String, file: String) {
90 | val encodedKey = encodedValue(key)
91 | val encodedFileName = encodedValue(file)
92 |
93 | val bytes = value.toByteArray()
94 | val base64 = encodeToString(bytes, Base64.DEFAULT)
95 |
96 | coroutineScope.launch {
97 | context.getSharedPreferences(
98 | encodedFileName,
99 | Context.MODE_PRIVATE)
100 | .edit()
101 | .putString(encodedKey, base64)
102 | .apply()
103 | }
104 | }
105 |
106 | override suspend fun getValue(key: String, file: String): String? {
107 | val encodedKey = encodedValue(key)
108 | val encodedFileName = encodedValue(file)
109 |
110 | val base64 = context.getSharedPreferences(
111 | encodedFileName,
112 | Context.MODE_PRIVATE)
113 | .getString(encodedKey, null)
114 |
115 | if (base64 != null) {
116 | val bytes = decode(base64, Base64.DEFAULT)
117 | return bytes.toString(Charsets.UTF_8)
118 | }
119 |
120 | return null
121 | }
122 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Logger.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.util.Log
4 |
5 | interface Logger {
6 | fun log(message: String)
7 | fun error(message: String)
8 | }
9 |
10 | object DefaultLogger : Logger {
11 | override fun log(message: String) {
12 | Log.d(TAG, message)
13 | }
14 |
15 | override fun error(message: String) {
16 | Log.e(TAG, message)
17 | }
18 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.os.*
4 | import androidx.appcompat.app.AppCompatActivity
5 |
6 | class MainActivity : AppCompatActivity() {
7 | override fun onCreate(savedInstanceState: Bundle?) {
8 | super.onCreate(savedInstanceState)
9 | setContentView(R.layout.activity_main)
10 | }
11 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Message.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class Message(val id: String, val message: String)
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/MessageType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | enum class MessageType(val value: String) {
4 | ID("id"),
5 | TYPE("type"),
6 | DATA("data"),
7 | ERROR("error"),
8 | READY("ready"),
9 | KEYS_EXCHANGED("keys_exchanged"),
10 | TERMINATE("terminate"),
11 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/NativeCurrency.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class NativeCurrency(
4 | val name: String?,
5 | val symbol: String,
6 | val decimals: Int
7 | )
8 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Network.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | enum class Network(val chainId: String) {
4 | ETHEREUM("0x1"),
5 | LINEAR("0xe708"),
6 | POLYGON("0x89"),
7 | AVALANCHE("0xa86a"),
8 | FANTOM_OPERA("0xfa"),
9 | BNB_SMART_CHAIN("0x38"),
10 | GOERLI("0x5"),
11 | KOVAN("0x2a");
12 |
13 | companion object {
14 | fun name(network: Network?): String {
15 | return when(network) {
16 | ETHEREUM -> "Ethereum"
17 | LINEAR -> "Linear"
18 | POLYGON -> "Polygon"
19 | AVALANCHE -> "Avalanche"
20 | FANTOM_OPERA -> "Fantom Opera"
21 | BNB_SMART_CHAIN -> "BNB Smart Chain"
22 | GOERLI -> "Goerli Testnet"
23 | KOVAN -> "Kovan Testnet"
24 | null -> {
25 | ""
26 | }
27 | }
28 | }
29 |
30 | fun symbol(chainId: String): String {
31 | val network = enumValues()
32 | .toList()
33 | .firstOrNull { it.chainId == chainId }
34 |
35 | return when(network) {
36 | ETHEREUM -> "ETH"
37 | LINEAR -> "LINA"
38 | POLYGON -> "MATIC"
39 | AVALANCHE -> "AVAX"
40 | FANTOM_OPERA -> "FTM"
41 | BNB_SMART_CHAIN -> "BSC"
42 | GOERLI -> "GETH"
43 | KOVAN -> "ETH"
44 | null -> {
45 | ""
46 | }
47 | }
48 | }
49 |
50 | fun fromChainId(chainId: String): Network? {
51 | for (network in values()) {
52 | if (network.chainId == chainId) {
53 | return network
54 | }
55 | }
56 | return null
57 | }
58 |
59 | fun rpcUrls(network: Network?): List {
60 | return when(network) {
61 | POLYGON -> listOf("https://polygon-rpc.com")
62 | FANTOM_OPERA -> listOf("https://rpc.ftm.tools/")
63 | AVALANCHE -> listOf("https://api.avax.network/ext/bc/C/rpc")
64 | BNB_SMART_CHAIN -> listOf("https://bsc-dataseed1.binance.org")
65 | else -> {
66 | listOf()
67 | }
68 | }
69 | }
70 |
71 | fun chainNameFor(chainId: String): String {
72 | val network = enumValues()
73 | .toList()
74 | .firstOrNull { it.chainId == chainId }
75 |
76 | return name(network)
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/OriginatorInfo.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 | import kotlinx.serialization.Serializable
3 |
4 | @Serializable
5 | data class OriginatorInfo(
6 | val title: String?,
7 | val url: String?,
8 | val icon: String?,
9 | val dappId: String?,
10 | val platform: String,
11 | val apiVersion: String
12 | )
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/ReadOnlyRPCProvider.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import org.json.JSONObject
4 |
5 | open class ReadOnlyRPCProvider(private val infuraAPIKey: String?, private val readonlyRPCMap: Map?, private val logger: Logger = DefaultLogger) {
6 | val rpcUrls: Map = when {
7 | readonlyRPCMap != null && infuraAPIKey != null -> {
8 | // Merge infuraReadonlyRPCMap with readonlyRPCMap, overriding infura's keys if they are present in readonlyRPCMap
9 | val mergedMap = infuraReadonlyRPCMap(infuraAPIKey).toMutableMap()
10 | mergedMap.putAll(readonlyRPCMap)
11 | mergedMap
12 | }
13 | readonlyRPCMap != null -> readonlyRPCMap
14 | infuraAPIKey != null -> infuraReadonlyRPCMap(infuraAPIKey)
15 | else -> emptyMap()
16 | }
17 |
18 | fun supportsChain(chainId: String): Boolean {
19 | val apiKey = infuraAPIKey ?: ""
20 | val readonlyMap = readonlyRPCMap ?: mapOf()
21 | return !rpcUrls[chainId].isNullOrEmpty() && (readonlyMap.containsKey(chainId) || apiKey.isNotEmpty())
22 | }
23 |
24 | fun infuraReadonlyRPCMap(infuraAPIKey: String) : Map {
25 | return mapOf(
26 | // ###### Ethereum ######
27 | // Mainnet
28 | "0x1" to "https://mainnet.infura.io/v3/${infuraAPIKey}",
29 |
30 | // Sepolia 11155111
31 | "0x2a" to "https://sepolia.infura.io/v3/${infuraAPIKey}",
32 |
33 | // ###### Linear ######
34 | // Mainnet
35 | "0xe708" to "https://linea-mainnet.infura.io/v3/${infuraAPIKey}",
36 | // Goerli Testnet
37 | "0xe704" to "https://linea-goerli.infura.io/v3/${infuraAPIKey}",
38 |
39 | // ###### Polygon ######
40 | // Mainnet
41 | "0x89" to "https://polygon-mainnet.infura.io/v3/${infuraAPIKey}",
42 | // Mumbai
43 | "0x13881" to "https://polygon-mumbai.infura.io/v3/${infuraAPIKey}",
44 | // ###### Optimism ######
45 | // Mainnet
46 | "0x45" to "https://optimism-mainnet.infura.io/v3/${infuraAPIKey}",
47 | // Goerli
48 | "0x1a4" to "https://optimism-goerli.infura.io/v3/${infuraAPIKey}",
49 | // ###### Arbitrum ######
50 | // Mainnet
51 | "0xa4b1" to "https://arbitrum-mainnet.infura.io/v3/${infuraAPIKey}",
52 | // Goerli
53 | "0x66eed" to "https://arbitrum-goerli.infura.io/v3/${infuraAPIKey}",
54 | // ###### Palm ######
55 | // Mainnet
56 | "0x2a15c308d" to "https://palm-mainnet.infura.io/v3/${infuraAPIKey}",
57 | // Testnet
58 | "0x2a15c3083" to "https://palm-testnet.infura.io/v3/${infuraAPIKey}",
59 | // ###### Avalanche C-Chain ######
60 | // Mainnet
61 | "0xa86a" to "https://avalanche-mainnet.infura.io/v3/${infuraAPIKey}",
62 | // Fuji
63 | "0xa869" to "https://avalanche-fuji.infura.io/v3/${infuraAPIKey}",
64 | // ###### NEAR ######
65 | // // Mainnet
66 | // "0x4e454152" to "https://near-mainnet.infura.io/v3/${infuraAPIKey}",
67 | // // Testnet
68 | // "0x4e454153" to "https://near-testnet.infura.io/v3/${infuraAPIKey}",
69 | // ###### Aurora ######
70 | // Mainnet
71 | "0x4e454152" to "https://aurora-mainnet.infura.io/v3/${infuraAPIKey}",
72 | // Testnet
73 | "0x4e454153" to "https://aurora-testnet.infura.io/v3/${infuraAPIKey}",
74 | // ###### StarkNet ######
75 | // Mainnet
76 | "0x534e5f4d41494e" to "https://starknet-mainnet.infura.io/v3/${infuraAPIKey}",
77 | // Goerli
78 | "0x534e5f474f45524c49" to "https://starknet-goerli.infura.io/v3/${infuraAPIKey}",
79 | // Goerli 2
80 | "0x534e5f474f45524c4932" to "https://starknet-goerli2.infura.io/v3/${infuraAPIKey}",
81 | // ###### Celo ######
82 | // Mainnet
83 | "0xa4ec" to "https://celo-mainnet.infura.io/v3/${infuraAPIKey}",
84 | // Alfajores Testnet
85 | "0xaef3" to "https://celo-alfajores.infura.io/v3/${infuraAPIKey}",
86 | )
87 | }
88 |
89 | open fun makeRequest(request: RpcRequest, chainId: String, dappMetadata: DappMetadata, callback: ((Result) -> Unit)?) {
90 | val httpClient = HttpClient()
91 |
92 | val devicePlatformInfo = DeviceInfo.platformDescription
93 | val headers = mapOf(
94 | "Metamask-Sdk-Info" to "Sdk/Android SdkVersion/${SDKInfo.VERSION} Platform/$devicePlatformInfo dApp/${dappMetadata.url} dAppTitle/${dappMetadata.name}"
95 | )
96 | httpClient.addHeaders(headers)
97 |
98 | val params: MutableMap = mutableMapOf()
99 | params["method"] = request.method
100 | params["jsonrpc"] = "2.0"
101 | params["id"] = request.id
102 | params["params"] = request.params ?: listOf()
103 |
104 | val endpoint = rpcUrls[chainId]
105 | if (endpoint == null) {
106 | callback?.invoke(Result.Error(RequestError(-1, "There is no defined network for chainId $chainId, please provide it via readonlyRPCMap")))
107 | return
108 | }
109 |
110 | httpClient.newCall(endpoint, parameters = params) { response, ioException ->
111 | if (response != null) {
112 | logger.log("InfuraProvider:: response $response")
113 | try {
114 | val result = JSONObject(response).optString("result") ?: ""
115 | callback?.invoke(Result.Success.Item(result))
116 | } catch (e: Exception) {
117 | logger.error("InfuraProvider:: error: ${e.message}")
118 | callback?.invoke(Result.Error(RequestError(-1, response)))
119 | }
120 | } else if (ioException != null) {
121 | callback?.invoke(Result.Success.Item(ioException.message ?: ""))
122 | }
123 | }
124 | }
125 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/RequestError.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RequestError(
7 | val code: Int,
8 | val message: String)
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/RequestInfo.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RequestInfo(
7 | val type: String,
8 | val originatorInfo: OriginatorInfo
9 | )
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/Result.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | sealed class Result {
4 | sealed class Success: Result() {
5 | data class Item(val value: String): Success()
6 | data class Items(val value: List): Success()
7 | data class ItemMap(val value: Map): Success()
8 | }
9 |
10 | data class Error(val error: RequestError): Result()
11 | }
12 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/RpcRequest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | sealed class RpcRequest {
4 | abstract val id: String
5 | abstract val method: String
6 | abstract val params: Any?
7 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SDKInfo.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | object SDKInfo {
4 | const val VERSION = "0.6.6"
5 | const val PLATFORM = "android"
6 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SDKOptions.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class SDKOptions(
4 | val infuraAPIKey: String?,
5 | var readonlyRPCMap: Map?
6 | )
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SecureStorage.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | interface SecureStorage {
4 | fun loadSecretKey()
5 | fun clear(file: String)
6 | fun clearValue(key: String, file: String)
7 | fun putValue(value: String, key: String, file: String)
8 | suspend fun getValue(key: String, file: String): String?
9 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SessionConfig.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class SessionConfig(
4 | val sessionId: String,
5 | val expiryDate: Long
6 | ) {
7 | fun isValid(): Boolean {
8 | return System.currentTimeMillis() < expiryDate
9 | }
10 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SessionManager.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.reflect.TypeToken
5 | import kotlinx.coroutines.*
6 | import java.lang.reflect.Type
7 |
8 | class SessionManager(
9 | private val store: SecureStorage,
10 | var sessionDuration: Long = 30 * 24 * 3600, // 30 days default
11 | private val logger: Logger = DefaultLogger
12 | ) {
13 | var sessionId: String = ""
14 |
15 | var onInitialized: () -> Unit = {}
16 | private val coroutineScope = CoroutineScope(Dispatchers.IO)
17 |
18 | companion object {
19 | const val SESSION_CONFIG_KEY = "SESSION_CONFIG_KEY"
20 | const val SESSION_CONFIG_FILE = "SESSION_CONFIG_FILE"
21 | const val SESSION_ACCOUNT_KEY = "SESSION_ACCOUNT_KEY"
22 | const val SESSION_CHAIN_ID_KEY = "SESSION_CHAIN_ID_KEY"
23 | const val DEFAULT_SESSION_DURATION: Long = 30 * 24 * 3600 // 30 days default
24 | }
25 |
26 | init {
27 | coroutineScope.launch {
28 | val id = getSessionConfig().sessionId
29 | sessionId = id
30 | onInitialized()
31 | }
32 | }
33 |
34 | fun updateSessionDuration(duration: Long) {
35 | logger.log("SessionManager:: Session duration extended by: ${duration/3600.0/24.0} days")
36 | coroutineScope.launch {
37 | sessionDuration = duration
38 | val sessionId = getSessionConfig().sessionId
39 | val expiryDate = System.currentTimeMillis() + sessionDuration * 1000
40 | val sessionConfig = SessionConfig(sessionId, expiryDate)
41 | saveSessionConfig(sessionConfig)
42 | }
43 | }
44 |
45 | suspend fun getSessionConfig(reset: Boolean = false): SessionConfig {
46 | if (reset) {
47 | store.clearValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
48 | return makeNewSessionConfig()
49 | }
50 |
51 | val sessionConfigJson = store.getValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
52 | ?: return makeNewSessionConfig()
53 |
54 | val type: Type = object : TypeToken() {}.type
55 |
56 | return try {
57 | val sessionConfig: SessionConfig = Gson().fromJson(sessionConfigJson, type)
58 |
59 | if (sessionConfig.isValid()) {
60 | SessionConfig(sessionConfig.sessionId, System.currentTimeMillis() + sessionDuration * 1000)
61 | } else {
62 | makeNewSessionConfig()
63 | }
64 | } catch(e: Exception) {
65 | logger.error("SessionManager: ${e.message}")
66 | makeNewSessionConfig()
67 | }
68 | }
69 |
70 | fun saveSessionConfig(sessionConfig: SessionConfig) {
71 | val sessionConfigJson = Gson().toJson(sessionConfig)
72 | store.putValue(sessionConfigJson, SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
73 | }
74 |
75 | fun clearSession(onComplete: () -> Unit) {
76 | coroutineScope.launch {
77 | store.clearValue(SESSION_CONFIG_KEY, SESSION_CONFIG_FILE)
78 | makeNewSessionConfig()
79 | sessionId = getSessionConfig().sessionId
80 | onComplete()
81 | }
82 | }
83 |
84 | fun makeNewSessionConfig(): SessionConfig {
85 | store.clear(SESSION_CONFIG_FILE)
86 | val sessionId = TimeStampGenerator.timestamp()
87 | val expiryDate = System.currentTimeMillis() + sessionDuration * 1000
88 | val sessionConfig = SessionConfig(sessionId, expiryDate)
89 | saveSessionConfig(sessionConfig)
90 | return sessionConfig
91 | }
92 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/SubmittedRequest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | data class
4 |
5 | SubmittedRequest(
6 | val request: RpcRequest,
7 | val callback: (Result) -> Unit
8 | )
9 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/java/io/metamask/androidsdk/TimeStampGenerator.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | object TimeStampGenerator {
4 | fun timestamp(): String {
5 | return System.currentTimeMillis().toString()
6 | }
7 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/CryptoTests.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import org.junit.Assert
4 | import org.junit.Before
5 | import org.junit.Test
6 |
7 | class CryptoTests {
8 | private lateinit var crypto: Encryption
9 | private lateinit var privateKey: String
10 | private lateinit var publicKey: String
11 |
12 | @Before
13 | fun setup() {
14 | crypto = MockCrypto()
15 | privateKey = crypto.generatePrivateKey()
16 | publicKey = crypto.publicKey(privateKey)
17 | }
18 |
19 | @Test
20 | fun testPrivateKeyIsNotNullOrEmpty() {
21 | assert(!privateKey.isNullOrEmpty())
22 | }
23 |
24 | @Test
25 | fun testPublicKeyIsNotNullOrEmpty() {
26 | assert(!publicKey.isNullOrEmpty())
27 | }
28 |
29 | @Test
30 | fun testEncryptDecrypt() {
31 | val plainText = "Text 2 encrypt!"
32 | val encryptedText = crypto.encrypt(publicKey, plainText)
33 | val decrypted = crypto.decrypt(privateKey, encryptedText)
34 |
35 | Assert.assertEquals(decrypted, plainText)
36 | }
37 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/KeyExchangeTests.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import org.junit.Before
4 | import org.junit.Test
5 | import org.junit.Assert.*
6 |
7 | import io.metamask.androidsdk.KeyExchangeMessageType.*
8 |
9 | class KeyExchangeTests {
10 | lateinit var keyExchange: KeyExchange
11 |
12 | @Before
13 | fun setup() {
14 | keyExchange = KeyExchange(MockCrypto())
15 | }
16 |
17 | @Test
18 | fun testStartNextKeyExchangeStepIsSYN() {
19 | val startKeyExchange = KeyExchangeMessage(KEY_HANDSHAKE_START.name, keyExchange.publicKey)
20 | val nextKeyExchangeMessage = keyExchange.nextKeyExchangeMessage(startKeyExchange)
21 | assertEquals(nextKeyExchangeMessage?.type, KEY_HANDSHAKE_SYN.name)
22 | }
23 |
24 | @Test
25 | fun testSYNNextKeyExchangeStepIsSYN_ACK() {
26 | val startKeyExchange = KeyExchangeMessage(KEY_HANDSHAKE_SYN.name, keyExchange.publicKey)
27 | val nextKeyExchangeMessage = keyExchange.nextKeyExchangeMessage(startKeyExchange)
28 | assertEquals(nextKeyExchangeMessage?.type, KEY_HANDSHAKE_SYNACK.name)
29 | }
30 |
31 | @Test
32 | fun testSYN_ACKNextKeyExchangeStepIsACK() {
33 | val startKeyExchange = KeyExchangeMessage(KEY_HANDSHAKE_SYNACK.name, keyExchange.publicKey)
34 | val nextKeyExchangeMessage = keyExchange.nextKeyExchangeMessage(startKeyExchange)
35 | assertEquals(nextKeyExchangeMessage?.type, KEY_HANDSHAKE_ACK.name)
36 | }
37 |
38 | @Test
39 | fun testACKNextKeyExchangeStepIsNull() {
40 | val startKeyExchange = KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, keyExchange.publicKey)
41 | val nextKeyExchangeMessage = keyExchange.nextKeyExchangeMessage(startKeyExchange)
42 | assertEquals(nextKeyExchangeMessage?.type, null)
43 | }
44 |
45 | @Test
46 | fun testUndefinedStepNextKeyExchangeStepIsNull() {
47 | val startKeyExchange = KeyExchangeMessage("KEY_HANDSHAKE_RANDOM", keyExchange.publicKey)
48 | val nextKeyExchangeMessage = keyExchange.nextKeyExchangeMessage(startKeyExchange)
49 | assertEquals(nextKeyExchangeMessage?.type, null)
50 | }
51 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/KeyStorageTests.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.coroutines.delay
4 | import kotlinx.coroutines.runBlocking
5 | import org.junit.Assert
6 | import org.junit.Before
7 | import org.junit.Test
8 |
9 | class KeyStorageTests {
10 | private lateinit var keyStorage: SecureStorage
11 | private val testFile = "testFile"
12 |
13 |
14 | @Before
15 | fun setUp() {
16 | keyStorage = MockKeyStorage()
17 | keyStorage.clear(testFile)
18 | }
19 |
20 | @Test
21 | fun testClearValue() = runBlocking {
22 | val testValue = "testValue"
23 | val testKey = "testKey"
24 |
25 | putValue(testValue, testKey)
26 | clearValue(testKey)
27 | delay(1000)
28 |
29 | val result = getValue(testKey)
30 | Assert.assertEquals(null, result)
31 | }
32 |
33 | @Test
34 | fun testClearAll() = runBlocking {
35 | val testValue1 = "testValue1"
36 | val testKey1 = "testKey1"
37 |
38 | val testValue2 = "testValue2"
39 | val testKey2 = "testKey2"
40 |
41 | putValue(testValue1, testKey1)
42 | putValue(testValue2, testKey2)
43 |
44 | clearAll()
45 | delay(1000)
46 |
47 | val result1 = getValue(testKey1)
48 | val result2 = getValue(testKey2)
49 | Assert.assertEquals(null, result1)
50 | Assert.assertEquals(null, result2)
51 | }
52 |
53 | @Test
54 | fun testPutValue() = runBlocking {
55 | val testValue = "testValue"
56 | val testKey = "testKey"
57 |
58 | putValue(testValue, testKey)
59 | delay(1000)
60 |
61 | val result = getValue(testKey)
62 | Assert.assertNotNull(result)
63 | }
64 |
65 | @Test
66 | fun testGetValue() = runBlocking {
67 | val testValue = "testValue"
68 | val testKey = "testKey"
69 |
70 | putValue(testValue, testKey)
71 | delay(1000)
72 |
73 | val result = getValue(testKey)
74 | Assert.assertEquals(testValue, result)
75 | }
76 |
77 | @Test
78 | fun testGetValueNotFound() = runBlocking {
79 | val testKey = "testRandomKey"
80 |
81 | val result = getValue(testKey)
82 | Assert.assertEquals(null, result)
83 | }
84 |
85 | // Helper methods
86 |
87 | private fun clearAll() {
88 | keyStorage.clear(testFile)
89 | }
90 |
91 | private fun clearValue(key: String) {
92 | keyStorage.clearValue(key, testFile)
93 | }
94 |
95 | private fun putValue(value: String, key: String) {
96 | keyStorage.putValue(value, key, testFile)
97 | }
98 |
99 | private suspend fun getValue(key: String): String? {
100 | return keyStorage.getValue(key, testFile)
101 | }
102 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientMessageServiceCallback.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.os.Bundle
4 |
5 | class MockClientMessageServiceCallback: ClientMessageServiceCallback() {
6 | var messageReceived = false
7 | var receivedMessage: Bundle? = null
8 |
9 | override fun onMessageReceived(bundle: Bundle) {
10 | super.onMessageReceived(bundle)
11 | messageReceived = true
12 | receivedMessage = bundle
13 | }
14 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockClientServiceConnection.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.content.ComponentName
4 | import android.os.Bundle
5 | import android.os.IBinder
6 | import io.metamask.nativesdk.IMessegeService
7 | import io.metamask.nativesdk.IMessegeServiceCallback
8 |
9 | class MockClientServiceConnection: ClientServiceConnection() {
10 | var serviceConectionCalled = false
11 | var serviceDisconnectionCalled = false
12 | var registerCallbackCalled = false
13 | var sendMessageCalled = false
14 | var sentMessage: Bundle? = null
15 |
16 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
17 | super.onServiceConnected(name, service)
18 | serviceConectionCalled = true
19 | }
20 |
21 | override fun onServiceDisconnected(name: ComponentName?) {
22 | onDisconnected?.invoke(name)
23 | serviceDisconnectionCalled = true
24 | }
25 |
26 | override fun onBindingDied(name: ComponentName?) {
27 | onBindingDied?.invoke(name)
28 | }
29 |
30 | override fun onNullBinding(name: ComponentName?) {
31 | onNullBinding?.invoke(name)
32 | }
33 |
34 | override fun registerCallback(callback: IMessegeServiceCallback) {
35 | super.registerCallback(callback)
36 | registerCallbackCalled = true
37 | }
38 |
39 | override fun sendMessage(bundle: Bundle) {
40 | super.sendMessage(bundle)
41 | sendMessageCalled = true
42 | sentMessage = bundle
43 | }
44 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCommunicationClientModule.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.content.Context
4 | import io.metamask.androidsdk.MockTracker
5 |
6 | class MockCommunicationClientModule(
7 | private val context: Context,
8 | private val keyStorage: SecureStorage,
9 | private val sessionManager: SessionManager,
10 | private val keyExchange: KeyExchange,
11 | private val serviceConnection: ClientServiceConnection,
12 | private val clientMessageServiceCallback: ClientMessageServiceCallback,
13 | private val tracker: Tracker,
14 | private val logger: Logger): CommunicationClientModuleInterface {
15 |
16 | override fun provideKeyStorage(): SecureStorage = keyStorage
17 | override fun provideSessionManager(keyStorage: SecureStorage): SessionManager = sessionManager
18 | override fun provideKeyExchange(): KeyExchange = keyExchange
19 | override fun provideLogger(): Logger = logger
20 | override fun provideTracker(): Tracker = tracker
21 |
22 | override fun provideClientServiceConnection(): ClientServiceConnection = serviceConnection
23 | override fun provideClientMessageServiceCallback(): ClientMessageServiceCallback = clientMessageServiceCallback
24 | override fun provideCommunicationClient(callback: EthereumEventCallback?): CommunicationClient = CommunicationClient(
25 | context,
26 | callback,
27 | sessionManager,
28 | keyExchange,
29 | serviceConnection,
30 | clientMessageServiceCallback,
31 | tracker,
32 | logger)
33 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockCrypto.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.launch
6 |
7 | class MockCrypto() : Encryption {
8 | private val rsaEncryption: RSAEncryption = RSAEncryption()
9 | override var onInitialized: () -> Unit = {}
10 |
11 | init {
12 | onInitialized()
13 | }
14 |
15 | override fun generatePrivateKey(): String {
16 | return rsaEncryption.generatePrivateKey()
17 | }
18 |
19 | override fun publicKey(privateKey: String): String {
20 | return rsaEncryption.publicKey(privateKey)
21 | }
22 |
23 | override fun encrypt(publicKey: String, message: String): String {
24 | return rsaEncryption.encrypt(publicKey, message)
25 | }
26 |
27 | override fun decrypt(privateKey: String, message: String): String {
28 | return rsaEncryption.decrypt(privateKey, message)
29 | }
30 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockEthereumEventCallback.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | class MockEthereumEventCallback : EthereumEventCallback {
4 | var account: String = ""
5 | var chainId: String = ""
6 |
7 | override fun updateAccount(account: String) {
8 | this.account = account
9 | }
10 |
11 | override fun updateChainId(newChainId: String) {
12 | this.chainId = newChainId
13 | }
14 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockKeyStorage.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import android.security.keystore.KeyGenParameterSpec
4 | import android.security.keystore.KeyProperties
5 | import javax.crypto.KeyGenerator
6 | import javax.crypto.SecretKey
7 |
8 | class MockKeyStorage : SecureStorage {
9 | val keyStoreAlias = "testStore"
10 | private val keyMap: MutableMap = mutableMapOf()
11 | private val sharedPreferencesMap: MutableMap> = mutableMapOf()
12 |
13 | fun isClear(): Boolean {
14 | return sharedPreferencesMap[SessionManager.SESSION_CONFIG_FILE].isNullOrEmpty()
15 | }
16 |
17 | override fun loadSecretKey() {
18 | val keyStoreAlias = "keyStoreAlias"
19 | keyMap[keyStoreAlias] = generateMockSecretKey()
20 | }
21 |
22 | override fun clear(file: String) {
23 | sharedPreferencesMap.remove(file)
24 | }
25 |
26 | override fun clearValue(key: String, file: String) {
27 | sharedPreferencesMap[file]?.remove(key)
28 | }
29 |
30 | override fun putValue(value: String, key: String, file: String) {
31 | val fileMap = sharedPreferencesMap.getOrPut(file) { mutableMapOf() }
32 | fileMap[key] = value
33 | sharedPreferencesMap[file] = fileMap
34 | }
35 |
36 | override suspend fun getValue(key: String, file: String): String? {
37 | val fileMap = sharedPreferencesMap[file]
38 | return fileMap?.get(key)
39 | }
40 |
41 | private fun generateMockSecretKey(): SecretKey {
42 | // Mock implementation: Generate a SecretKey for testing
43 | val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
44 | keyGenerator.init(
45 | KeyGenParameterSpec.Builder(
46 | keyStoreAlias,
47 | KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
48 | )
49 | .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
50 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
51 | .build()
52 | )
53 | return keyGenerator.generateKey()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockReadOnlyRPCProvider.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | class MockReadOnlyRPCProvider(private val infuraAPIKey: String, private val readOnlyRPCMap: Map?, private val logger: Logger = TestLogger) : ReadOnlyRPCProvider(infuraAPIKey, readOnlyRPCMap, logger) {
4 | var mockResponse: String? = null // This can hold the mock response for the request.
5 |
6 | override fun makeRequest(request: RpcRequest, chainId: String, dappMetadata: DappMetadata, callback: ((Result) -> Unit)?) {
7 | if (mockResponse != null) {
8 | callback?.invoke(Result.Success.Item(mockResponse!!))
9 | } else {
10 | callback?.invoke(Result.Error(RequestError(-1, "No response set in MockInfuraProvider")))
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/MockTracker.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | class MockTracker(override var enableDebug: Boolean = true) : Tracker {
4 |
5 | var trackedEvent: Event? = null
6 | var trackedEventParams: MutableMap? = null
7 |
8 | override fun trackEvent(event: Event, params: MutableMap) {
9 | if (!enableDebug) { return }
10 |
11 | params["event"] = event.value
12 | trackedEvent = event
13 | trackedEventParams = params
14 | }
15 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/RSAEncryption.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import java.security.KeyFactory
4 | import java.security.KeyPairGenerator
5 | import java.security.KeyPair
6 | import java.security.PrivateKey
7 | import java.security.PublicKey
8 | import java.security.spec.PKCS8EncodedKeySpec
9 | import java.security.spec.X509EncodedKeySpec
10 | import java.util.Base64
11 | import javax.crypto.Cipher
12 |
13 | class RSAEncryption : Encryption {
14 |
15 | override var onInitialized: () -> Unit = {}
16 | private val keyPair: KeyPair
17 |
18 | init {
19 | val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
20 | keyPairGenerator.initialize(2048)
21 | keyPair = keyPairGenerator.generateKeyPair()
22 | }
23 |
24 | override fun generatePrivateKey(): String {
25 | val privateKey = keyPair.private
26 | return Base64.getEncoder().encodeToString(privateKey.encoded)
27 | }
28 |
29 | override fun publicKey(privateKey: String): String {
30 | val publicKey = keyPair.public
31 | return Base64.getEncoder().encodeToString(publicKey.encoded)
32 | }
33 |
34 | override fun encrypt(publicKey: String, message: String): String {
35 | val keyFactory = KeyFactory.getInstance("RSA")
36 | val publicKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))
37 | val publicK = keyFactory.generatePublic(publicKeySpec)
38 |
39 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
40 | cipher.init(Cipher.ENCRYPT_MODE, publicK)
41 |
42 | val encryptedBytes = cipher.doFinal(message.toByteArray())
43 | return Base64.getEncoder().encodeToString(encryptedBytes)
44 | }
45 |
46 | override fun decrypt(privateKey: String, message: String): String {
47 | val keyFactory = KeyFactory.getInstance("RSA")
48 | val privateKeySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))
49 | val privateK = keyFactory.generatePrivate(privateKeySpec)
50 |
51 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
52 | cipher.init(Cipher.DECRYPT_MODE, privateK)
53 |
54 | val decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(message))
55 | return String(decryptedBytes)
56 | }
57 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/SessionConfigTests.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import org.junit.Test
4 |
5 | class SessionConfigTests {
6 | companion object {
7 | const val SESSION_ID = "TEST_SESSION_ID"
8 | }
9 |
10 | @Test
11 | fun testSessionConfigIsValid() {
12 | val sessionDuration: Long = 7 * 24 * 3600 // 7 days
13 | val expiryDate = System.currentTimeMillis() + sessionDuration * 1000
14 | val sessionConfig = SessionConfig(SESSION_ID, expiryDate)
15 | assert(sessionConfig.isValid())
16 | }
17 |
18 | @Test
19 | fun testSessionConfigIsNotValid() {
20 | val sessionDuration: Long = 60 // 1 minute
21 | val expiryDate = System.currentTimeMillis() - sessionDuration * 1000 // 1 minute ago
22 | val sessionConfig = SessionConfig(SESSION_ID, expiryDate)
23 | assert(!sessionConfig.isValid())
24 | }
25 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/SessionManagerTests.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | import kotlinx.coroutines.ExperimentalCoroutinesApi
4 | import kotlinx.coroutines.delay
5 | import kotlinx.coroutines.runBlocking
6 | import kotlinx.coroutines.test.advanceUntilIdle
7 | import kotlinx.coroutines.test.runTest
8 | import org.junit.Assert.assertEquals
9 | import org.junit.Assert.assertNotEquals
10 | import org.junit.Assert.assertNotNull
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Before
13 | import org.junit.Test
14 |
15 | class SessionManagerTests {
16 | private val sessionConfigFile: String = "SESSION_CONFIG_FILE"
17 | private val sessionConfigKey: String = "SESSION_CONFIG_KEY"
18 |
19 | private lateinit var keyStorage: SecureStorage
20 | private lateinit var sessionManager: SessionManager
21 | @Before
22 | fun setUp() {
23 | keyStorage = MockKeyStorage()
24 | keyStorage.clearValue(key = sessionConfigKey, file = sessionConfigFile)
25 | keyStorage.clear(sessionConfigFile)
26 | sessionManager = SessionManager(store = keyStorage, logger = TestLogger)
27 | sessionManager.clearSession{}
28 | }
29 |
30 | @OptIn(ExperimentalCoroutinesApi::class)
31 | @Test
32 | fun testInitLoadsSessionConfig() = runTest {
33 | assertNotNull(sessionManager.sessionId)
34 | }
35 | @OptIn(ExperimentalCoroutinesApi::class)
36 | @Test
37 | fun testDefaultSessionDuration() = runTest {
38 | val sessionConfig = sessionManager.getSessionConfig()
39 | val defaultDuration = 30 * 24 * 3600L // 30 days
40 | assertEquals(sessionConfig.expiryDate/1000, System.currentTimeMillis()/1000 + defaultDuration)
41 | }
42 | @OptIn(ExperimentalCoroutinesApi::class)
43 | @Test
44 | fun testUpdateSessionDuration() = runTest {
45 | val newDuration: Long = 14 * 24 * 3600
46 | sessionManager.updateSessionDuration(newDuration)
47 | advanceUntilIdle()
48 | val sessionConfig = sessionManager.getSessionConfig()
49 | assertEquals(sessionConfig.expiryDate/1000, System.currentTimeMillis()/1000 + newDuration)
50 | }
51 | @OptIn(ExperimentalCoroutinesApi::class)
52 | @Test
53 | fun testSessionConfigIsValid() = runTest {
54 | val sessionConfig = sessionManager.getSessionConfig()
55 | assertTrue(sessionConfig.isValid())
56 | }
57 | @OptIn(ExperimentalCoroutinesApi::class)
58 | @Test
59 | fun testSessionConfigReset() = runBlocking {
60 | val initialSessionConfig = sessionManager.getSessionConfig()
61 | delay(1000)
62 | val resetSessionConfig = sessionManager.getSessionConfig(reset = true)
63 |
64 | assertNotEquals(initialSessionConfig.sessionId, resetSessionConfig.sessionId)
65 | }
66 | @OptIn(ExperimentalCoroutinesApi::class)
67 | @Test
68 | fun testSaveSessionConfig() = runTest {
69 | val sessionConfig = SessionConfig("test_session", System.currentTimeMillis() + 1000L)
70 | sessionManager.saveSessionConfig(sessionConfig)
71 |
72 | val savedConfig = sessionManager.getSessionConfig()
73 | assertEquals(sessionConfig.sessionId, savedConfig.sessionId)
74 | }
75 | @OptIn(ExperimentalCoroutinesApi::class)
76 | @Test
77 | fun testClearSession() = runTest {
78 | sessionManager.clearSession { }
79 | advanceUntilIdle()
80 |
81 | val sessionConfig = sessionManager.getSessionConfig()
82 | assertNotEquals("", sessionManager.sessionId)
83 | assertNotEquals("", sessionConfig.sessionId)
84 | }
85 | @OptIn(ExperimentalCoroutinesApi::class)
86 | @Test
87 | fun testMakeNewSessionConfig() = runTest {
88 | val newConfig = sessionManager.makeNewSessionConfig()
89 |
90 | assertTrue(newConfig.isValid())
91 | assertNotEquals("", newConfig.sessionId)
92 | }
93 | }
--------------------------------------------------------------------------------
/metamask-android-sdk/src/test/java/io/metamask/androidsdk/TestLogger.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.androidsdk
2 |
3 | object TestLogger : Logger {
4 | override fun log(message: String) {
5 | // No-op
6 | }
7 |
8 | override fun error(message: String) {
9 | // No-op
10 | }
11 | }
--------------------------------------------------------------------------------
/nativesdk/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/nativesdk/README.md:
--------------------------------------------------------------------------------
1 | # MetaMask Android SDK Native Module
2 |
3 | ## Overview
4 | The MetaMask Android SDK Native Module is a Kotlin [Android Native Module](https://reactnative.dev/docs/native-modules-android) embedded in the MetaMask wallet that acts as the server-side of the [Inter Process Communication (IPC)](https://developer.android.com/guide/components/processes-and-threads#IPC) between the MetaMask SDK in the Android dapps and the MetaMask wallet. The IPC mechanism is implemented using the [Android Interface Definition Language (AIDL)](https://developer.android.com/guide/components/aidl).
5 |
6 | This module handles encrypted communication between the dapp and MetaMask and then relays the messages over to the Android SDK communication layer implemented in React Native in the wallet. The wallet calls the Native Module via NativeModules - an API that enables react-native code to call native Kotlin primitives.
7 |
8 | ## Compiling
9 | To compile the nativesdk as a `.aar` file, you need to modify the project's `settings.gradle` file to have the nativesdk as a standalone module to include to the project:
10 | ```
11 | include ':app'
12 | include ':nativesdk'
13 | include ':metamask-android-sdk'
14 | ```
15 | Then run
16 | ```
17 | ./gradlew assembleDebug
18 | ```
19 | The output file will be located in `nativesdk/build/outputs/aar`. This nativesdk library is then embedded in the MetaMask mobile in the path `android/libs/nativesdk.aar`. It becomes the server-side component of the AIDL communication mechanism described below in the Architecture section.
20 |
21 | ## Architecture
22 | The client SDK communicates with the server SDK (Android Native Module) via IPC implemented using AIDL.
23 |
24 | ### Dapp <-> MetaMask Inter-app Communication
25 | This happens over AIDL IPC. The communication over the IPC protocol is entirely encrypted using Elliptic Curve Integrated Encryption (ECIES). We cross-compiled the rust version of the [ecies.org](https://ecies.org/) implementation. More information on the mechanics of this encryption can be found in the
26 | [Official MetaMask SDK documentation ](https://docs.metamask.io/wallet/concepts/sdk/#communication-layer).
27 |
28 | ### Native Module <-> React Communication
29 | Within MetaMask, the communication from the native module to React Native happens via message broadcast, while that from React Native to the native module happens via `NativeModules` - a React Native API that enables react native code to call mobile native primitives (Kotlin).
30 |
31 | The architectural diagram of the communication is illustrated below
32 |
33 | 
34 |
--------------------------------------------------------------------------------
/nativesdk/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("org.jetbrains.kotlin.android")
4 | }
5 |
6 | android {
7 | namespace = "io.metamask.nativesdk"
8 | compileSdk = 33
9 |
10 | defaultConfig {
11 | minSdk = 23
12 | targetSdk = 33
13 |
14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles("consumer-rules.pro")
16 | }
17 |
18 | buildTypes {
19 | release {
20 | isMinifyEnabled = false
21 | proguardFiles(
22 | getDefaultProguardFile("proguard-android-optimize.txt"),
23 | "proguard-rules.pro"
24 | )
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility = JavaVersion.VERSION_1_8
29 | targetCompatibility = JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = "1.8"
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | compileOnly(files("libs/ecies.aar"))
39 | implementation("com.facebook.react:react-android:0.73.3")
40 | implementation("androidx.core:core-ktx:1.7.0")
41 | implementation ("com.squareup.okhttp3:okhttp:4.9.2")
42 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
43 | implementation("androidx.appcompat:appcompat:1.6.1")
44 | implementation("com.google.android.material:material:1.9.0")
45 | testImplementation("junit:junit:4.13.2")
46 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
47 | }
--------------------------------------------------------------------------------
/nativesdk/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/nativesdk/consumer-rules.pro
--------------------------------------------------------------------------------
/nativesdk/illustrations/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/nativesdk/illustrations/architecture.png
--------------------------------------------------------------------------------
/nativesdk/libs/ecies.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MetaMask/metamask-android-sdk/a448378fbedc3afbf70759ba71294f7819af2f37/nativesdk/libs/ecies.aar
--------------------------------------------------------------------------------
/nativesdk/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/nativesdk/src/androidTest/java/io/metamask/nativesdk/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("io.metamask.nativesdk.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/nativesdk/src/main/aidl/io/metamask/nativesdk/IMessegeService.aidl:
--------------------------------------------------------------------------------
1 | // IMessegeService.aidl
2 | package io.metamask.nativesdk;
3 | import io.metamask.nativesdk.IMessegeServiceCallback;
4 |
5 | // Declare any non-default types here with import statements
6 |
7 | interface IMessegeService {
8 | void registerCallback(in IMessegeServiceCallback callback);
9 | void sendMessage(inout Bundle message);
10 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/aidl/io/metamask/nativesdk/IMessegeServiceCallback.aidl:
--------------------------------------------------------------------------------
1 | // IMessegeServiceCallback.aidl
2 | package io.metamask.nativesdk;
3 |
4 | // Declare any non-default types here with import statements
5 |
6 | interface IMessegeServiceCallback {
7 | void onMessageReceived(inout Bundle response);
8 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Analytics.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | // Making Analytics static as CommunicationClient cannot be instantiated
4 | class Analytics {
5 | companion object {
6 | var enableDebug: Boolean = true
7 | private val httpClient: HttpClient = HttpClient()
8 |
9 | fun trackEvent(event: Event, params: MutableMap) {
10 | if (!enableDebug) { return }
11 |
12 | Logger.log("Analytics: ${event.value}")
13 |
14 | params["event"] = event.value
15 | httpClient.newCall(Endpoints.ANALYTICS, params)
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/ConnectionStatusManager.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import java.lang.ref.WeakReference
4 |
5 | class ConnectionStatusManager private constructor() {
6 | private var connectionStatusCallbackRef: WeakReference? = null
7 |
8 | companion object {
9 | private var instance: ConnectionStatusManager? = null
10 |
11 | fun getInstance(): ConnectionStatusManager {
12 | if (instance == null) {
13 | instance = ConnectionStatusManager()
14 | }
15 | return instance as ConnectionStatusManager
16 | }
17 | }
18 |
19 | fun setCallback(callback: MetaMaskConnectionStatusCallback) {
20 | Logger.log("ConnectionStatusManager:: Added connection status callback $callback")
21 | connectionStatusCallbackRef = WeakReference(callback)
22 | }
23 |
24 | fun onMetaMaskConnect() {
25 | Logger.log("ConnectionStatusManager:: metamask connected")
26 | connectionStatusCallbackRef?.get()?.onMetaMaskConnect()
27 | }
28 |
29 | fun onMetaMaskReady() {
30 | Logger.log("ConnectionStatusManager:: metamask bound")
31 | connectionStatusCallbackRef?.get()?.onMetaMaskReady()
32 | }
33 |
34 | fun onMetaMaskDisconnect() {
35 | Logger.log("ConnectionStatusManager:: metamask disconnected")
36 | connectionStatusCallbackRef?.get()?.onMetaMaskDisconnect()
37 | }
38 |
39 | fun onMetaMaskBroadcastRegistered() {
40 | Logger.log("ConnectionStatusManager:: metamask broadcast registered")
41 | connectionStatusCallbackRef?.get()?.onMetaMaskBroadcastRegistered()
42 | }
43 |
44 | fun onMetaMaskBroadcastUnregistered() {
45 | Logger.log("ConnectionStatusManager:: metamask broadcast unregistered")
46 | connectionStatusCallbackRef?.get()?.onMetaMaskBroadcastUnregistered()
47 | }
48 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Constants.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | const val TAG = "NATIVE_SDK"
4 | const val MESSAGE = "message"
5 | const val KEY_EXCHANGE = "key_exchange"
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Crypto.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import com.crypto.ecies.Ecies
4 |
5 | class Crypto {
6 | private val ecies = Ecies()
7 |
8 | fun generatePrivateKey(): String {
9 | return ecies.privateKey()
10 | }
11 |
12 | fun publicKey(privateKey: String): String {
13 | return ecies.publicKeyFrom(privateKey)
14 | }
15 |
16 | fun encrypt(publicKey: String, message: String): String {
17 | return ecies.encrypt(publicKey, message)
18 | }
19 |
20 | fun decrypt(privateKey: String, message: String): String {
21 | return ecies.decrypt(privateKey, message)
22 | }
23 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Endpoint.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | class Endpoints {
4 | companion object {
5 | const val BASE_URL = "https://metamask-sdk-socket.metafi.codefi.network"
6 | const val ANALYTICS = "$BASE_URL/debug"
7 | }
8 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Event.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | enum class Event(val value: String) {
4 | SDK_CONNECTION_REQUEST_STARTED("sdk_connect_request_started"),
5 | SDK_CONNECTION_ESTABLISHED("sdk_connection_established"),
6 | SDK_DISCONNECTED("sdk_disconnected")
7 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/EventType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | enum class EventType(val value: String) {
4 | KEYS_EXCHANGED("keys_exchanged"),
5 | CLIENTS_CONNECTED("clients_connected"),
6 | CLIENTS_DISCONNECTED("clients_disconnected"),
7 | MESSAGE("message"),
8 | TERMINATE("terminate"),
9 | BIND("bind")
10 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/HttpClient.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import android.util.Log
4 | import okhttp3.*
5 | import java.io.IOException
6 |
7 | class HttpClient {
8 | private val client = OkHttpClient()
9 |
10 | fun newCall(baseUrl: String, parameters: Map? = null) {
11 | val request: Request
12 |
13 | if (parameters != null) {
14 | val requestBodyBuilder = FormBody.Builder()
15 |
16 | for ((key, value) in parameters) {
17 | requestBodyBuilder.add(key, value)
18 | }
19 |
20 | val requestBody: RequestBody = requestBodyBuilder.build()
21 | request = Request.Builder()
22 | .url(baseUrl)
23 | .post(requestBody)
24 | .build()
25 | } else {
26 | request = Request.Builder()
27 | .url(baseUrl)
28 | .build()
29 | }
30 |
31 | client.newCall(request).enqueue(object: Callback {
32 | override fun onFailure(call: Call, e: IOException) {
33 | Log.e(TAG,"HttpClient: ${e.message}")
34 | }
35 |
36 | override fun onResponse(call: Call, response: Response) {
37 | Log.d(TAG,"HttpClient: ${response}")
38 | }
39 | })
40 | }
41 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/KeyExchange.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import io.metamask.nativesdk.KeyExchangeMessageType.*
4 |
5 | data class KeyExchangeMessage(
6 | val type: String,
7 | val publicKey: String?
8 | )
9 |
10 | class KeyExchange {
11 | var step: String = NONE.name
12 | private val crypto: Crypto = Crypto()
13 |
14 | private lateinit var privateKey: String
15 | lateinit var publicKey: String
16 | private var theirPublicKey: String? = null
17 | private var isKeysExchanged = false
18 |
19 | init {
20 | resetKeys()
21 | }
22 |
23 | companion object {
24 | const val TYPE = "type"
25 | const val PUBLIC_KEY = "public_key"
26 |
27 | private var instance: KeyExchange? = null
28 |
29 | fun getInstance(): KeyExchange {
30 | if (instance == null) {
31 | instance = KeyExchange()
32 | }
33 | return instance as KeyExchange
34 | }
35 | }
36 |
37 | private fun setIsKeysExchanged(newValue: Boolean) {
38 | synchronized(this) {
39 | isKeysExchanged = newValue
40 | }
41 | }
42 |
43 | fun keysExchanged(): Boolean {
44 | synchronized(this) {
45 | return isKeysExchanged
46 | }
47 | }
48 |
49 | fun resetKeys() {
50 | privateKey = crypto.generatePrivateKey()
51 | publicKey = crypto.publicKey(privateKey)
52 | setIsKeysExchanged(false)
53 | theirPublicKey = null
54 | }
55 |
56 | fun encrypt(message: String): String {
57 | val key: String = theirPublicKey ?: throw NullPointerException("theirPublicKey is null")
58 | return crypto.encrypt(key, message)
59 | }
60 |
61 | fun decrypt(message: String): String {
62 | val key: String = privateKey
63 | return crypto.decrypt(key, message)
64 | }
65 |
66 | fun complete() {
67 | setIsKeysExchanged(true)
68 | }
69 |
70 | fun nextKeyExchangeMessage(current: KeyExchangeMessage): KeyExchangeMessage? {
71 | current.publicKey?.let {
72 | theirPublicKey = it
73 | }
74 |
75 | step = current.type
76 |
77 | val nextStep = when(current.type) {
78 | KEY_HANDSHAKE_START.name -> KeyExchangeMessage(KEY_HANDSHAKE_SYN.name, publicKey)
79 | KEY_HANDSHAKE_SYN.name -> KeyExchangeMessage(KEY_HANDSHAKE_SYNACK.name, publicKey)
80 | KEY_HANDSHAKE_SYNACK.name -> KeyExchangeMessage(KEY_HANDSHAKE_ACK.name, publicKey)
81 | KEY_HANDSHAKE_ACK.name -> null
82 | else -> null
83 | }
84 |
85 | step = nextStep?.type ?: NONE.name
86 | return nextStep
87 | }
88 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/KeyExchangeMessageType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | enum class KeyExchangeMessageType {
8 | @SerialName("none")
9 | NONE,
10 | @SerialName("key_handshake_start")
11 | KEY_HANDSHAKE_START,
12 | @SerialName("key_handshake_check")
13 | KEY_HANDSHAKE_CHECK,
14 | @SerialName("key_exchange_SYN")
15 | KEY_HANDSHAKE_SYN,
16 | @SerialName("key_exchange_SYNACK")
17 | KEY_HANDSHAKE_SYNACK,
18 | @SerialName("key_exchange_ACK")
19 | KEY_HANDSHAKE_ACK
20 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/Logger.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import android.util.Log
4 |
5 | class Logger {
6 | companion object {
7 | fun log(msg: String): Int {
8 | return Log.d(TAG, msg)
9 | }
10 |
11 | fun error(e: String): Int {
12 | return Log.e(TAG, e)
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/MessageType.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | enum class MessageType {
4 | CREATE_CHANNEL,
5 | CONNECT_TO_CHANNEL,
6 | SEND_MESSAGE,
7 | GET_KEY_INFO,
8 | PAUSE,
9 | RESET_KEYS,
10 | PING,
11 | KEY_CHECK,
12 | IS_CONNECTED,
13 | RESUME,
14 | DISCONNECT
15 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/MetaMaskConnectionStatusCallback.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | interface MetaMaskConnectionStatusCallback {
4 | fun onMetaMaskReady()
5 | fun onMetaMaskConnect()
6 | fun onMetaMaskDisconnect()
7 | fun onMetaMaskBroadcastRegistered()
8 | fun onMetaMaskBroadcastUnregistered()
9 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/NativeSDKPackage.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import android.view.View
4 | import com.facebook.react.ReactPackage
5 | import com.facebook.react.bridge.NativeModule
6 | import com.facebook.react.bridge.ReactApplicationContext
7 | import com.facebook.react.uimanager.ReactShadowNode
8 | import com.facebook.react.uimanager.ViewManager
9 |
10 | class NativeSDKPackage : ReactPackage {
11 | override fun createViewManagers(
12 | reactContext: ReactApplicationContext
13 | ): MutableList>> = mutableListOf()
14 |
15 | override fun createNativeModules(
16 | reactContext: ReactApplicationContext
17 | ): MutableList = listOf(CommunicationClient(reactContext)).toMutableList()
18 | }
--------------------------------------------------------------------------------
/nativesdk/src/main/java/io/metamask/nativesdk/SessionManager.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | class SessionManager {
4 | var sessionId = ""
5 |
6 | companion object {
7 | private var instance: SessionManager? = null
8 |
9 | fun getInstance(): SessionManager {
10 | if (instance == null) {
11 | instance = SessionManager()
12 | }
13 | return instance as SessionManager
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/nativesdk/src/test/java/io/metamask/nativesdk/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.metamask.nativesdk
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/scripts/ecies-publish-module.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 | apply plugin: 'org.jetbrains.dokka'
4 |
5 | task androidSourcesJar(type: Jar) {
6 | archiveClassifier.set('sources')
7 | if (project.plugins.findPlugin("com.android.library")) {
8 | from android.sourceSets.main.java.srcDirs
9 | from android.sourceSets.main.kotlin.srcDirs
10 | } else {
11 | from sourceSets.main.java.srcDirs
12 | from sourceSets.main.kotlin.srcDirs
13 | }
14 | }
15 |
16 | tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
17 | pluginsMapConfiguration.set(
18 | ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
19 | )
20 | }
21 |
22 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
23 | archiveClassifier.set('javadoc')
24 | from dokkaJavadoc.outputDirectory
25 | }
26 |
27 | artifacts {
28 | archives androidSourcesJar
29 | archives javadocJar
30 | }
31 |
32 | group = PUBLISH_GROUP_ID
33 | version = PUBLISH_VERSION
34 |
35 | afterEvaluate {
36 | publishing {
37 | publications {
38 | release(MavenPublication) {
39 | groupId PUBLISH_GROUP_ID
40 | artifactId PUBLISH_ARTIFACT_ID
41 | version PUBLISH_VERSION
42 | if (project.plugins.findPlugin("com.android.library")) {
43 | from components.release
44 | } else {
45 | from components.java
46 | }
47 |
48 | artifact androidSourcesJar
49 | artifact javadocJar
50 |
51 | pom {
52 | name = PUBLISH_ARTIFACT_ID
53 | description = 'MetaMask Ecies Encryption'
54 | url = 'https://github.com/MetaMask/metamask-android-sdk'
55 | licenses {
56 | license {
57 | name = 'ConsenSys License'
58 | url = 'https://github.com/MetaMask/metamask-android-sdk/blob/main/LICENSE'
59 | }
60 | }
61 | developers {
62 | developer {
63 | id = 'elefantel'
64 | name = 'Mpendulo Ndlovu'
65 | email = 'mpendulo.ndlovu@consensys.net'
66 | }
67 | }
68 | scm {
69 | connection = 'scm:git:github.com/MetaMask/metamask-android-sdk.git'
70 | developerConnection = 'scm:git:ssh://github.com/MetaMask/metamask-android-sdk.git'
71 | url = 'https://github.com/MetaMask/metamask-android-sdk/tree/main'
72 | }
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
79 | signing {
80 | useInMemoryPgpKeys(
81 | rootProject.ext["signing.keyId"],
82 | rootProject.ext["signing.key"],
83 | rootProject.ext["signing.password"],
84 | )
85 | sign publishing.publications
86 | }
--------------------------------------------------------------------------------
/scripts/metamask-android-sdk-publish-module.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 | apply plugin: 'org.jetbrains.dokka'
4 |
5 | task androidSourcesJar(type: Jar) {
6 | archiveClassifier.set('sources')
7 | if (project.plugins.findPlugin("com.android.library")) {
8 | from android.sourceSets.main.java.srcDirs
9 | from android.sourceSets.main.kotlin.srcDirs
10 | } else {
11 | from sourceSets.main.java.srcDirs
12 | from sourceSets.main.kotlin.srcDirs
13 | }
14 | }
15 |
16 | tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
17 | pluginsMapConfiguration.set(
18 | ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
19 | )
20 | }
21 |
22 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
23 | archiveClassifier.set('javadoc')
24 | from dokkaJavadoc.outputDirectory
25 | }
26 |
27 | artifacts {
28 | archives androidSourcesJar
29 | archives javadocJar
30 | }
31 |
32 | group = PUBLISH_GROUP_ID
33 | version = PUBLISH_VERSION
34 |
35 | afterEvaluate {
36 | publishing {
37 | publications {
38 | release(MavenPublication) {
39 | groupId PUBLISH_GROUP_ID
40 | artifactId PUBLISH_ARTIFACT_ID
41 | version PUBLISH_VERSION
42 | if (project.plugins.findPlugin("com.android.library")) {
43 | from components.release
44 | } else {
45 | from components.java
46 | }
47 |
48 | artifact androidSourcesJar
49 | artifact javadocJar
50 |
51 | pom {
52 | name = PUBLISH_ARTIFACT_ID
53 | description = 'MetaMask SDK for Android'
54 | url = 'https://github.com/MetaMask/metamask-android-sdk'
55 | licenses {
56 | license {
57 | name = 'ConsenSys License'
58 | url = 'https://github.com/MetaMask/metamask-android-sdk/blob/main/LICENSE'
59 | }
60 | }
61 | developers {
62 | developer {
63 | id = 'elefantel'
64 | name = 'Mpendulo Ndlovu'
65 | email = 'mpendulo.ndlovu@consensys.net'
66 | }
67 | }
68 | scm {
69 | connection = 'scm:git:github.com/MetaMask/metamask-android-sdk.git'
70 | developerConnection = 'scm:git:ssh://github.com/MetaMask/metamask-android-sdk.git'
71 | url = 'https://github.com/MetaMask/metamask-android-sdk/tree/main'
72 | }
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
79 | signing {
80 | useInMemoryPgpKeys(
81 | rootProject.ext["signing.keyId"] ?: "",
82 | rootProject.ext["signing.key"] ?: "",
83 | rootProject.ext["signing.password"] ?: "",
84 | )
85 | sign publishing.publications
86 | }
--------------------------------------------------------------------------------
/scripts/publish-auth.gradle:
--------------------------------------------------------------------------------
1 | def usePropertiesFromEnvironment() {
2 | // Use system environment variables
3 | rootProject.ext["ossrhToken"] = System.getenv('OSSRH_TOKEN') ?: ""
4 | rootProject.ext["ossrhTokenPassword"] = System.getenv('OSSRH_TOKEN_PASSWORD') ?: ""
5 | rootProject.ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') ?: ""
6 | rootProject.ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') ?: ""
7 | rootProject.ext["signing.password"] = System.getenv('SIGNING_PASSWORD') ?: ""
8 | rootProject.ext["signing.key"] = System.getenv('SIGNING_KEY') ?: ""
9 | rootProject.ext["snapshot"] = System.getenv('SNAPSHOT') ?: ""
10 | rootProject.ext["rootVersionName"] = '0.1.2'
11 | }
12 |
13 | File secretPropsFile = project.rootProject.file('local.properties')
14 | if (secretPropsFile.exists()) {
15 | // Read local.properties file first if it exists
16 | Properties p = new Properties()
17 | new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
18 | p.each { name, value -> ext[name] = value }
19 |
20 | if (!p.containsKey("ossrhToken")) {
21 | usePropertiesFromEnvironment()
22 | }
23 | } else {
24 | usePropertiesFromEnvironment()
25 | }
26 |
27 | // Set up Sonatype repository
28 |
29 | nexusPublishing {
30 | repositories {
31 | sonatype {
32 | stagingProfileId = sonatypeStagingProfileId
33 | username = ossrhToken
34 | password = ossrhTokenPassword
35 |
36 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
37 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
38 | }
39 | }
40 | }
41 |
42 | tasks.withType(dokkaHtmlMultiModule.getClass()) {
43 | includes.from("DokkaRoot.md")
44 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenLocal()
6 | mavenCentral()
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenLocal()
14 | mavenCentral()
15 | }
16 | }
17 | rootProject.name = "MetaMaskAndroidSDKClient"
18 | include ':app'
19 | include ':metamask-android-sdk'
20 |
--------------------------------------------------------------------------------