306 lines
15 KiB
Kotlin
306 lines
15 KiB
Kotlin
package cash.z.ecc.android.sdk.exception
|
|
|
|
import cash.z.ecc.android.sdk.internal.SaplingParameters
|
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
import cash.z.wallet.sdk.rpc.Service
|
|
import io.grpc.Status
|
|
import io.grpc.Status.Code.UNAVAILABLE
|
|
|
|
/**
|
|
* Marker for all custom exceptions from the SDK. Making it an interface would result in more typing
|
|
* so it's a supertype, instead.
|
|
*/
|
|
open class SdkException(message: String, cause: Throwable?) : RuntimeException(message, cause)
|
|
|
|
/**
|
|
* Exceptions thrown in the Rust layer of the SDK. We may not always be able to surface details about this
|
|
* exception so it's important for the SDK to provide helpful messages whenever these errors are encountered.
|
|
*/
|
|
sealed class RustLayerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
class BalanceException(cause: Throwable) : RustLayerException(
|
|
"Error while requesting the current balance over " +
|
|
"JNI. This might mean that the database has been corrupted and needs to be rebuilt. Verify that " +
|
|
"blocks are not missing or have not been scanned out of order.",
|
|
cause
|
|
)
|
|
}
|
|
|
|
/**
|
|
* User-facing exceptions thrown by the transaction repository.
|
|
*/
|
|
sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
object FalseStart : RepositoryException(
|
|
"The channel is closed. Note that once a repository has stopped it " +
|
|
"cannot be restarted. Verify that the repository is not being restarted."
|
|
)
|
|
object Unprepared : RepositoryException(
|
|
"Unprepared repository: Data cannot be accessed before the repository is prepared." +
|
|
" Ensure that things have been properly initialized. If you see this error it most" +
|
|
" likely means that you are accessing transactions or other data before starting the" +
|
|
" Synchronizer. Previously, this was a silent bug that would cause problems later." +
|
|
" Mostly, during database migrations. Now, we catch this early and explicitly prevent" +
|
|
" it from happening."
|
|
)
|
|
}
|
|
|
|
/**
|
|
* High-level exceptions thrown by the synchronizer, which do not fall within the umbrella of a
|
|
* child component.
|
|
*/
|
|
sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
object FalseStart : SynchronizerException(
|
|
"This synchronizer was already started. Multiple calls to start are not" +
|
|
"allowed and once a synchronizer has stopped it cannot be restarted."
|
|
)
|
|
object NotYetStarted : SynchronizerException(
|
|
"The synchronizer has not yet started. Verify that" +
|
|
" start has been called prior to this operation and that the coroutineScope is not" +
|
|
" being accessed before it is initialized."
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Potentially user-facing exceptions that occur while processing compact blocks.
|
|
*/
|
|
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
class DataDbMissing(path: String) : CompactBlockProcessorException(
|
|
"No data db file found at path $path. Verify " +
|
|
"that the data DB has been initialized via `rustBackend.initDataDb(path)`"
|
|
)
|
|
open class ConfigurationException(
|
|
message: String,
|
|
cause: Throwable?
|
|
) : CompactBlockProcessorException(message, cause)
|
|
class FileInsteadOfPath(fileName: String) : ConfigurationException(
|
|
"Invalid Path: the given path appears to be a" +
|
|
" file name instead of a path: $fileName. The RustBackend expects the absolutePath to the database rather" +
|
|
" than just the database filename because Rust does not access the app Context." +
|
|
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.",
|
|
null
|
|
)
|
|
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
|
|
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException(
|
|
"Error while downloading blocks. This most " +
|
|
"likely means the server is down or slow to respond. See logs for details.",
|
|
cause
|
|
)
|
|
class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException(
|
|
"Error while scanning blocks. This most " +
|
|
"likely means a block was missed or a reorg was mishandled. See logs for details.",
|
|
cause
|
|
)
|
|
class Disconnected(cause: Throwable? = null) :
|
|
CompactBlockProcessorException("Disconnected Error. Unable to download blocks due to ${cause?.message}", cause)
|
|
object Uninitialized : CompactBlockProcessorException(
|
|
"Cannot process blocks because the wallet has not been" +
|
|
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
|
|
" can be fixed by re-importing the wallet."
|
|
)
|
|
object NoAccount : CompactBlockProcessorException(
|
|
"Attempting to scan without an account. This is probably a setup error or a race condition."
|
|
)
|
|
|
|
open class EnhanceTransactionError(
|
|
message: String,
|
|
val height: BlockHeight?,
|
|
cause: Throwable
|
|
) : CompactBlockProcessorException(message, cause) {
|
|
class EnhanceTxDownloadError(
|
|
height: BlockHeight?,
|
|
cause: Throwable
|
|
) : EnhanceTransactionError(
|
|
"Error while attempting to download a transaction to enhance",
|
|
height,
|
|
cause
|
|
)
|
|
class EnhanceTxDecryptError(
|
|
height: BlockHeight?,
|
|
cause: Throwable
|
|
) : EnhanceTransactionError(
|
|
"Error while attempting to decrypt and store a transaction to enhance",
|
|
height,
|
|
cause
|
|
)
|
|
}
|
|
|
|
class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException(
|
|
"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try " +
|
|
"updating the client or switching servers."
|
|
)
|
|
|
|
class MismatchedBranch(
|
|
clientBranch: String?,
|
|
serverBranch: String?,
|
|
networkName: String?
|
|
) : CompactBlockProcessorException(
|
|
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName " +
|
|
"but it was $serverBranch! Try updating the client or switching servers."
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Exceptions related to the wallet's birthday.
|
|
*/
|
|
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
object UninitializedBirthdayException : BirthdayException(
|
|
"Error the birthday cannot be" +
|
|
" accessed before it is initialized. Verify that the new, import or open functions" +
|
|
" have been called on the initializer."
|
|
)
|
|
class MissingBirthdayFilesException(directory: String) : BirthdayException(
|
|
"Cannot initialize wallet because no birthday files were found in the $directory directory."
|
|
)
|
|
class ExactBirthdayNotFoundException internal constructor(
|
|
birthday: BlockHeight,
|
|
nearestMatch: Checkpoint? = null
|
|
) : BirthdayException(
|
|
"Unable to find birthday that exactly matches $birthday.${
|
|
if (nearestMatch != null) {
|
|
" An exact match was request but the nearest match found was ${nearestMatch.height}."
|
|
} else {
|
|
""
|
|
}
|
|
}"
|
|
)
|
|
class BirthdayFileNotFoundException(directory: String, height: BlockHeight?) : BirthdayException(
|
|
"Unable to find birthday file for $height verify that $directory/$height.json exists."
|
|
)
|
|
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) : BirthdayException(
|
|
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
|
|
"where the first portion is an Int representing the height of the tree contained in the file",
|
|
cause
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Exceptions thrown by the initializer.
|
|
*/
|
|
sealed class InitializeException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
object SeedRequired : InitializeException(
|
|
"A pending database migration requires the wallet's seed. Call this initialization " +
|
|
"method again with the seed."
|
|
)
|
|
class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause)
|
|
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException(
|
|
"Failed to initialize the blocks table" +
|
|
" because it already exists in $dbPath",
|
|
cause
|
|
)
|
|
object MissingBirthdayException : InitializeException(
|
|
"Expected a birthday for this wallet but failed to find one. This usually means that " +
|
|
"wallet setup did not happen correctly. A workaround might be to interpret the " +
|
|
"birthday, based on the contents of the wallet data but it is probably better " +
|
|
"not to mask this error because the root issue should be addressed."
|
|
)
|
|
object MissingViewingKeyException : InitializeException(
|
|
"Expected a unified viewingKey for this wallet but failed to find one. This usually means" +
|
|
" that wallet setup happened incorrectly. A workaround might be to derive the" +
|
|
" unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" +
|
|
" better not to mask this error because the root issue should be addressed."
|
|
)
|
|
class MissingAddressException(description: String, cause: Throwable? = null) : InitializeException(
|
|
"Expected a $description address for this wallet but failed to find one. This usually" +
|
|
" means that wallet setup happened incorrectly. If this problem persists, a" +
|
|
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
|
|
" will restore any missing address information. Meanwhile, please report that" +
|
|
" this happened so that the root issue can be uncovered and corrected." +
|
|
if (cause != null) "\nCaused by: $cause" else ""
|
|
)
|
|
object DatabasePathException :
|
|
InitializeException(
|
|
"Critical failure to locate path for storing databases. Perhaps this device prevents" +
|
|
" apps from storing data? We cannot initialize the wallet unless we can store" +
|
|
" data."
|
|
)
|
|
|
|
class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializeException(
|
|
"Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" +
|
|
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
|
|
)
|
|
|
|
object MissingDefaultBirthdayException : InitializeException(
|
|
"The birthday height is missing and it is unclear which value to use as a default."
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Exceptions thrown while interacting with lightwalletd.
|
|
*/
|
|
sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
object InsecureConnection : LightWalletException(
|
|
"Error: attempted to connect to lightwalletd" +
|
|
" with an insecure connection! Plaintext connections are only allowed when the" +
|
|
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
|
|
" because this choice should be explicit."
|
|
)
|
|
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
|
|
LightWalletException(
|
|
"Error: the lightwalletd server is using a consensus branch" +
|
|
" (branch: $lwdBranch) that does not match the transactions being created" +
|
|
" (branch: $sdkBranch). This probably means the SDK and Server are on two" +
|
|
" different chains, most likely because of a recent network upgrade (NU). Either" +
|
|
" update the SDK to match lightwalletd or use a lightwalletd that matches the SDK."
|
|
)
|
|
|
|
open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
|
|
class ChainInfoNotMatching(
|
|
val propertyNames: String,
|
|
val expectedInfo: Service.LightdInfo,
|
|
val actualInfo: Service.LightdInfo
|
|
) : ChangeServerException(
|
|
"Server change error: the $propertyNames values did not match."
|
|
)
|
|
class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) {
|
|
companion object {
|
|
private fun Status.toMessage(): String {
|
|
return when (this.code) {
|
|
UNAVAILABLE -> {
|
|
"Error: the new server is unavailable. Verify that the host and port are correct. Failed " +
|
|
"with $this"
|
|
}
|
|
else -> "Changing servers failed with status $this"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Potentially user-facing exceptions thrown while encoding transactions.
|
|
*/
|
|
sealed class TransactionEncoderException(
|
|
message: String,
|
|
cause: Throwable? = null
|
|
) : SdkException(message, cause) {
|
|
class FetchParamsException internal constructor(
|
|
internal val parameters: SaplingParameters,
|
|
message: String
|
|
) : TransactionEncoderException("Failed to fetch params: $parameters, due to: $message")
|
|
class ValidateParamsException internal constructor(
|
|
internal val parameters: SaplingParameters,
|
|
message: String
|
|
) : TransactionEncoderException("Failed to validate fetched params: $parameters, due to:$message")
|
|
object MissingParamsException : TransactionEncoderException(
|
|
"Cannot send funds due to missing spend or output params and attempting to download them failed."
|
|
)
|
|
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
|
|
"Unable to find transactionId $transactionId in the repository. This means the wallet created a transaction " +
|
|
"and then returned a row ID that does not actually exist. This is a scenario where the wallet should " +
|
|
"have thrown an exception but failed to do so."
|
|
)
|
|
class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException(
|
|
"The transaction returned by the wallet," +
|
|
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
|
|
"thrown an exception but failed to do so."
|
|
)
|
|
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
|
|
"Cannot" +
|
|
" create spending transaction because scanning is incomplete. We must scan up to the" +
|
|
" latest height to know which consensus rules to apply. However, the last scanned" +
|
|
" height was $lastScannedHeight."
|
|
)
|
|
}
|