From 7b135a1d53b1f560fe5876eda2a1081354d4ab36 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 15:54:29 +0300 Subject: [PATCH 1/9] add test for KPAccountCreator --- identity/build.gradle | 4 +-- .../androidTest/java/KPAccountCreatorTest.kt | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 identity/src/androidTest/java/KPAccountCreatorTest.kt diff --git a/identity/build.gradle b/identity/build.gradle index 11214fba..27c14158 100644 --- a/identity/build.gradle +++ b/identity/build.gradle @@ -39,8 +39,8 @@ dependencies { implementation project(":did") implementation project(":core") -// androidTestImplementation "com.android.support.test:runner:$test_runner_version" -// androidTestImplementation "com.android.support.test:rules:$test_runner_version" + androidTestImplementation "com.android.support.test:runner:$test_runner_version" + androidTestImplementation "com.android.support.test:rules:$test_runner_version" testImplementation "junit:junit:$junit_version" diff --git a/identity/src/androidTest/java/KPAccountCreatorTest.kt b/identity/src/androidTest/java/KPAccountCreatorTest.kt new file mode 100644 index 00000000..737ba68c --- /dev/null +++ b/identity/src/androidTest/java/KPAccountCreatorTest.kt @@ -0,0 +1,33 @@ +package me.uport.sdk.identity + +import android.content.Context +import android.support.test.InstrumentationRegistry +import kotlinx.coroutines.experimental.runBlocking +import me.uport.sdk.core.Networks +import org.junit.Assert.* +import org.junit.Test + +import org.junit.Before + +class KPAccountCreatorTest { + + lateinit var appContext: Context + + @Before + fun run_before_every_test() { + appContext = InstrumentationRegistry.getTargetContext() + } + + @Test + fun createAccount() { + runBlocking { + val account = KPAccountCreator(appContext).createAccount(Networks.rinkeby.network_id) + assertNotNull(account) + assertNotEquals(Account.blank, account) + assertTrue(account.signerType == SignerType.KeyPair) + assertTrue(account.address.isNotEmpty()) + assertTrue(account.publicAddress.isNotEmpty()) + assertTrue(account.deviceAddress.isNotEmpty()) + } + } +} \ No newline at end of file From 253e198d0a38de5de6dfcf9192fedf906d303aa3 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 15:56:33 +0300 Subject: [PATCH 2/9] simplify KPAccountCreator using coroutine --- .../me/uport/sdk/identity/KPAccountCreator.kt | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt b/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt index c045436d..e587c42c 100644 --- a/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt +++ b/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt @@ -3,27 +3,22 @@ package me.uport.sdk.identity import android.content.Context import com.uport.sdk.signer.UportHDSigner import com.uport.sdk.signer.encryption.KeyProtection +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch -class KPAccountCreator(private val context: Context) : AccountCreator { +class KPAccountCreator(private val appContext: Context) : AccountCreator { override fun createAccount(networkId: String, forceRestart: Boolean, callback: AccountCreatorCallback) { - - val signer = UportHDSigner() - - signer.createHDSeed(context, KeyProtection.Level.SIMPLE) { err, rootAddress, _ -> - if (err != null) { - return@createHDSeed callback(err, Account.blank) - } - signer.computeAddressForPath(context, - rootAddress, - Account.GENERIC_DEVICE_KEY_DERIVATION_PATH, - "") { ex, deviceAddress, _ -> - if (ex != null) { - return@computeAddressForPath callback(err, Account.blank) - } - - val acc = Account( - rootAddress, + launch { + val signer = UportHDSigner() + try { + val (handle, _) = signer.createHDSeed(appContext, KeyProtection.Level.SIMPLE) + val (deviceAddress, _) = signer.computeAddressForPath(appContext, + handle, + Account.GENERIC_DEVICE_KEY_DERIVATION_PATH, + "") + val account = Account( + handle, deviceAddress, networkId, deviceAddress, @@ -33,8 +28,11 @@ class KPAccountCreator(private val context: Context) : AccountCreator { SignerType.KeyPair ) - return@computeAddressForPath callback(null, acc) + launch(UI) { callback(null, account) } + } catch (err: Exception) { + launch(UI) { callback(err, Account.blank) } } + } } From 90d06b36057211f9fba450f7514d0e9ce66264fd Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 15:57:24 +0300 Subject: [PATCH 3/9] add `importAccount()` method to `AccountCreator` interface --- .../java/me/uport/sdk/identity/AccountCreator.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/identity/src/main/java/me/uport/sdk/identity/AccountCreator.kt b/identity/src/main/java/me/uport/sdk/identity/AccountCreator.kt index 5c8d48d3..3da0d817 100644 --- a/identity/src/main/java/me/uport/sdk/identity/AccountCreator.kt +++ b/identity/src/main/java/me/uport/sdk/identity/AccountCreator.kt @@ -6,9 +6,11 @@ typealias AccountCreatorCallback = (err: Exception?, acc: Account) -> Unit interface AccountCreator { fun createAccount(networkId: String, forceRestart: Boolean = false, callback: AccountCreatorCallback) + + fun importAccount(networkId: String, seedPhrase: String, forceRestart: Boolean, callback: AccountCreatorCallback) } -suspend fun AccountCreator.createAccount(networkId: String, forceRestart: Boolean): Account = suspendCoroutine { continuation -> +suspend fun AccountCreator.createAccount(networkId: String, forceRestart: Boolean = false): Account = suspendCoroutine { continuation -> this.createAccount(networkId, forceRestart) { err, account -> if (err != null) { continuation.resumeWithException(err) @@ -16,4 +18,14 @@ suspend fun AccountCreator.createAccount(networkId: String, forceRestart: Boolea continuation.resume(account) } } +} + +suspend fun AccountCreator.importAccount(networkId: String, seedPhrase: String, forceRestart: Boolean = false): Account = suspendCoroutine { continuation -> + this.importAccount(networkId, seedPhrase, forceRestart) { err, account -> + if (err != null) { + continuation.resumeWithException(err) + } else { + continuation.resume(account) + } + } } \ No newline at end of file From 6189b33a68f32c5fbe2099a95f8643aaac742171 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 17:40:59 +0300 Subject: [PATCH 4/9] add a test for KeyPair account imports --- .../androidTest/java/KPAccountCreatorTest.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/identity/src/androidTest/java/KPAccountCreatorTest.kt b/identity/src/androidTest/java/KPAccountCreatorTest.kt index 737ba68c..c5ef5ac3 100644 --- a/identity/src/androidTest/java/KPAccountCreatorTest.kt +++ b/identity/src/androidTest/java/KPAccountCreatorTest.kt @@ -5,13 +5,12 @@ import android.support.test.InstrumentationRegistry import kotlinx.coroutines.experimental.runBlocking import me.uport.sdk.core.Networks import org.junit.Assert.* -import org.junit.Test - import org.junit.Before +import org.junit.Test class KPAccountCreatorTest { - lateinit var appContext: Context + private lateinit var appContext: Context @Before fun run_before_every_test() { @@ -30,4 +29,21 @@ class KPAccountCreatorTest { assertTrue(account.deviceAddress.isNotEmpty()) } } + + @Test + fun importAccount() { + + val referenceSeedPhrase = "vessel ladder alter error federal sibling chat ability sun glass valve picture" + + runBlocking { + val account = KPAccountCreator(appContext).importAccount(Networks.rinkeby.network_id, referenceSeedPhrase) + assertNotNull(account) + assertNotEquals(Account.blank, account) + assertTrue(account.signerType == SignerType.KeyPair) + assertEquals("2opxPamUQoLarQHAoVDKo2nDNmfQLNCZif4", account.address) + assertEquals("0x847e5e3e8b2961c2225cb4a2f719d5409c7488c6", account.publicAddress) + assertEquals("0x847e5e3e8b2961c2225cb4a2f719d5409c7488c6", account.deviceAddress) + assertEquals("0x794adde0672914159c1b77dd06d047904fe96ac8", account.handle) + } + } } \ No newline at end of file From c6a35dd68a65ffdca6228182b890b4588caad3ab Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 17:41:35 +0300 Subject: [PATCH 5/9] add import support to account creator implementations --- .../me/uport/sdk/identity/KPAccountCreator.kt | 16 +++++++-- .../identity/MetaIdentityAccountCreator.kt | 34 ++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt b/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt index e587c42c..1b384e40 100644 --- a/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt +++ b/identity/src/main/java/me/uport/sdk/identity/KPAccountCreator.kt @@ -8,11 +8,15 @@ import kotlinx.coroutines.experimental.launch class KPAccountCreator(private val appContext: Context) : AccountCreator { - override fun createAccount(networkId: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + private fun createOrImportAccount(networkId: String, phrase: String?, callback: AccountCreatorCallback) { launch { val signer = UportHDSigner() try { - val (handle, _) = signer.createHDSeed(appContext, KeyProtection.Level.SIMPLE) + val (handle, _) = if (phrase.isNullOrBlank()) { + signer.createHDSeed(appContext, KeyProtection.Level.SIMPLE) + } else { + signer.importHDSeed(appContext, KeyProtection.Level.SIMPLE, phrase!!) + } val (deviceAddress, _) = signer.computeAddressForPath(appContext, handle, Account.GENERIC_DEVICE_KEY_DERIVATION_PATH, @@ -36,4 +40,12 @@ class KPAccountCreator(private val appContext: Context) : AccountCreator { } } + override fun createAccount(networkId: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + createOrImportAccount(networkId, null, callback) + } + + override fun importAccount(networkId: String, seedPhrase: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + createOrImportAccount(networkId, seedPhrase, callback) + } + } \ No newline at end of file diff --git a/identity/src/main/java/me/uport/sdk/identity/MetaIdentityAccountCreator.kt b/identity/src/main/java/me/uport/sdk/identity/MetaIdentityAccountCreator.kt index fcabd48d..5f9e2273 100644 --- a/identity/src/main/java/me/uport/sdk/identity/MetaIdentityAccountCreator.kt +++ b/identity/src/main/java/me/uport/sdk/identity/MetaIdentityAccountCreator.kt @@ -11,7 +11,6 @@ import me.uport.sdk.identity.endpoints.lookupIdentityInfo import me.uport.sdk.identity.endpoints.requestIdentityCreation - class MetaIdentityAccountCreator( private val context: Context, private val fuelTokenProvider: IFuelTokenProvider) : AccountCreator { @@ -28,7 +27,7 @@ class MetaIdentityAccountCreator( * * To force the creation of a new identity, use [forceRestart] */ - override fun createAccount(networkId: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + private fun createOrImportAccount(networkId: String, phrase: String?, forceRestart: Boolean, callback: AccountCreatorCallback) { var (state, oldBundle) = if (forceRestart) { (AccountCreationState.NONE to PersistentBundle()) @@ -41,13 +40,24 @@ class MetaIdentityAccountCreator( when (state) { AccountCreationState.NONE -> { - signer.createHDSeed(context, KeyProtection.Level.SIMPLE) { err, rootAddress, _ -> - if (err != null) { - return@createHDSeed fail(err, callback) + if (phrase.isNullOrEmpty()) { + signer.createHDSeed(context, KeyProtection.Level.SIMPLE) { err, rootAddress, _ -> + if (err != null) { + return@createHDSeed fail(err, callback) + } + val bundle = oldBundle.copy(rootAddress = rootAddress) + progress.save(AccountCreationState.ROOT_KEY_CREATED, bundle) + return@createHDSeed createAccount(networkId, false, callback) + } + } else { + signer.importHDSeed(context, KeyProtection.Level.SIMPLE, phrase!!) { err, rootAddress, _ -> + if (err != null) { + return@importHDSeed fail(err, callback) + } + val bundle = oldBundle.copy(rootAddress = rootAddress) + progress.save(AccountCreationState.ROOT_KEY_CREATED, bundle) + return@importHDSeed createAccount(networkId, false, callback) } - val bundle = oldBundle.copy(rootAddress = rootAddress) - progress.save(AccountCreationState.ROOT_KEY_CREATED, bundle) - return@createHDSeed createAccount(networkId, false, callback) } } @@ -150,6 +160,14 @@ class MetaIdentityAccountCreator( } } + override fun createAccount(networkId: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + createOrImportAccount(networkId, null, forceRestart, callback) + } + + override fun importAccount(networkId: String, seedPhrase: String, forceRestart: Boolean, callback: AccountCreatorCallback) { + createOrImportAccount(networkId, seedPhrase, forceRestart, callback) + } + private fun fail(err: Exception, callback: AccountCreatorCallback) { progress.save(AccountCreationState.NONE) return callback(err, Account.blank) From 873f67c5cc75df8ecaba662b4f9096db996472a4 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 18:34:45 +0300 Subject: [PATCH 6/9] add an extra test to check thread of account creation callback --- sdk/src/androidTest/java/me/uport/sdk/UportTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt index dfa80e47..d84c95eb 100644 --- a/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt +++ b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt @@ -1,5 +1,6 @@ package me.uport.sdk +import android.os.Looper import android.support.test.InstrumentationRegistry import kotlinx.coroutines.experimental.runBlocking import me.uport.sdk.core.Networks @@ -8,6 +9,7 @@ import org.junit.Assert.* import org.junit.Before import org.junit.Test import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit class UportTest { @@ -34,4 +36,15 @@ class UportTest { } } + @Test + fun account_completion_called_on_main_thread() { + val latch = CountDownLatch(1) + Uport.createAccount(Networks.rinkeby) { _, _ -> + assertTrue(Looper.getMainLooper().isCurrentThread) + latch.countDown() + } + + latch.await(15, TimeUnit.SECONDS) + } + } \ No newline at end of file From 5028d4789fba3fd242cfe243c035ba42561467a2 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 18:36:02 +0300 Subject: [PATCH 7/9] migrate Uport account creation code to use coroutine --- sdk/src/main/java/me/uport/sdk/Uport.kt | 31 ++++++++++--------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/sdk/src/main/java/me/uport/sdk/Uport.kt b/sdk/src/main/java/me/uport/sdk/Uport.kt index 882a9217..a1caf4cf 100644 --- a/sdk/src/main/java/me/uport/sdk/Uport.kt +++ b/sdk/src/main/java/me/uport/sdk/Uport.kt @@ -4,15 +4,10 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences -import android.os.Handler -import android.os.Looper.getMainLooper import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.launch import me.uport.sdk.core.EthNetwork -import me.uport.sdk.identity.Account -import me.uport.sdk.identity.AccountCreatorCallback -import me.uport.sdk.identity.IFuelTokenProvider -import me.uport.sdk.identity.KPAccountCreator +import me.uport.sdk.identity.* import kotlin.coroutines.experimental.suspendCoroutine object Uport { @@ -90,7 +85,7 @@ object Uport { * * To really create a new account, call [deleteAccount] first. */ - fun createAccount(networkId: String, completion: AccountCreatorCallback) { + private fun createAccount(networkId: String, completion: AccountCreatorCallback) { if (!initialized) { throw UportNotInitializedException() } @@ -101,19 +96,17 @@ object Uport { return } - val creator = KPAccountCreator(config.applicationContext) - return creator.createAccount(networkId) { err, acc -> - if (err != null) { - Handler(getMainLooper()).post { completion(err, acc) } - @Suppress("LABEL_NAME_CLASH") - return@createAccount - } + launch { + try { + val creator = KPAccountCreator(config.applicationContext) + val acc = creator.createAccount(networkId) + prefs.edit().putString(DEFAULT_ACCOUNT, acc.toJson()).apply() + defaultAccount = defaultAccount ?: acc - val serialized = acc.toJson() - prefs.edit().putString(DEFAULT_ACCOUNT, serialized).apply() - defaultAccount = defaultAccount ?: acc - - Handler(getMainLooper()).post { completion(err, acc) } + launch(UI) { completion(null, acc) } + } catch (err: Exception) { + launch(UI) { completion(err, Account.blank) } + } } } From 794fe25ddc54b00c120bb89e0dbb40799205e7eb Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 18:39:35 +0300 Subject: [PATCH 8/9] add test to check for account import functionality (failing) --- .../androidTest/java/me/uport/sdk/UportTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt index d84c95eb..79a562e1 100644 --- a/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt +++ b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt @@ -47,4 +47,22 @@ class UportTest { latch.await(15, TimeUnit.SECONDS) } + @Test + fun account_can_be_imported() { + val tested = Uport + val referenceSeedPhrase = "vessel ladder alter error federal sibling chat ability sun glass valve picture" + + tested.defaultAccount = null + + runBlocking { + val account = tested.createAccount(Networks.rinkeby, referenceSeedPhrase) + assertNotNull(account) + assertNotEquals(Account.blank, account) + assertEquals("2opxPamUQoLarQHAoVDKo2nDNmfQLNCZif4", account.address) + assertEquals("0x847e5e3e8b2961c2225cb4a2f719d5409c7488c6", account.publicAddress) + assertEquals("0x847e5e3e8b2961c2225cb4a2f719d5409c7488c6", account.deviceAddress) + assertEquals("0x794adde0672914159c1b77dd06d047904fe96ac8", account.handle) + } + } + } \ No newline at end of file From e0f3dab8ef71bd18121156782145fac8757b4846 Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 26 Jul 2018 18:47:01 +0300 Subject: [PATCH 9/9] allow `Uport.createAccount()` to receive a seedPhrase to recover an account --- sdk/src/main/java/me/uport/sdk/Uport.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sdk/src/main/java/me/uport/sdk/Uport.kt b/sdk/src/main/java/me/uport/sdk/Uport.kt index a1caf4cf..a03a12b6 100644 --- a/sdk/src/main/java/me/uport/sdk/Uport.kt +++ b/sdk/src/main/java/me/uport/sdk/Uport.kt @@ -54,8 +54,8 @@ object Uport { * * To really create a new account, call [deleteAccount] first. */ - fun createAccount(network: EthNetwork, completion: AccountCreatorCallback) { - return createAccount(network.network_id, completion) + fun createAccount(network: EthNetwork, seedPhrase: String? = null, completion: AccountCreatorCallback) { + return createAccount(network.network_id, seedPhrase, completion) } /** @@ -66,8 +66,8 @@ object Uport { * The created account is saved as [defaultAccount] before returning with a result * */ - suspend fun createAccount(network: EthNetwork): Account = suspendCoroutine { cont -> - this.createAccount(network) { err, acc -> + suspend fun createAccount(network: EthNetwork, seedPhrase: String? = null): Account = suspendCoroutine { cont -> + this.createAccount(network, seedPhrase) { err, acc -> if (err != null) { cont.resumeWithException(err) } else { @@ -85,12 +85,12 @@ object Uport { * * To really create a new account, call [deleteAccount] first. */ - private fun createAccount(networkId: String, completion: AccountCreatorCallback) { + private fun createAccount(networkId: String, seedPhrase: String?, completion: AccountCreatorCallback) { if (!initialized) { throw UportNotInitializedException() } - //single account limitation should disappear in future versions + //FIXME: single account limitation should disappear in future versions if (defaultAccount != null) { launch(UI) { completion(null, defaultAccount!!) } return @@ -99,7 +99,11 @@ object Uport { launch { try { val creator = KPAccountCreator(config.applicationContext) - val acc = creator.createAccount(networkId) + val acc = if (seedPhrase.isNullOrBlank()) { + creator.createAccount(networkId) + } else { + creator.importAccount(networkId, seedPhrase!!) + } prefs.edit().putString(DEFAULT_ACCOUNT, acc.toJson()).apply() defaultAccount = defaultAccount ?: acc