diff --git a/README.md b/README.md index daea8bd4..0ea7fb32 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Expect breaking changes! This SDK is currently being distributed using [jitpack](https://jitpack.io/) +[![](https://jitpack.io/v/uport-project/uport-android-sdk.svg)](https://jitpack.io/#uport-project/uport-android-sdk) + In your main `build.gradle` file, add: ```groovy @@ -23,7 +25,7 @@ allprojects { In your application `build.gradle`: ```groovy -def uport_sdk_version = "v0.1.0" +def uport_sdk_version = "v0.1.1" dependencies { ... // core SDK @@ -33,8 +35,6 @@ dependencies { ### Usage -This preview version requires that the SDK be configured with a functional `IFuelTokenProvider` -There is a `FuelTokenProvider` implementation provided in the `fuelingservice` library ##### Configure uPort in your Application class @@ -133,6 +133,10 @@ but that may be removed when pure kotlin implementations of the required cryptog ### Changelog +* 0.1.1 + * add option to import seeds phrases as account + * bugfix: default account is updated on first creation + * 0.1.0 * default account type is `KeyPair` * updated kethereum to 0.53 , some APIs have changed to extension functions diff --git a/build.gradle b/build.gradle index 797035e3..0d16aee6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { ext { kotlin_version = '1.2.51' - android_tools_version = "3.2.0-beta02" + android_tools_version = "3.3.0-alpha03" build_tools_version = "27.0.3" @@ -18,7 +18,8 @@ buildscript { play_services_version = "15.0.0" espresso_version = "3.0.1" junit_version = "4.12" - mockito_version = "2.12.0" + mockito_version = "2.18.3" + mockito_kotlin_version = "1.5.0" coroutines_version = "0.22.5" moshi_version = "1.6.0" @@ -30,7 +31,7 @@ buildscript { khex_version = "0.5" uport_signer_version = "0.2.0" - uport_sdk_version = "0.1.0" + uport_sdk_version = "0.1.1" } repositories { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 122de41d..d51c81ab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip 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..c5ef5ac3 --- /dev/null +++ b/identity/src/androidTest/java/KPAccountCreatorTest.kt @@ -0,0 +1,49 @@ +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.Before +import org.junit.Test + +class KPAccountCreatorTest { + + private 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()) + } + } + + @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 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 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..1b384e40 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,26 @@ package me.uport.sdk.identity import android.content.Context import com.uport.sdk.signer.UportHDSigner import com.uport.sdk.signer.encryption.KeyProtection - -class KPAccountCreator(private val context: 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) +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.launch + +class KPAccountCreator(private val appContext: Context) : AccountCreator { + + private fun createOrImportAccount(networkId: String, phrase: String?, callback: AccountCreatorCallback) { + launch { + val signer = UportHDSigner() + try { + val (handle, _) = if (phrase.isNullOrBlank()) { + signer.createHDSeed(appContext, KeyProtection.Level.SIMPLE) + } else { + signer.importHDSeed(appContext, KeyProtection.Level.SIMPLE, phrase!!) } - - val acc = Account( - rootAddress, + val (deviceAddress, _) = signer.computeAddressForPath(appContext, + handle, + Account.GENERIC_DEVICE_KEY_DERIVATION_PATH, + "") + val account = Account( + handle, deviceAddress, networkId, deviceAddress, @@ -33,9 +32,20 @@ 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) } } + } } + 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) diff --git a/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt new file mode 100644 index 00000000..79a562e1 --- /dev/null +++ b/sdk/src/androidTest/java/me/uport/sdk/UportTest.kt @@ -0,0 +1,68 @@ +package me.uport.sdk + +import android.os.Looper +import android.support.test.InstrumentationRegistry +import kotlinx.coroutines.experimental.runBlocking +import me.uport.sdk.core.Networks +import me.uport.sdk.identity.Account +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class UportTest { + + @Before + fun run_before_every_test() { + val config = Uport.Configuration() + .setApplicationContext(InstrumentationRegistry.getTargetContext()) + Uport.initialize(config) + } + + @Test + fun default_account_gets_updated() { + + val tested = Uport + + tested.defaultAccount = null + + runBlocking { + val acc = tested.createAccount(Networks.rinkeby) + assertNotNull(acc) + assertNotEquals(Account.blank, acc) + + assertNotNull(tested.defaultAccount) + } + } + + @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) + } + + @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 diff --git a/sdk/src/main/java/me/uport/sdk/Uport.kt b/sdk/src/main/java/me/uport/sdk/Uport.kt index 5cdd18a2..a03a12b6 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 { @@ -59,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) } /** @@ -71,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 { @@ -90,29 +85,32 @@ object Uport { * * To really create a new account, call [deleteAccount] first. */ - 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 } - 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 = if (seedPhrase.isNullOrBlank()) { + creator.createAccount(networkId) + } else { + creator.importAccount(networkId, seedPhrase!!) + } + prefs.edit().putString(DEFAULT_ACCOUNT, acc.toJson()).apply() + defaultAccount = defaultAccount ?: acc + + launch(UI) { completion(null, acc) } + } catch (err: Exception) { + launch(UI) { completion(err, Account.blank) } } - - val serialized = acc.toJson() - prefs.edit().putString(DEFAULT_ACCOUNT, serialized).apply() - - Handler(getMainLooper()).post { completion(err, acc) } } }