Note that this change modifies the on-disk representation of the wallet. Anyone who has an earlier build install will see a runtime crash. This is expected, as we are not supporting data migrations at this stage of development. The solution is to uninstall/reinstall the app.
This commit is contained in:
parent
072d73e99c
commit
f9afd2e5f9
|
@ -132,7 +132,7 @@ PLAY_CORE_KTX_VERSION=1.8.1
|
||||||
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||||
ZCASH_BIP39_VERSION=1.0.2
|
ZCASH_BIP39_VERSION=1.0.2
|
||||||
# TODO [#279]: Revert to stable SDK before app release
|
# TODO [#279]: Revert to stable SDK before app release
|
||||||
ZCASH_SDK_VERSION=1.7.0-beta01
|
ZCASH_SDK_VERSION=1.8.0-beta01-SNAPSHOT
|
||||||
ZXING_VERSION=3.5.0
|
ZXING_VERSION=3.5.0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
package cash.z.ecc.sdk.model
|
|
||||||
|
|
||||||
import androidx.test.filters.SmallTest
|
|
||||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
|
||||||
import cash.z.ecc.sdk.fixture.WalletBirthdayFixture
|
|
||||||
import cash.z.ecc.sdk.test.count
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class WalletBirthdayTest {
|
|
||||||
@Test
|
|
||||||
@SmallTest
|
|
||||||
fun serialize() {
|
|
||||||
val walletBirthday = WalletBirthdayFixture.new()
|
|
||||||
|
|
||||||
val jsonObject = walletBirthday.toJson()
|
|
||||||
assertEquals(5, jsonObject.keys().count())
|
|
||||||
assertTrue(jsonObject.has(WalletBirthday.KEY_VERSION))
|
|
||||||
assertTrue(jsonObject.has(WalletBirthday.KEY_HEIGHT))
|
|
||||||
assertTrue(jsonObject.has(WalletBirthday.KEY_HASH))
|
|
||||||
assertTrue(jsonObject.has(WalletBirthday.KEY_EPOCH_SECONDS))
|
|
||||||
assertTrue(jsonObject.has(WalletBirthday.KEY_TREE))
|
|
||||||
|
|
||||||
assertEquals(1, jsonObject.getInt(WalletBirthday.KEY_VERSION))
|
|
||||||
assertEquals(WalletBirthdayFixture.HEIGHT, jsonObject.getInt(WalletBirthday.KEY_HEIGHT))
|
|
||||||
assertEquals(WalletBirthdayFixture.HASH, jsonObject.getString(WalletBirthday.KEY_HASH))
|
|
||||||
assertEquals(WalletBirthdayFixture.EPOCH_SECONDS, jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS))
|
|
||||||
assertEquals(WalletBirthdayFixture.TREE, jsonObject.getString(WalletBirthday.KEY_TREE))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SmallTest
|
|
||||||
fun epoch_seconds_as_long_that_would_overflow_int() {
|
|
||||||
val walletBirthday = WalletBirthdayFixture.new(time = Long.MAX_VALUE)
|
|
||||||
|
|
||||||
val jsonObject = walletBirthday.toJson()
|
|
||||||
|
|
||||||
assertEquals(Long.MAX_VALUE, jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS))
|
|
||||||
|
|
||||||
WalletBirthday.from(jsonObject).also {
|
|
||||||
assertEquals(Long.MAX_VALUE, it.time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@SmallTest
|
|
||||||
fun round_trip() {
|
|
||||||
val walletBirthday = WalletBirthdayFixture.new()
|
|
||||||
|
|
||||||
val deserialized = WalletBirthday.from(walletBirthday.toJson())
|
|
||||||
|
|
||||||
assertEquals(walletBirthday, deserialized)
|
|
||||||
assertFalse(walletBirthday === deserialized)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
package cash.z.ecc.sdk.fixture
|
package cash.z.ecc.sdk.fixture
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||||
import cash.z.ecc.sdk.model.PersistableWallet
|
import cash.z.ecc.sdk.model.PersistableWallet
|
||||||
import cash.z.ecc.sdk.model.SeedPhrase
|
import cash.z.ecc.sdk.model.SeedPhrase
|
||||||
|
@ -9,13 +9,15 @@ object PersistableWalletFixture {
|
||||||
|
|
||||||
val NETWORK = ZcashNetwork.Testnet
|
val NETWORK = ZcashNetwork.Testnet
|
||||||
|
|
||||||
val BIRTHDAY = WalletBirthdayFixture.new()
|
// These came from the mainnet 1500000.json file
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
val BIRTHDAY = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
|
||||||
|
|
||||||
val SEED_PHRASE = SeedPhraseFixture.new()
|
val SEED_PHRASE = SeedPhraseFixture.new()
|
||||||
|
|
||||||
fun new(
|
fun new(
|
||||||
network: ZcashNetwork = NETWORK,
|
network: ZcashNetwork = NETWORK,
|
||||||
birthday: WalletBirthday = BIRTHDAY,
|
birthday: BlockHeight = BIRTHDAY,
|
||||||
seedPhrase: SeedPhrase = SEED_PHRASE
|
seedPhrase: SeedPhrase = SEED_PHRASE
|
||||||
) = PersistableWallet(network, birthday, seedPhrase)
|
) = PersistableWallet(network, birthday, seedPhrase)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
package cash.z.ecc.sdk.fixture
|
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
|
||||||
|
|
||||||
object WalletBirthdayFixture {
|
|
||||||
|
|
||||||
const val HEIGHT = 1500000
|
|
||||||
const val HASH = "00047a34c61409682f44640af9352023ad92f69b827d0f2b288f152ebea50f46"
|
|
||||||
const val EPOCH_SECONDS = 1627076501L
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
const val TREE = "01172b95f271c6af8f68388f08c8ef970db8ec8d8d61204ecb7b2bb2c38262b92d0010016284585a6c85dadfef27ff33f1403926b4bb391de92e8be797e4280cc4ca2971000001a1ff388639379c0120782b3929bd8871af797be4b651f694aa961bad65a9c12400000001d806c98bda9653d5ae22757eed750871e16e0fb657f52c3d771a4411668e84330001260f6e9fac0922f98d58afbcc3f391ac19d5d944081466929a33b99df19c0e6a0000013d2fd009bf8a22d68f720eac19c411c99014ed9c5f85d5942e15d1fc039e28680001f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
|
||||||
|
|
||||||
fun new(
|
|
||||||
height: Int = HEIGHT,
|
|
||||||
hash: String = HASH,
|
|
||||||
time: Long = EPOCH_SECONDS,
|
|
||||||
tree: String = TREE
|
|
||||||
) = WalletBirthday(height = height, hash = hash, time = time, tree = tree)
|
|
||||||
}
|
|
|
@ -2,28 +2,42 @@ package cash.z.ecc.sdk.model
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||||
|
|
||||||
fun CompactBlockProcessor.ProcessorInfo.downloadProgress() = if (lastDownloadRange.isEmpty()) {
|
fun CompactBlockProcessor.ProcessorInfo.downloadProgress(): PercentDecimal {
|
||||||
PercentDecimal.ONE_HUNDRED_PERCENT
|
val lastDownloadRangeSnapshot = lastDownloadRange
|
||||||
} else {
|
val lastDownloadedHeightSnapshot = lastDownloadedHeight
|
||||||
val numerator = (lastDownloadedHeight - lastDownloadRange.first + 1)
|
|
||||||
.toFloat()
|
|
||||||
.coerceAtLeast(PercentDecimal.MIN)
|
|
||||||
val denominator = (lastDownloadRange.last - lastDownloadRange.first + 1).toFloat()
|
|
||||||
|
|
||||||
val progress = (numerator / denominator).coerceAtMost(PercentDecimal.MAX)
|
return if (lastDownloadRangeSnapshot?.isEmpty() != false || lastDownloadedHeightSnapshot == null) {
|
||||||
|
PercentDecimal.ONE_HUNDRED_PERCENT
|
||||||
|
} else {
|
||||||
|
val numerator = (lastDownloadedHeightSnapshot.value - lastDownloadRangeSnapshot.start.value + 1)
|
||||||
|
.toFloat()
|
||||||
|
.coerceAtLeast(PercentDecimal.MIN)
|
||||||
|
val denominator = (lastDownloadRangeSnapshot.endInclusive.value - lastDownloadRangeSnapshot.start.value + 1)
|
||||||
|
.toFloat()
|
||||||
|
|
||||||
PercentDecimal(progress)
|
val progress = (numerator / denominator).coerceAtMost(PercentDecimal.MAX)
|
||||||
|
|
||||||
|
PercentDecimal(progress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CompactBlockProcessor.ProcessorInfo.scanProgress() = if (lastScanRange.isEmpty()) {
|
fun CompactBlockProcessor.ProcessorInfo.scanProgress(): PercentDecimal {
|
||||||
PercentDecimal.ONE_HUNDRED_PERCENT
|
val lastScanRangeSnapshot = lastScanRange
|
||||||
} else {
|
val lastScannedHeightSnapshot = lastScannedHeight
|
||||||
val numerator = (lastScannedHeight - lastScanRange.first + 1).toFloat().coerceAtLeast(PercentDecimal.MIN)
|
|
||||||
val demonimator = (lastScanRange.last - lastScanRange.first + 1).toFloat()
|
|
||||||
|
|
||||||
val progress = (numerator / demonimator).coerceAtMost(PercentDecimal.MAX)
|
return if (lastScanRangeSnapshot?.isEmpty() != false || lastScannedHeightSnapshot == null) {
|
||||||
|
PercentDecimal.ONE_HUNDRED_PERCENT
|
||||||
|
} else {
|
||||||
|
val numerator = (lastScannedHeightSnapshot.value - lastScanRangeSnapshot.start.value + 1)
|
||||||
|
.toFloat()
|
||||||
|
.coerceAtLeast(PercentDecimal.MIN)
|
||||||
|
val demonimator = (lastScanRangeSnapshot.endInclusive.value - lastScanRangeSnapshot.start.value + 1)
|
||||||
|
.toFloat()
|
||||||
|
|
||||||
PercentDecimal(progress)
|
val progress = (numerator / demonimator).coerceAtMost(PercentDecimal.MAX)
|
||||||
|
|
||||||
|
PercentDecimal(progress)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are estimates
|
// These are estimates
|
||||||
|
|
|
@ -3,8 +3,7 @@ package cash.z.ecc.sdk.model
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toEntropy
|
import cash.z.ecc.android.bip39.toEntropy
|
||||||
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
|
||||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||||
import cash.z.ecc.sdk.type.fromResources
|
import cash.z.ecc.sdk.type.fromResources
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -16,7 +15,7 @@ import org.json.JSONObject
|
||||||
*/
|
*/
|
||||||
data class PersistableWallet(
|
data class PersistableWallet(
|
||||||
val network: ZcashNetwork,
|
val network: ZcashNetwork,
|
||||||
val birthday: WalletBirthday?,
|
val birthday: BlockHeight?,
|
||||||
val seedPhrase: SeedPhrase
|
val seedPhrase: SeedPhrase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ data class PersistableWallet(
|
||||||
put(KEY_VERSION, VERSION_1)
|
put(KEY_VERSION, VERSION_1)
|
||||||
put(KEY_NETWORK_ID, network.id)
|
put(KEY_NETWORK_ID, network.id)
|
||||||
birthday?.let {
|
birthday?.let {
|
||||||
put(KEY_BIRTHDAY, it.toJson())
|
put(KEY_BIRTHDAY, it.value)
|
||||||
}
|
}
|
||||||
put(KEY_SEED_PHRASE, seedPhrase.joinToString())
|
put(KEY_SEED_PHRASE, seedPhrase.joinToString())
|
||||||
}
|
}
|
||||||
|
@ -48,15 +47,19 @@ data class PersistableWallet(
|
||||||
fun from(jsonObject: JSONObject): PersistableWallet {
|
fun from(jsonObject: JSONObject): PersistableWallet {
|
||||||
when (val version = jsonObject.getInt(KEY_VERSION)) {
|
when (val version = jsonObject.getInt(KEY_VERSION)) {
|
||||||
VERSION_1 -> {
|
VERSION_1 -> {
|
||||||
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
|
val network = run {
|
||||||
|
val networkId = jsonObject.getInt(KEY_NETWORK_ID)
|
||||||
|
ZcashNetwork.from(networkId)
|
||||||
|
}
|
||||||
val birthday = if (jsonObject.has(KEY_BIRTHDAY)) {
|
val birthday = if (jsonObject.has(KEY_BIRTHDAY)) {
|
||||||
WalletBirthday.from(jsonObject.getJSONObject(KEY_BIRTHDAY))
|
val birthdayBlockHeightLong = jsonObject.getLong(KEY_BIRTHDAY)
|
||||||
|
BlockHeight.new(network, birthdayBlockHeightLong)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
|
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
|
||||||
|
|
||||||
return PersistableWallet(ZcashNetwork.from(networkId), birthday, SeedPhrase.new(seedPhrase))
|
return PersistableWallet(network, birthday, SeedPhrase.new(seedPhrase))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw IllegalArgumentException("Unsupported version $version")
|
throw IllegalArgumentException("Unsupported version $version")
|
||||||
|
@ -69,11 +72,11 @@ data class PersistableWallet(
|
||||||
*/
|
*/
|
||||||
suspend fun new(application: Application): PersistableWallet {
|
suspend fun new(application: Application): PersistableWallet {
|
||||||
val zcashNetwork = ZcashNetwork.fromResources(application)
|
val zcashNetwork = ZcashNetwork.fromResources(application)
|
||||||
val walletBirthday = WalletBirthdayTool.loadNearest(application, zcashNetwork)
|
val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
|
||||||
|
|
||||||
val seedPhrase = newSeedPhrase()
|
val seedPhrase = newSeedPhrase()
|
||||||
|
|
||||||
return PersistableWallet(zcashNetwork, walletBirthday, seedPhrase)
|
return PersistableWallet(zcashNetwork, birthday, seedPhrase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package cash.z.ecc.sdk.model
|
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.type.WalletBirthday
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
internal val WalletBirthday.Companion.VERSION_1
|
|
||||||
get() = 1
|
|
||||||
internal val WalletBirthday.Companion.KEY_VERSION
|
|
||||||
get() = "version"
|
|
||||||
internal val WalletBirthday.Companion.KEY_HEIGHT
|
|
||||||
get() = "height"
|
|
||||||
internal val WalletBirthday.Companion.KEY_HASH
|
|
||||||
get() = "hash"
|
|
||||||
internal val WalletBirthday.Companion.KEY_EPOCH_SECONDS
|
|
||||||
get() = "epoch_seconds"
|
|
||||||
internal val WalletBirthday.Companion.KEY_TREE
|
|
||||||
get() = "tree"
|
|
||||||
|
|
||||||
fun WalletBirthday.Companion.from(jsonString: String) = from(JSONObject(jsonString))
|
|
||||||
|
|
||||||
fun WalletBirthday.Companion.from(jsonObject: JSONObject): WalletBirthday {
|
|
||||||
when (val version = jsonObject.getInt(WalletBirthday.KEY_VERSION)) {
|
|
||||||
WalletBirthday.VERSION_1 -> {
|
|
||||||
val height = jsonObject.getInt(WalletBirthday.KEY_HEIGHT)
|
|
||||||
val hash = jsonObject.getString(WalletBirthday.KEY_HASH)
|
|
||||||
val epochSeconds = jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS)
|
|
||||||
val tree = jsonObject.getString(WalletBirthday.KEY_TREE)
|
|
||||||
|
|
||||||
return WalletBirthday(height, hash, epochSeconds, tree)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw IllegalArgumentException("Unsupported version $version")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun WalletBirthday.toJson() = JSONObject().apply {
|
|
||||||
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)
|
|
||||||
put(WalletBirthday.KEY_HEIGHT, height)
|
|
||||||
put(WalletBirthday.KEY_HASH, hash)
|
|
||||||
put(WalletBirthday.KEY_EPOCH_SECONDS, time)
|
|
||||||
put(WalletBirthday.KEY_TREE, tree)
|
|
||||||
}
|
|
|
@ -136,13 +136,15 @@ class WalletCoordinator(context: Context) {
|
||||||
* In order for a rescan to occur, the synchronizer must be loaded already
|
* In order for a rescan to occur, the synchronizer must be loaded already
|
||||||
* which would happen if the UI is collecting it.
|
* which would happen if the UI is collecting it.
|
||||||
*
|
*
|
||||||
* @return True if the rewind was performed and false if the wipe was not performed.
|
* @return True if the rescan was performed and false if the rescan was not performed.
|
||||||
*/
|
*/
|
||||||
suspend fun rescanBlockchain(): Boolean {
|
suspend fun rescanBlockchain(): Boolean {
|
||||||
synchronizerMutex.withLock {
|
synchronizerMutex.withLock {
|
||||||
synchronizer.value?.let {
|
synchronizer.value?.let {
|
||||||
it.rewindToNearestHeight(it.latestBirthdayHeight, true)
|
it.latestBirthdayHeight?.let { height ->
|
||||||
return true
|
it.rewindToNearestHeight(height, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +237,6 @@ private suspend fun PersistableWallet.toConfig(): Initializer.Config {
|
||||||
val vk = deriveViewingKey()
|
val vk = deriveViewingKey()
|
||||||
|
|
||||||
return Initializer.Config {
|
return Initializer.Config {
|
||||||
it.importWallet(vk, birthday?.height, network, network.defaultHost, network.defaultPort)
|
it.importWallet(vk, birthday, network, network.defaultHost, network.defaultPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,13 @@ object WalletSnapshotFixture {
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
fun new(
|
fun new(
|
||||||
status: Synchronizer.Status = STATUS,
|
status: Synchronizer.Status = STATUS,
|
||||||
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
|
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
),
|
||||||
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
||||||
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
||||||
transparentBalance: WalletBalance = TRANSPARENT_BALANCE,
|
transparentBalance: WalletBalance = TRANSPARENT_BALANCE,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||||
import cash.z.ecc.android.sdk.db.entity.Transaction
|
import cash.z.ecc.android.sdk.db.entity.Transaction
|
||||||
import cash.z.ecc.android.sdk.db.entity.isMined
|
import cash.z.ecc.android.sdk.db.entity.isMined
|
||||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||||
|
@ -282,7 +283,7 @@ sealed class SynchronizerError {
|
||||||
override fun getCauseMessage(): String? = error?.localizedMessage
|
override fun getCauseMessage(): String? = error?.localizedMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
class Chain(val x: Int, val y: Int) : SynchronizerError() {
|
class Chain(val x: BlockHeight, val y: BlockHeight) : SynchronizerError() {
|
||||||
override fun getCauseMessage(): String = "$x, $y"
|
override fun getCauseMessage(): String = "$x, $y"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue