diff --git a/app/build.gradle b/app/build.gradle index 698a8b6..1754ec0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 32 @@ -35,6 +36,10 @@ android { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } + +// lintOptions { +// disable 'LogNotTimber', 'StringFormatInTimber', 'ThrowableNotAtBeginning', 'BinaryOperationInTimber', 'TimberArgCount', 'TimberArgTypes', 'TimberTagLength', 'TimberExceptionLogging' +// } } dependencies { @@ -58,9 +63,23 @@ dependencies { // Retrofit implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.moshi:moshi-kotlin:1.13.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + + // Web3 + implementation 'com.github.mobilekosmos:kotlin-walletconnect-lib:0.9.9.8' + implementation 'org.java-websocket:Java-WebSocket:1.5.3' + implementation ('org.web3j:core:4.8.7-android'){ + exclude group: 'org.bouncycastle', module: '*' + } + + //1.5.0 is currently the latest stable version of AndroidX Core for Kotlin. + //If you already have "androidx.core:core" implemented, remove it. + implementation 'androidx.core:core-ktx:1.5.+' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.5.+' } + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1219675..adf3736 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -11,8 +12,10 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" + android:networkSecurityConfig="@xml/network_config" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:targetApi="n"> @@ -24,6 +27,8 @@ + + diff --git a/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java b/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java index 1e7b4a3..b8bda2c 100644 --- a/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java +++ b/app/src/main/java/ai/elimu/crowdsource/BaseApplication.java @@ -3,22 +3,57 @@ import android.app.Application; import android.util.Log; +import androidx.annotation.NonNull; + +import com.squareup.moshi.Moshi; +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory; + +import org.jetbrains.annotations.NotNull; +import org.walletconnect.Session; +import org.walletconnect.impls.FileWCSessionStore; +import org.walletconnect.impls.MoshiPayloadAdapter; +import org.walletconnect.impls.OkHttpTransport; +import org.walletconnect.impls.WCSession; +import org.walletconnect.impls.WCSessionStore; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Random; +import java.util.UUID; + +import ai.elimu.crowdsource.server.BridgeServer; import ai.elimu.crowdsource.util.SharedPreferencesHelper; import ai.elimu.crowdsource.util.VersionHelper; import ai.elimu.model.v2.enums.Language; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.internal.Intrinsics; +import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import timber.log.Timber; public class BaseApplication extends Application { + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + public static Session.FullyQualifiedConfig config; + public static Session session; + private static OkHttpClient client; + private static Moshi moshi; + private static BridgeServer bridge; + private static WCSessionStore storage; @Override public void onCreate() { Log.i(getClass().getName(), "onCreate"); super.onCreate(); - - // Log config - Timber.plant(new Timber.DebugTree()); + this.initMoshi(); + this.initClient(); + this.initBridge(); + this.initSessionStorage(); + if(BuildConfig.DEBUG){ + Timber.plant(new Timber.DebugTree()); + } Timber.i("onCreate"); VersionHelper.updateAppVersion(getApplicationContext()); @@ -48,4 +83,111 @@ public String getBaseUrl() { public String getRestUrl() { return getBaseUrl() + "/rest/v2"; } + + + private void initClient() { + OkHttpClient var10000 = (new OkHttpClient.Builder()).build(); + Intrinsics.checkNotNullExpressionValue(var10000, "OkHttpClient.Builder().build()"); + client = var10000; + } + + private void initMoshi() { + Moshi var10000 = (new com.squareup.moshi.Moshi.Builder()).add(new KotlinJsonAdapterFactory()).build(); + Intrinsics.checkNotNullExpressionValue(var10000, "Moshi.Builder().build()"); + moshi = var10000; + } + + private void initBridge() { + bridge = new BridgeServer(moshi); + bridge.start(); + } + + private void initSessionStorage() { + File tmp = new File(this.getCacheDir(), "session_store.json"); + try { + tmp.createNewFile(); + storage = new FileWCSessionStore(tmp, moshi); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + @NotNull + public static String bytesToHex(byte[] bytes) { + Intrinsics.checkNotNullParameter(bytes, "bytes"); + char[] hexChars = new char[bytes.length * 2]; + int j = 0; + + for (int var4 = bytes.length; j < var4; ++j) { + int v = bytes[j] & 255; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 15]; + } + + return new String(hexChars); + } + + public static void resetSession() throws Exception { + if (session != null) { + session.clearCallbacks(); + } + byte[] randomBytes = new byte[32]; + Random r = new Random(); + r.nextBytes(randomBytes); + String key = bytesToHex(randomBytes); + String uuid = UUID.randomUUID().toString(); + config = new Session.FullyQualifiedConfig(uuid, "http://localhost:" + BridgeServer.Companion.getPORT(), key, "wc", 1); +// config = new Session.FullyQualifiedConfig(uuid, "https://bridge.walletconnect.org", "f70af2060965927b7e709503e71b3cbf", "wc", 1); + session = new WCSession( + config, + new WrappedMoshiPayloadAdapter(new MoshiPayloadAdapter(moshi)), + storage, + new WrappedOkHttpTransportBuilder(new OkHttpTransport.Builder(client, moshi)), + new Session.PeerMeta( + "elimu.ai", + "Elimu Crowdsource App", + "Elimu Crowdsource App", + Collections.emptyList() + ), + null, + null + ); + session.offer(); + } + + static class WrappedMoshiPayloadAdapter implements Session.PayloadAdapter { + private final MoshiPayloadAdapter wrapped; + + public WrappedMoshiPayloadAdapter(MoshiPayloadAdapter wrapped) { + this.wrapped = wrapped; + } + + @NonNull + @Override + public Session.MethodCall parse(@NonNull String s, @NonNull String s1) { + return this.wrapped.parse(s, s1); + } + + @NonNull + @Override + public String prepare(@NonNull Session.MethodCall methodCall, @NonNull String s) { + return this.wrapped.prepare(methodCall, s); + } + } + + static class WrappedOkHttpTransportBuilder implements Session.Transport.Builder { + private final OkHttpTransport.Builder wrapped; + + public WrappedOkHttpTransportBuilder(OkHttpTransport.Builder wrapped) { + this.wrapped = wrapped; + } + + @NonNull + @Override + public Session.Transport build(@NonNull String s, @NonNull Function1 function1, @NonNull Function1 function11) { + return this.wrapped.build(s, function1, function11); + } + } } diff --git a/app/src/main/java/ai/elimu/crowdsource/MainActivity.java b/app/src/main/java/ai/elimu/crowdsource/MainActivity.java index 67bd8a6..70a5757 100644 --- a/app/src/main/java/ai/elimu/crowdsource/MainActivity.java +++ b/app/src/main/java/ai/elimu/crowdsource/MainActivity.java @@ -3,24 +3,46 @@ import android.content.Intent; import android.os.Bundle; import android.text.TextUtils; +import android.view.View; +import android.widget.Button; import androidx.appcompat.app.AppCompatActivity; +import ai.elimu.crowdsource.ui.BottomNavigationActivity; import ai.elimu.crowdsource.ui.authentication.SignInWithGoogleActivity; +import ai.elimu.crowdsource.ui.authentication.SignInWithWeb3Activity; import ai.elimu.crowdsource.ui.language.SelectLanguageActivity; -import ai.elimu.crowdsource.ui.BottomNavigationActivity; import ai.elimu.crowdsource.util.SharedPreferencesHelper; import ai.elimu.model.v2.enums.Language; import timber.log.Timber; public class MainActivity extends AppCompatActivity { + private Button signInWeb3Button, signInGoogleButton; + @Override protected void onCreate(Bundle savedInstanceState) { Timber.i("onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + signInWeb3Button = findViewById(R.id.w3_sign_in); + signInGoogleButton = findViewById(R.id.g_sign_in); + signInWeb3Button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent signInWithWeb3Intent = new Intent(getApplicationContext(), SignInWithWeb3Activity.class); + startActivity(signInWithWeb3Intent); + } + }); + signInGoogleButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Redirect to sign-in with Google + Intent signInWithGoogleIntent = new Intent(getApplicationContext(), SignInWithGoogleActivity.class); + startActivity(signInWithGoogleIntent); + } + }); } @Override @@ -28,6 +50,7 @@ protected void onStart() { Timber.i("onStart"); super.onStart(); + // Check if language has been selected Language language = SharedPreferencesHelper.getLanguage(getApplicationContext()); Timber.i("language: " + language); @@ -39,13 +62,10 @@ protected void onStart() { } else { // Check for an existing signed-in Contributor String providerIdGoogle = SharedPreferencesHelper.getProviderIdGoogle(getApplicationContext()); + String web3Account = SharedPreferencesHelper.getProviderIdWeb3(getApplicationContext()); Timber.i("providerIdGoogle: " + providerIdGoogle); - if (TextUtils.isEmpty(providerIdGoogle)) { - // Redirect to sign-in with Google - Intent signInWithGoogleIntent = new Intent(getApplicationContext(), SignInWithGoogleActivity.class); - startActivity(signInWithGoogleIntent); - finish(); - } else { + Timber.i("web3 account: " + web3Account); + if (!TextUtils.isEmpty(providerIdGoogle) || !TextUtils.isEmpty(web3Account)) { // Redirect to crowdsourcing activity selection Intent intent = new Intent(getApplicationContext(), BottomNavigationActivity.class); startActivity(intent); diff --git a/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt b/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt new file mode 100644 index 0000000..6ccf5ee --- /dev/null +++ b/app/src/main/java/ai/elimu/crowdsource/server/BridgeServer.kt @@ -0,0 +1,100 @@ +package ai.elimu.crowdsource.server + +import android.util.Log +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import org.java_websocket.WebSocket +import org.java_websocket.handshake.ClientHandshake +import org.java_websocket.server.WebSocketServer +import java.lang.Exception +import java.lang.ref.WeakReference +import java.net.InetSocketAddress +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class BridgeServer(moshi: Moshi) : WebSocketServer(InetSocketAddress(PORT)) { + + private val adapter = moshi.adapter>( + Types.newParameterizedType( + Map::class.java, + String::class.java, + Any::class.java + ) + ) + + private val pubs: MutableMap>> = ConcurrentHashMap() + private val pubsLock = Any() + private val pubsCache: MutableMap = ConcurrentHashMap() + + override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) { + Log.d("#####", "onOpen: ${conn?.remoteSocketAddress?.address?.hostAddress}") + } + + override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) { + Log.d("#####", "onClose: ${conn?.remoteSocketAddress?.address?.hostAddress}") + conn?.let { cleanUpSocket(it) } + } + + override fun onMessage(conn: WebSocket?, message: String?) { + Log.d("#####", "Message: $message") + try { + conn ?: error("Unknown socket") + message?.also { + val msg = adapter.fromJson(it) ?: error("Invalid message") + val type: String = msg["type"] as String? ?: error("Type not found") + val topic: String = msg["topic"] as String? ?: error("Topic not found") + when (type) { + "pub" -> { + var sendMessage = false + pubs[topic]?.forEach { r -> + r.get()?.apply { + send(message) + sendMessage = true + } + } + if (!sendMessage) { + Log.d("#####", "Cache message: $message") + pubsCache[topic] = message + } + } + "sub" -> { + pubs.getOrPut(topic, { mutableListOf() }).add(WeakReference(conn)) + pubsCache[topic]?.let { cached -> + Log.d("#####", "Send cached: $cached") + conn.send(cached) + } + } + "ack" -> { + pubsCache.remove(topic); + } + else -> error("Unknown type") + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onStart() { + Log.d("#####", "Server started") + connectionLostTimeout = 0 + } + + override fun onError(conn: WebSocket?, ex: Exception?) { + Log.d("#####", "onError") + ex?.printStackTrace() + conn?.let { cleanUpSocket(it) } + } + + private fun cleanUpSocket(conn: WebSocket) { + synchronized(pubsLock) { + pubs.forEach { + it.value.removeAll { r -> r.get().let { v -> v == null || v == conn } } + } + } + } + + companion object { + val PORT = 5000 + Random().nextInt(60000) + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithWeb3Activity.java b/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithWeb3Activity.java new file mode 100644 index 0000000..541ac1c --- /dev/null +++ b/app/src/main/java/ai/elimu/crowdsource/ui/authentication/SignInWithWeb3Activity.java @@ -0,0 +1,270 @@ +package ai.elimu.crowdsource.ui.authentication; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import org.json.JSONException; +import org.json.JSONObject; +import org.walletconnect.Session; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import ai.elimu.crowdsource.BaseApplication; +import ai.elimu.crowdsource.MainActivity; +import ai.elimu.crowdsource.R; +import ai.elimu.crowdsource.rest.ContributorService; +import ai.elimu.crowdsource.util.EthersUtils; +import ai.elimu.crowdsource.util.SharedPreferencesHelper; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.Retrofit; +import timber.log.Timber; + +/** + * Prompts the Contributor for access to her Web3 account. Then stores the account details in the + * webapp's database. + *

+ * See https://docs.metamask.io/guide/signing-data.html#signing-data-with-metamask + */ +public class SignInWithWeb3Activity extends AppCompatActivity implements Session.Callback { + + public static final String W3_SIGN_MESSAGE = "I verify ownership of this account 👍"; + private static String providerIdWeb3 = ""; + private static String provideIdWeb3Signature = ""; + private Button connectW3Button; + + private ProgressBar signInProgressBar; + + + private long txRequest; + private Button signInW3Button; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Timber.i("onCreate"); + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_sign_in_with_web3); + + signInProgressBar = findViewById(R.id.sign_in_progressbar); + + signInW3Button = findViewById(R.id.sign_in_web3_button); + connectW3Button = findViewById(R.id.connect_web3_button); + connectW3Button.setOnClickListener(view -> { + try { + BaseApplication.resetSession(); + } catch (Exception e) { + Toast.makeText(this, "WalletConnect session is null!", Toast.LENGTH_LONG).show(); + return; + } + BaseApplication.session.addCallback(SignInWithWeb3Activity.this); + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(BaseApplication.config.toWCUri())); + startActivity(i); + }); + + signInW3Button.setOnClickListener(view -> { + if (BaseApplication.session.approvedAccounts() == null || BaseApplication.session.approvedAccounts().size() == 0) { + Toast.makeText(this, "No approved account found", Toast.LENGTH_LONG).show(); + return; + } + String from = BaseApplication.session.approvedAccounts().get(0); + long txRequest = System.currentTimeMillis(); + Timber.tag("web3.wallet").i("from: %s", from); + BaseApplication.session.performMethodCall( + new Session.MethodCall.SignMessage( + txRequest, + from, + W3_SIGN_MESSAGE + ), + response -> { + if (response.id() == SignInWithWeb3Activity.this.txRequest) { + SignInWithWeb3Activity.this.txRequest = -1; + if (response.getResult() != null) { + + Timber.tag("web3.wallet").i("signed message: %s", response); + boolean recovered = false; + recovered = EthersUtils.verifyMessage(from, W3_SIGN_MESSAGE, response.getResult().toString()); + Timber.tag("web3.wallet").i("recovered address: %s", recovered); + if (recovered) { + provideIdWeb3Signature = response.getResult().toString(); + providerIdWeb3 = from; + } + } + } + return null; + } + ); + navigateToWallet(); + this.txRequest = txRequest; + }); + + } + + @Override + protected void onStart() { + Timber.i("onStart"); + super.onStart(); + + // Web3 Sign In button. + initialSetup(); + + if (!providerIdWeb3.equals("")) { + updateUI(); + } + + + } + + private void updateUI() { + Timber.i("updateUI"); + + + // Display the progressbar while connecting to the webapp + signInW3Button.setVisibility(View.GONE); + connectW3Button.setVisibility(View.GONE); + signInProgressBar.setVisibility(View.VISIBLE); + + // Prepare JSON object to be sent to the webapp's REST API + JSONObject contributorJSONObject = new JSONObject(); + try { + contributorJSONObject.put("providerIdWeb3", providerIdWeb3); + contributorJSONObject.put("providerIdWeb3Signature", provideIdWeb3Signature); + } catch (JSONException e) { + Timber.e(e); + } + Timber.i("contributorJSONObject: %s", contributorJSONObject); + + // Register the Contributor in the webapp's database + BaseApplication baseApplication = (BaseApplication) getApplication(); + Retrofit retrofit = baseApplication.getRetrofit(); + ContributorService contributorService = retrofit.create(ContributorService.class); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), contributorJSONObject.toString()); + Call call = contributorService.createContributorWeb3(requestBody); + Timber.i("call.request(): %s", call.request()); + ExecutorService executorService = Executors.newSingleThreadExecutor(); + executorService.execute(new Runnable() { + @Override + public void run() { + Timber.i("run"); + + try { + Response response = call.execute(); + Timber.i("response: %s", response); + Timber.i("response.isSuccessful(): %s", response.isSuccessful()); + if (response.isSuccessful()) { + String bodyString = response.body().string(); + Timber.i("bodyString: %s", bodyString); + + // Persist the Contributor's account details in SharedPreferences + SharedPreferencesHelper.storeProviderIdWeb3(getApplicationContext(), provideIdWeb3Signature); + + // Redirect to the MainActivity + Intent mainActivityIntent = new Intent(getApplicationContext(), MainActivity.class); + startActivity(mainActivityIntent); + finish(); + } else { + String errorBodyString = response.errorBody().string(); + Timber.e("errorBodyString: %s", errorBodyString); + // TODO: Handle error + + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), "Error " + response.code() + ": \"" + response.message() + "\"", Toast.LENGTH_LONG).show(); + + // Hide the progressbar + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); + signInProgressBar.setVisibility(View.GONE); + providerIdWeb3 = ""; + provideIdWeb3Signature = ""; + }); + } + } catch (IOException e) { + Timber.e(e); + // TODO: Handle error + + runOnUiThread(() -> { + Toast.makeText(getApplicationContext(), "Error: " + e.getClass().getSimpleName(), Toast.LENGTH_LONG).show(); + + // Hide the progressbar + signInW3Button.setVisibility(View.VISIBLE); + connectW3Button.setVisibility(View.VISIBLE); + signInProgressBar.setVisibility(View.GONE); + providerIdWeb3 = ""; + provideIdWeb3Signature = ""; + }); + } + } + }); + + } + + + @Override + public void onMethodCall(@NonNull Session.MethodCall methodCall) { + + } + + private void sessionApproved() { + signInW3Button.setVisibility(View.VISIBLE); + } + + private void sessionClosed() { + signInW3Button.setVisibility(View.INVISIBLE); + } + + @Override + public void onStatus(@NonNull Session.Status status) { + if (status instanceof Session.Status.Error) { + + } else if (status instanceof Session.Status.Approved) { + sessionApproved(); + } else if (status instanceof Session.Status.Closed) { + sessionClosed(); + } else if (status instanceof Session.Status.Connected) { + requestConnectionToWallet(); + } else if (status instanceof Session.Status.Disconnected) { + + } + } + + private void requestConnectionToWallet() { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(BaseApplication.config.toWCUri())); + startActivity(i); + } + + private void navigateToWallet() { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("wc:")); + startActivity(i); + } + + private void initialSetup() { + if (BaseApplication.session != null) { + BaseApplication.session.addCallback(this); + sessionApproved(); + } + + } + + @Override + protected void onDestroy() { + BaseApplication.session.removeCallback(this); + super.onDestroy(); + } + +} diff --git a/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java b/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java new file mode 100644 index 0000000..3f780eb --- /dev/null +++ b/app/src/main/java/ai/elimu/crowdsource/util/EthersUtils.java @@ -0,0 +1,60 @@ +package ai.elimu.crowdsource.util; + +import org.web3j.crypto.ECDSASignature; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; +import org.web3j.crypto.Sign.SignatureData; +import org.web3j.utils.Numeric; + +import java.math.BigInteger; +import java.util.Arrays; + +import timber.log.Timber; + +public class EthersUtils { + private static final String MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"; + + public static boolean verifyMessage(String address, String message, String signature) { + String recovered = EthersUtils.recoverAddress(EthersUtils.hashMessage(message), signature); + Timber.tag("ethers-utils").i("recovered address: %s", recovered); + return address.equalsIgnoreCase(recovered); + } + + public static byte[] hashMessage(String message) { + // Note: The label prefix is part of the standard + String label = "\u0019Ethereum Signed Message:\n" + String.valueOf(message.getBytes().length) + message; + // Get message hash using SHA-3 + return Hash.sha3((label).getBytes()); + } + + public static String recoverAddress(byte[] digest, String signature) { + SignatureData sd = EthersUtils.getSignatureData(signature); + for (int i = 0; i < 4; i++) { + final BigInteger publicKey = Sign.recoverFromSignature( + (byte) i, + new ECDSASignature( + new BigInteger(1, sd.getR()), + new BigInteger(1, sd.getS()) + ), + digest + ); + + if (publicKey != null) { + return "0x" + Keys.getAddress(publicKey); + } + } + return null; + } + + private static SignatureData getSignatureData(String signature) { + byte[] signatureBytes = Numeric.hexStringToByteArray(signature); + byte v = signatureBytes[64]; + if (v < 27) { + v += 27; + } + byte[] r = (byte[]) Arrays.copyOfRange(signatureBytes, 0, 32); + byte[] s = (byte[]) Arrays.copyOfRange(signatureBytes, 32, 64); + return new SignatureData(v, r, s); + } +} \ No newline at end of file diff --git a/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java b/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java index b3a0d22..e689b71 100644 --- a/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java +++ b/app/src/main/java/ai/elimu/crowdsource/util/SharedPreferencesHelper.java @@ -65,6 +65,7 @@ public static void storeProviderIdGoogle(Context context, String providerIdGoogl sharedPreferences.edit().putString(PREF_PROVIDER_ID_GOOGLE, providerIdGoogle).apply(); } + public static String getProviderIdGoogle(Context context) { Timber.i("getProviderIdGoogle"); SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE); @@ -76,7 +77,6 @@ public static String getProviderIdGoogle(Context context) { } } - public static void storeProviderIdWeb3(Context context, String providerIdWeb3) { Timber.i("storeProviderIdWeb3"); SharedPreferences sharedPreferences = context.getSharedPreferences(SHARED_PREFS, Context.MODE_PRIVATE); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6cb963f..f97b84e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,5 +6,29 @@ android:layout_marginHorizontal="@dimen/activity_horizontal_margin" android:layout_marginVertical="@dimen/activity_vertical_margin"> + + + + +