[#1095] PersistableWallet endpoint API
* [#1095] PersistableWallet endpoint API * Fix Ktlint warnings * Add PersistableWallet 1_2 migration test * Changelog update
This commit is contained in:
parent
1032f88035
commit
6bf7b12982
|
@ -6,6 +6,11 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- `PersistableWallet` API provides a new `endpoint` parameter of type `LightWalletEndpoint`, which could be used for
|
||||
the Lightwalletd server customization. The new parameter is part of PersistableWallet persistence. The SDK handles
|
||||
the persistence migration internally.
|
||||
|
||||
## [2.0.0] - 2023-09-25
|
||||
|
||||
## [2.0.0-rc.4] - 2023-09-22
|
||||
|
|
|
@ -25,8 +25,10 @@ import cash.z.ecc.android.sdk.model.WalletBalance
|
|||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import cash.z.ecc.android.sdk.model.send
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
|
@ -143,10 +145,12 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
val application = getApplication<Application>()
|
||||
|
||||
viewModelScope.launch {
|
||||
val network = ZcashNetwork.fromResources(application)
|
||||
val newWallet = PersistableWallet.new(
|
||||
application,
|
||||
ZcashNetwork.fromResources(application),
|
||||
WalletInitMode.NewWallet
|
||||
application = application,
|
||||
zcashNetwork = network,
|
||||
endpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
walletInitMode = WalletInitMode.NewWallet
|
||||
)
|
||||
persistWallet(newWallet)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import cash.z.ecc.android.sdk.fixture.WalletFixture
|
|||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
|
||||
@Preview(name = "Seed")
|
||||
@Composable
|
||||
|
@ -75,10 +77,11 @@ private fun ConfigureSeedMainContent(
|
|||
Button(
|
||||
onClick = {
|
||||
val newWallet = PersistableWallet(
|
||||
zcashNetwork,
|
||||
WalletFixture.Alice.getBirthday(zcashNetwork),
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
WalletInitMode.RestoreWallet
|
||||
network = zcashNetwork,
|
||||
endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork),
|
||||
birthday = WalletFixture.Alice.getBirthday(zcashNetwork),
|
||||
seedPhrase = SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
walletInitMode = WalletInitMode.RestoreWallet
|
||||
)
|
||||
onExistingWallet(newWallet)
|
||||
}
|
||||
|
@ -88,10 +91,11 @@ private fun ConfigureSeedMainContent(
|
|||
Button(
|
||||
onClick = {
|
||||
val newWallet = PersistableWallet(
|
||||
zcashNetwork,
|
||||
WalletFixture.Ben.getBirthday(zcashNetwork),
|
||||
SeedPhrase.new(WalletFixture.Ben.seedPhrase),
|
||||
WalletInitMode.RestoreWallet
|
||||
network = zcashNetwork,
|
||||
endpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork),
|
||||
birthday = WalletFixture.Ben.getBirthday(zcashNetwork),
|
||||
seedPhrase = SeedPhrase.new(WalletFixture.Ben.seedPhrase),
|
||||
walletInitMode = WalletInitMode.RestoreWallet
|
||||
)
|
||||
onExistingWallet(newWallet)
|
||||
}
|
||||
|
|
|
@ -4,12 +4,16 @@ import cash.z.ecc.android.sdk.WalletInitMode
|
|||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.Testnet
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
|
||||
object PersistableWalletFixture {
|
||||
|
||||
val NETWORK = ZcashNetwork.Testnet
|
||||
|
||||
val ENDPOINT = LightWalletEndpoint.Testnet
|
||||
|
||||
// These came from the mainnet 1500000.json file
|
||||
@Suppress("MagicNumber")
|
||||
val BIRTHDAY = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
|
||||
|
@ -20,8 +24,17 @@ object PersistableWalletFixture {
|
|||
|
||||
fun new(
|
||||
network: ZcashNetwork = NETWORK,
|
||||
endpoint: LightWalletEndpoint = ENDPOINT,
|
||||
birthday: BlockHeight = BIRTHDAY,
|
||||
seedPhrase: SeedPhrase = SEED_PHRASE,
|
||||
walletInitMode: WalletInitMode = WALLET_INIT_MODE
|
||||
) = PersistableWallet(network, birthday, seedPhrase, walletInitMode)
|
||||
) = PersistableWallet(network, endpoint, birthday, seedPhrase, walletInitMode)
|
||||
|
||||
fun persistVersionOne() = PersistableWallet.toCustomJson(
|
||||
version = PersistableWallet.VERSION_1,
|
||||
network = NETWORK,
|
||||
endpoint = null,
|
||||
birthday = BIRTHDAY,
|
||||
seed = SEED_PHRASE
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@ import androidx.test.filters.SmallTest
|
|||
import cash.z.ecc.android.sdk.count
|
||||
import cash.z.ecc.android.sdk.fixture.PersistableWalletFixture
|
||||
import cash.z.ecc.android.sdk.fixture.SeedPhraseFixture
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PersistableWalletTest {
|
||||
@Test
|
||||
|
@ -16,18 +17,30 @@ class PersistableWalletTest {
|
|||
val persistableWallet = PersistableWalletFixture.new()
|
||||
|
||||
val jsonObject = persistableWallet.toJson()
|
||||
assertEquals(4, jsonObject.keys().count())
|
||||
assertEquals(7, jsonObject.keys().count())
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_VERSION))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_NETWORK_ID))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_ENDPOINT_HOST))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_ENDPOINT_PORT))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_ENDPOINT_IS_SECURE))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_SEED_PHRASE))
|
||||
assertTrue(jsonObject.has(PersistableWallet.KEY_BIRTHDAY))
|
||||
|
||||
assertEquals(1, jsonObject.getInt(PersistableWallet.KEY_VERSION))
|
||||
assertEquals(PersistableWallet.VERSION_2, jsonObject.getInt(PersistableWallet.KEY_VERSION))
|
||||
assertEquals(ZcashNetwork.Testnet.id, jsonObject.getInt(PersistableWallet.KEY_NETWORK_ID))
|
||||
assertEquals(
|
||||
PersistableWalletFixture.SEED_PHRASE.joinToString(),
|
||||
jsonObject.getString(PersistableWallet.KEY_SEED_PHRASE)
|
||||
)
|
||||
assertEquals(PersistableWalletFixture.ENDPOINT.host, jsonObject.getString(PersistableWallet.KEY_ENDPOINT_HOST))
|
||||
assertEquals(PersistableWalletFixture.ENDPOINT.port, jsonObject.getInt(PersistableWallet.KEY_ENDPOINT_PORT))
|
||||
assertEquals(
|
||||
PersistableWalletFixture.ENDPOINT.isSecure,
|
||||
jsonObject.getBoolean(
|
||||
PersistableWallet
|
||||
.KEY_ENDPOINT_IS_SECURE
|
||||
)
|
||||
)
|
||||
|
||||
// Birthday serialization is tested in a separate file
|
||||
}
|
||||
|
@ -50,4 +63,69 @@ class PersistableWalletTest {
|
|||
|
||||
assertFalse(actual.contains(SeedPhraseFixture.SEED_PHRASE))
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun get_seed_phrase_test() {
|
||||
val json = PersistableWalletFixture.new().toJson()
|
||||
assertEquals(
|
||||
PersistableWalletFixture.SEED_PHRASE.joinToString(),
|
||||
PersistableWallet.getSeedPhrase(json)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun get_birthday_test() {
|
||||
val json = PersistableWalletFixture.new().toJson()
|
||||
assertEquals(
|
||||
PersistableWalletFixture.SEED_PHRASE.joinToString(),
|
||||
PersistableWallet.getSeedPhrase(json)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun get_network_test() {
|
||||
val json = PersistableWalletFixture.new().toJson()
|
||||
assertEquals(
|
||||
PersistableWalletFixture.BIRTHDAY.value,
|
||||
PersistableWallet.getBirthday(json, PersistableWalletFixture.NETWORK)!!.value
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun get_endpoint_test() {
|
||||
val json = PersistableWalletFixture.new().toJson()
|
||||
assertEquals(
|
||||
PersistableWalletFixture.ENDPOINT.host,
|
||||
PersistableWallet.getEndpoint(json).host
|
||||
)
|
||||
assertEquals(
|
||||
PersistableWalletFixture.ENDPOINT.port,
|
||||
PersistableWallet.getEndpoint(json).port
|
||||
)
|
||||
assertEquals(
|
||||
PersistableWalletFixture.ENDPOINT.isSecure,
|
||||
PersistableWallet.getEndpoint(json).isSecure
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun version_1_2_migration_test() {
|
||||
val json = PersistableWalletFixture.persistVersionOne()
|
||||
assertEquals(
|
||||
PersistableWallet.VERSION_1,
|
||||
PersistableWallet.getVersion(json)
|
||||
)
|
||||
|
||||
// Wallet version one deserialized by code supporting version two
|
||||
val persistableWallet = PersistableWallet.from(json)
|
||||
assertEquals(
|
||||
LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
|
||||
persistableWallet.endpoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import android.content.Context
|
|||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -77,7 +75,7 @@ class WalletCoordinator(
|
|||
val closeableSynchronizer = Synchronizer.new(
|
||||
context = context,
|
||||
zcashNetwork = persistableWallet.network,
|
||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
|
||||
lightWalletEndpoint = persistableWallet.endpoint,
|
||||
birthday = persistableWallet.birthday,
|
||||
seed = persistableWallet.seedPhrase.toByteArray(),
|
||||
walletInitMode = persistableWallet.walletInitMode,
|
||||
|
|
|
@ -4,15 +4,28 @@ import android.app.Application
|
|||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toEntropy
|
||||
import cash.z.ecc.android.sdk.WalletInitMode
|
||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Represents everything needed to save and restore a wallet.
|
||||
*
|
||||
* @param network the network in which the wallet operates
|
||||
* @param endpoint the endpoint with witch the wallet communicates
|
||||
* @param birthday the birthday of the wallet
|
||||
* @param seedPhrase the seed phrase of the wallet
|
||||
* @param walletInitMode required parameter with one of [WalletInitMode] values. Use [WalletInitMode.NewWallet] when
|
||||
* starting synchronizer for a newly created wallet. Or use [WalletInitMode.RestoreWallet] when restoring an existing
|
||||
* wallet that was created at some point in the past. Or use the last [WalletInitMode.ExistingWallet] type for a
|
||||
* wallet which is already initialized and needs follow-up block synchronization. Note that this parameter is NOT
|
||||
* persisted along with the rest of persistable wallet data.
|
||||
*/
|
||||
data class PersistableWallet(
|
||||
val network: ZcashNetwork,
|
||||
val endpoint: LightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||
val birthday: BlockHeight?,
|
||||
val seedPhrase: SeedPhrase,
|
||||
val walletInitMode: WalletInitMode
|
||||
|
@ -27,8 +40,11 @@ data class PersistableWallet(
|
|||
// Note: We're using a hand-crafted serializer so that we're less likely to have accidental
|
||||
// breakage from reflection or annotation based methods, and so that we can carefully manage versioning.
|
||||
fun toJson() = JSONObject().apply {
|
||||
put(KEY_VERSION, VERSION_1)
|
||||
put(KEY_VERSION, VERSION_2)
|
||||
put(KEY_NETWORK_ID, network.id)
|
||||
put(KEY_ENDPOINT_HOST, endpoint.host)
|
||||
put(KEY_ENDPOINT_PORT, endpoint.port)
|
||||
put(KEY_ENDPOINT_IS_SECURE, endpoint.isSecure)
|
||||
birthday?.let {
|
||||
put(KEY_BIRTHDAY, it.value)
|
||||
}
|
||||
|
@ -39,51 +55,92 @@ data class PersistableWallet(
|
|||
override fun toString() = "PersistableWallet"
|
||||
|
||||
companion object {
|
||||
private const val VERSION_1 = 1
|
||||
internal const val VERSION_1 = 1
|
||||
internal const val VERSION_2 = 2
|
||||
|
||||
internal const val KEY_VERSION = "v"
|
||||
internal const val KEY_NETWORK_ID = "network_ID"
|
||||
internal const val KEY_ENDPOINT_HOST = "endpoint_host"
|
||||
internal const val KEY_ENDPOINT_PORT = "key_endpoint_port"
|
||||
internal const val KEY_ENDPOINT_IS_SECURE = "key_endpoint_is_secure"
|
||||
internal const val KEY_BIRTHDAY = "birthday"
|
||||
internal const val KEY_SEED_PHRASE = "seed_phrase"
|
||||
|
||||
// Note: This is not the ideal way to hold such a value. But we also want to avoid persisting the wallet
|
||||
// initialization mode with the persistable wallet.
|
||||
// Note: [walletInitMode] is excluded from the serialization to avoid persisting the wallet initialization mode
|
||||
// with the persistable wallet.
|
||||
private var _walletInitMode: WalletInitMode = WalletInitMode.ExistingWallet
|
||||
|
||||
fun from(jsonObject: JSONObject): PersistableWallet {
|
||||
when (val version = jsonObject.getInt(KEY_VERSION)) {
|
||||
VERSION_1 -> {
|
||||
val network = run {
|
||||
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
|
||||
ZcashNetwork.from(networkId)
|
||||
}
|
||||
val birthday = if (jsonObject.has(KEY_BIRTHDAY)) {
|
||||
val birthdayBlockHeightLong = jsonObject.getLong(KEY_BIRTHDAY)
|
||||
BlockHeight.new(network, birthdayBlockHeightLong)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
|
||||
// Common parameters
|
||||
val network = getNetwork(jsonObject)
|
||||
val birthday = getBirthday(jsonObject, network)
|
||||
val seedPhrase = getSeedPhrase(jsonObject)
|
||||
// From version 2
|
||||
val endpoint: LightWalletEndpoint
|
||||
|
||||
return PersistableWallet(
|
||||
network = network,
|
||||
birthday = birthday,
|
||||
seedPhrase = SeedPhrase.new(seedPhrase),
|
||||
walletInitMode = _walletInitMode
|
||||
)
|
||||
when (val version = getVersion(jsonObject)) {
|
||||
VERSION_1 -> {
|
||||
endpoint = LightWalletEndpoint.defaultForNetwork(network)
|
||||
}
|
||||
VERSION_2 -> {
|
||||
endpoint = getEndpoint(jsonObject)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Unsupported version $version")
|
||||
}
|
||||
}
|
||||
|
||||
return PersistableWallet(
|
||||
network = network,
|
||||
endpoint = endpoint,
|
||||
birthday = birthday,
|
||||
seedPhrase = SeedPhrase.new(seedPhrase),
|
||||
walletInitMode = _walletInitMode
|
||||
)
|
||||
}
|
||||
|
||||
internal fun getVersion(jsonObject: JSONObject): Int {
|
||||
return jsonObject.getInt(KEY_VERSION)
|
||||
}
|
||||
internal fun getSeedPhrase(jsonObject: JSONObject): String {
|
||||
return jsonObject.getString(KEY_SEED_PHRASE)
|
||||
}
|
||||
internal fun getNetwork(jsonObject: JSONObject): ZcashNetwork {
|
||||
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
|
||||
return ZcashNetwork.from(networkId)
|
||||
}
|
||||
internal fun getBirthday(jsonObject: JSONObject, network: ZcashNetwork): BlockHeight? {
|
||||
return if (jsonObject.has(KEY_BIRTHDAY)) {
|
||||
val birthdayBlockHeightLong = jsonObject.getLong(KEY_BIRTHDAY)
|
||||
BlockHeight.new(network, birthdayBlockHeightLong)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
internal fun getEndpoint(jsonObject: JSONObject): LightWalletEndpoint {
|
||||
return jsonObject.run {
|
||||
val host = getString(KEY_ENDPOINT_HOST)
|
||||
val port = getInt(KEY_ENDPOINT_PORT)
|
||||
val isSecure = getBoolean(KEY_ENDPOINT_IS_SECURE)
|
||||
LightWalletEndpoint(host, port, isSecure)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A new PersistableWallet with a random seed phrase.
|
||||
*
|
||||
* @param zcashNetwork the network in which the wallet operates
|
||||
* @param endpoint the endpoint with witch the wallet communicates
|
||||
* @param walletInitMode required parameter with one of [WalletInitMode] values. Use [WalletInitMode.NewWallet]
|
||||
* when starting synchronizer for a newly created wallet. Or use [WalletInitMode.RestoreWallet] when
|
||||
* restoring an existing wallet that was created at some point in the past. Or use the last [WalletInitMode
|
||||
* .ExistingWallet] type for a wallet which is already initialized and needs follow-up block synchronization.
|
||||
* Note that this parameter is NOT persisted along with the rest of persistable wallet data.
|
||||
*/
|
||||
suspend fun new(
|
||||
application: Application,
|
||||
zcashNetwork: ZcashNetwork,
|
||||
endpoint: LightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(zcashNetwork),
|
||||
walletInitMode: WalletInitMode
|
||||
): PersistableWallet {
|
||||
val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
|
||||
|
@ -92,11 +149,38 @@ data class PersistableWallet(
|
|||
|
||||
return PersistableWallet(
|
||||
zcashNetwork,
|
||||
endpoint,
|
||||
birthday,
|
||||
seedPhrase,
|
||||
walletInitMode
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this function is internal and allowed only for testing purposes.
|
||||
*
|
||||
* @return Wallet serialized to JSON format, suitable for long-term encrypted storage.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
internal fun toCustomJson(
|
||||
version: Int,
|
||||
network: ZcashNetwork,
|
||||
endpoint: LightWalletEndpoint?,
|
||||
birthday: BlockHeight?,
|
||||
seed: SeedPhrase
|
||||
) = JSONObject().apply {
|
||||
put(KEY_VERSION, version)
|
||||
put(KEY_NETWORK_ID, network.id)
|
||||
endpoint?.let {
|
||||
put(KEY_ENDPOINT_HOST, it.host)
|
||||
put(KEY_ENDPOINT_PORT, it.port)
|
||||
put(KEY_ENDPOINT_IS_SECURE, it.isSecure)
|
||||
}
|
||||
birthday?.let {
|
||||
put(KEY_BIRTHDAY, it.value)
|
||||
}
|
||||
put(KEY_SEED_PHRASE, seed.joinToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue