diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/TONWalletKitException.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/TONWalletKitException.kt new file mode 100644 index 00000000..025054e8 --- /dev/null +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/TONWalletKitException.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit + +/** + * Strongly-typed errors raised by [TONWalletKit] for SDK lifecycle / usage failures. + * + * These are distinct from [WalletKitBridgeException], which represents failures of the + * JavaScript bridge itself. A [TONWalletKitException] signals that the SDK was used + * incorrectly (e.g. before initialization or after destruction). + */ +sealed class TONWalletKitException(message: String, cause: Throwable? = null) : Exception(message, cause) { + + /** The SDK was used before [TONWalletKit.initialize] was called. */ + class NotInitialized : TONWalletKitException( + "TONWalletKit.initialize() must be called before using the SDK.", + ) + + /** + * The SDK instance has already been destroyed. A new instance must be created. + * + * @property method The method that was invoked on the destroyed instance, when known. + */ + class Destroyed(val method: String? = null) : TONWalletKitException( + method?.let { "Cannot call method '$it' - SDK has been destroyed" } + ?: "TONWalletKit instance has been destroyed. Create a new instance.", + ) + + /** Lazy auto-initialization of the SDK failed. */ + class AutoInitializationFailed(cause: Throwable) : TONWalletKitException( + "Failed to auto-initialize WalletKit: ${cause.message}", + cause, + ) +} diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONBase64.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONBase64.kt index 927852fb..1e998eed 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONBase64.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONBase64.kt @@ -73,10 +73,14 @@ data class TONBase64( /** * Parses a Base64-encoded string, validating it upfront. * - * @throws IllegalArgumentException if [value] is not valid Base64. + * @throws TONBase64ValidationException if [value] is not valid Base64. */ fun parse(value: String): TONBase64 { - Base64.decode(value, Base64.DEFAULT) + try { + Base64.decode(value, Base64.DEFAULT) + } catch (e: IllegalArgumentException) { + throw TONBase64ValidationException(value) + } return TONBase64(value) } } @@ -101,3 +105,12 @@ object TONBase64Serializer : KSerializer { return TONBase64(decoder.decodeString()) } } + +/** + * Raised when a string fails [TONBase64] validation. + * + * @property invalidValue The string that was not valid Base64. + */ +class TONBase64ValidationException( + val invalidValue: String, +) : IllegalArgumentException("$invalidValue is not a valid base64 encoded string.") diff --git a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONHex.kt b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONHex.kt index baa01f49..c088f5bc 100644 --- a/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONHex.kt +++ b/TONWalletKit-Android/api/src/main/java/io/ton/walletkit/model/TONHex.kt @@ -89,6 +89,19 @@ data class TONHex( fun fromString(string: String, withPrefix: Boolean = true): TONHex { return fromData(string.toByteArray(Charsets.UTF_8), withPrefix) } + + /** + * Parses a hex string, validating it upfront. + * + * @throws TONHexValidationException if [value] is not a valid hex string. + */ + fun parse(value: String): TONHex { + val hex = TONHex(value) + if (hex.data == null) { + throw TONHexValidationException(value) + } + return hex + } } override fun toString(): String = value @@ -111,3 +124,12 @@ object TONHexSerializer : KSerializer { return TONHex(decoder.decodeString()) } } + +/** + * Raised when a string fails [TONHex] validation. + * + * @property invalidValue The string that was not a valid hex string. + */ +class TONHexValidationException( + val invalidValue: String, +) : IllegalArgumentException("$invalidValue is not a valid hex string.") diff --git a/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs b/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs index 171ebe70..f83bcc42 100644 --- a/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs +++ b/TONWalletKit-Android/impl/src/main/assets/walletkit/walletkit-android-bridge.mjs @@ -33747,6 +33747,15 @@ var init_SwapProvider = __esmMin((() => { })); //#endregion //#region ../walletkit/dist/esm/defi/errors.js +/** +* Guarantees a typed error: returns `error` unchanged when it already is a {@link DefiError} +* (including subclasses like `SwapError`, `StakingError`, etc.), otherwise wraps it in a +* `DefiError` with the {@link DefiErrorCode.Unknown} code. Use in manager catch blocks so the +* public API always throws a `DefiError`. +*/ +function toDefiError(error, message) { + return error instanceof DefiError ? error : new DefiError(message, DefiErrorCode.Unknown, error); +} var DefiErrorCode, DefiError; var init_errors$4 = __esmMin((() => { (function(DefiErrorCode) { @@ -33756,6 +33765,7 @@ var init_errors$4 = __esmMin((() => { DefiErrorCode["UnsupportedNetwork"] = "UNSUPPORTED_NETWORK"; DefiErrorCode["InvalidParams"] = "INVALID_PARAMS"; DefiErrorCode["InvalidProvider"] = "INVALID_PROVIDER"; + DefiErrorCode["Unknown"] = "UNKNOWN"; })(DefiErrorCode || (DefiErrorCode = {})); DefiError = class extends Error { code; @@ -33769,27 +33779,6 @@ var init_errors$4 = __esmMin((() => { }; })); //#endregion -//#region ../walletkit/dist/esm/defi/swap/errors.js -var SwapErrorCode, SwapError; -var init_errors$3 = __esmMin((() => { - init_errors$4(); - (function(SwapErrorCode) { - SwapErrorCode["InvalidQuote"] = "INVALID_QUOTE"; - SwapErrorCode["InsufficientLiquidity"] = "INSUFFICIENT_LIQUIDITY"; - SwapErrorCode["QuoteExpired"] = "QUOTE_EXPIRED"; - SwapErrorCode["BuildTxFailed"] = "BUILD_TX_FAILED"; - SwapErrorCode["NetworkError"] = "NETWORK_ERROR"; - })(SwapErrorCode || (SwapErrorCode = {})); - SwapError = class extends DefiError { - code; - constructor(message, code, details) { - super(message, code, details); - this.name = "SwapError"; - this.code = code; - } - }; -})); -//#endregion //#region ../walletkit/dist/esm/defi/DefiManager.js var DefiManager; var init_DefiManager = __esmMin((() => { @@ -33814,7 +33803,7 @@ var init_DefiManager = __esmMin((() => { registerProvider(input) { const provider = resolveProvider(input, this.createFactoryContext()); const providerId = provider.providerId; - if (!providerId) throw this.createError("Provider must have a providerId", DefiErrorCode.InvalidProvider); + if (!providerId) throw new DefiError("Provider must have a providerId", DefiErrorCode.InvalidProvider); const oldProvider = this.providers.find((p) => p.providerId === providerId); if (oldProvider) this.removeProvider(oldProvider); this.providers = [...this.providers, provider]; @@ -33846,7 +33835,7 @@ var init_DefiManager = __esmMin((() => { */ setDefaultProvider(providerId) { const provider = this.providers.find((p) => p.providerId === providerId); - if (!provider) throw this.createError(`Provider '${providerId}' not found`, DefiErrorCode.ProviderNotFound, { + if (!provider) throw new DefiError(`Provider '${providerId}' not found`, DefiErrorCode.ProviderNotFound, { provider: providerId, registered: this.providers.map((p) => p.providerId) }); @@ -33864,9 +33853,9 @@ var init_DefiManager = __esmMin((() => { */ getProvider(providerId) { const providerName = providerId || this.defaultProviderId; - if (!providerName) throw this.createError("No default provider set. Register a provider first.", DefiErrorCode.NoDefaultProvider); + if (!providerName) throw new DefiError("No default provider set. Register a provider first.", DefiErrorCode.NoDefaultProvider); const provider = this.providers.find((p) => p.providerId === providerName); - if (!provider) throw this.createError(`Provider '${providerName}' not found`, DefiErrorCode.ProviderNotFound, { + if (!provider) throw new DefiError(`Provider '${providerName}' not found`, DefiErrorCode.ProviderNotFound, { provider: providerName, registered: this.providers.map((p) => p.providerId) }); @@ -33894,9 +33883,9 @@ var init_DefiManager = __esmMin((() => { //#region ../walletkit/dist/esm/defi/swap/SwapManager.js var log$21, SwapManager; var init_SwapManager = __esmMin((() => { - init_errors$3(); init_Logger(); init_DefiManager(); + init_errors$4(); log$21 = globalLogger.createChild("SwapManager"); SwapManager = class extends DefiManager { constructor(createFactoryContext) { @@ -33929,7 +33918,7 @@ var init_SwapManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to get swap quote"); } } /** @@ -33952,11 +33941,29 @@ var init_SwapManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to build swap transaction"); } } - createError(message, code, details) { - return new SwapError(message, code, details); + }; +})); +//#endregion +//#region ../walletkit/dist/esm/defi/swap/errors.js +var SwapErrorCode, SwapError; +var init_errors$3 = __esmMin((() => { + init_errors$4(); + (function(SwapErrorCode) { + SwapErrorCode["InvalidQuote"] = "INVALID_QUOTE"; + SwapErrorCode["InsufficientLiquidity"] = "INSUFFICIENT_LIQUIDITY"; + SwapErrorCode["QuoteExpired"] = "QUOTE_EXPIRED"; + SwapErrorCode["BuildTxFailed"] = "BUILD_TX_FAILED"; + SwapErrorCode["NetworkError"] = "NETWORK_ERROR"; + })(SwapErrorCode || (SwapErrorCode = {})); + SwapError = class extends DefiError { + code; + constructor(message, code, details) { + super(message, code, details); + this.name = "SwapError"; + this.code = code; } }; })); @@ -33980,30 +33987,12 @@ var init_StakingProvider = __esmMin((() => { }; })); //#endregion -//#region ../walletkit/dist/esm/defi/staking/errors.js -var StakingErrorCode, StakingError; -var init_errors$2 = __esmMin((() => { - init_errors$4(); - (function(StakingErrorCode) { - StakingErrorCode["InvalidParams"] = "INVALID_PARAMS"; - StakingErrorCode["UnsupportedOperation"] = "UNSUPPORTED_OPERATION"; - })(StakingErrorCode || (StakingErrorCode = {})); - StakingError = class extends DefiError { - code; - constructor(message, code, details) { - super(message, code, details); - this.name = "StakingError"; - this.code = code; - } - }; -})); -//#endregion //#region ../walletkit/dist/esm/defi/staking/StakingManager.js var log$20, StakingManager; var init_StakingManager = __esmMin((() => { - init_errors$2(); init_Logger(); init_DefiManager(); + init_errors$4(); log$20 = globalLogger.createChild("StakingManager"); StakingManager = class extends DefiManager { constructor(createFactoryContext) { @@ -34025,7 +34014,7 @@ var init_StakingManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to get staking quote"); } } /** @@ -34042,7 +34031,7 @@ var init_StakingManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to build staking transaction"); } } /** @@ -34065,7 +34054,7 @@ var init_StakingManager = __esmMin((() => { userAddress, network }); - throw error; + throw toDefiError(error, "Failed to get staking balance"); } } /** @@ -34085,7 +34074,7 @@ var init_StakingManager = __esmMin((() => { error, network }); - throw error; + throw toDefiError(error, "Failed to get staking info"); } } /** @@ -34105,16 +34094,26 @@ var init_StakingManager = __esmMin((() => { error, network }); - throw error; + throw toDefiError(error, "Failed to get staking metadata"); } } - createError(message, code, details) { - const errorCode = Object.values(StakingErrorCode).includes(code) ? code : StakingErrorCode.InvalidParams; - log$20.error(message, { - code, - details - }); - return new StakingError(message, errorCode, details); + }; +})); +//#endregion +//#region ../walletkit/dist/esm/defi/staking/errors.js +var StakingErrorCode, StakingError; +var init_errors$2 = __esmMin((() => { + init_errors$4(); + (function(StakingErrorCode) { + StakingErrorCode["InvalidParams"] = "INVALID_PARAMS"; + StakingErrorCode["UnsupportedOperation"] = "UNSUPPORTED_OPERATION"; + })(StakingErrorCode || (StakingErrorCode = {})); + StakingError = class extends DefiError { + code; + constructor(message, code, details) { + super(message, code, details); + this.name = "StakingError"; + this.code = code; } }; })); @@ -34134,38 +34133,12 @@ var init_GaslessProvider = __esmMin((() => { }; })); //#endregion -//#region ../walletkit/dist/esm/defi/gasless/errors.js -var GaslessErrorCode, GaslessError; -var init_errors$1 = __esmMin((() => { - init_errors$4(); - (function(GaslessErrorCode) { - GaslessErrorCode["UnsupportedFeeAsset"] = "UNSUPPORTED_FEE_ASSET"; - GaslessErrorCode["UnsupportedOperation"] = "UNSUPPORTED_OPERATION"; - GaslessErrorCode["QuoteFailed"] = "QUOTE_FAILED"; - GaslessErrorCode["SendFailed"] = "SEND_FAILED"; - GaslessErrorCode["ConfigFailed"] = "CONFIG_FAILED"; - GaslessErrorCode["SignMessageNotSupported"] = "SIGN_MESSAGE_NOT_SUPPORTED"; - GaslessErrorCode["TooManyMessages"] = "TOO_MANY_MESSAGES"; - GaslessErrorCode["QuoteExpired"] = "QUOTE_EXPIRED"; - GaslessErrorCode["WalletMismatch"] = "WALLET_MISMATCH"; - GaslessErrorCode["FeeAssetNotOwned"] = "FEE_ASSET_NOT_OWNED"; - })(GaslessErrorCode || (GaslessErrorCode = {})); - GaslessError = class extends DefiError { - code; - constructor(message, code, details) { - super(message, code, details); - this.name = "GaslessError"; - this.code = code; - } - }; -})); -//#endregion //#region ../walletkit/dist/esm/defi/gasless/GaslessManager.js var log$19, GaslessManager; var init_GaslessManager = __esmMin((() => { init_Logger(); init_DefiManager(); - init_errors$1(); + init_errors$4(); log$19 = globalLogger.createChild("GaslessManager"); GaslessManager = class extends DefiManager { constructor(createFactoryContext) { @@ -34181,7 +34154,7 @@ var init_GaslessManager = __esmMin((() => { return await this.getProvider(selectedProviderId).getMetadata(); } catch (error) { log$19.error("Failed to get gasless provider metadata", { error }); - throw error; + throw toDefiError(error, "Failed to get gasless provider metadata"); } } /** @@ -34200,7 +34173,7 @@ var init_GaslessManager = __esmMin((() => { return await provider.getConfig(targetNetwork); } catch (error) { log$19.error("Failed to get gasless config", { error }); - throw error; + throw toDefiError(error, "Failed to get gasless config"); } } /** @@ -34221,7 +34194,7 @@ var init_GaslessManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to quote gasless transaction"); } } /** @@ -34236,11 +34209,34 @@ var init_GaslessManager = __esmMin((() => { return await this.getProvider(providerId ?? this.defaultProviderId).sendTransaction(params); } catch (error) { log$19.error("Failed to send gasless transaction", { error }); - throw error; + throw toDefiError(error, "Failed to send gasless transaction"); } } - createError(message, code, details) { - return new GaslessError(message, code, details); + }; +})); +//#endregion +//#region ../walletkit/dist/esm/defi/gasless/errors.js +var GaslessErrorCode, GaslessError; +var init_errors$1 = __esmMin((() => { + init_errors$4(); + (function(GaslessErrorCode) { + GaslessErrorCode["UnsupportedFeeAsset"] = "UNSUPPORTED_FEE_ASSET"; + GaslessErrorCode["UnsupportedOperation"] = "UNSUPPORTED_OPERATION"; + GaslessErrorCode["QuoteFailed"] = "QUOTE_FAILED"; + GaslessErrorCode["SendFailed"] = "SEND_FAILED"; + GaslessErrorCode["ConfigFailed"] = "CONFIG_FAILED"; + GaslessErrorCode["SignMessageNotSupported"] = "SIGN_MESSAGE_NOT_SUPPORTED"; + GaslessErrorCode["TooManyMessages"] = "TOO_MANY_MESSAGES"; + GaslessErrorCode["QuoteExpired"] = "QUOTE_EXPIRED"; + GaslessErrorCode["WalletMismatch"] = "WALLET_MISMATCH"; + GaslessErrorCode["FeeAssetNotOwned"] = "FEE_ASSET_NOT_OWNED"; + })(GaslessErrorCode || (GaslessErrorCode = {})); + GaslessError = class extends DefiError { + code; + constructor(message, code, details) { + super(message, code, details); + this.name = "GaslessError"; + this.code = code; } }; })); @@ -36686,6 +36682,7 @@ var init_errors = __esmMin((() => { (function(CryptoOnrampErrorCode) { CryptoOnrampErrorCode["ProviderError"] = "PROVIDER_ERROR"; CryptoOnrampErrorCode["QuoteFailed"] = "QUOTE_FAILED"; + CryptoOnrampErrorCode["DepositFailed"] = "DEPOSIT_FAILED"; CryptoOnrampErrorCode["RefundAddressRequired"] = "REFUND_ADDRESS_REQUIRED"; CryptoOnrampErrorCode["InvalidRefundAddress"] = "INVALID_REFUND_ADDRESS"; CryptoOnrampErrorCode["ReversedAmountNotSupported"] = "REVERSED_AMOUNT_NOT_SUPPORTED"; @@ -36710,7 +36707,7 @@ var init_errors = __esmMin((() => { //#region ../walletkit/dist/esm/defi/crypto-onramp/CryptoOnrampManager.js var log$11, CryptoOnrampManager; var init_CryptoOnrampManager = __esmMin((() => { - init_errors(); + init_errors$4(); init_Logger(); init_DefiManager(); log$11 = globalLogger.createChild("CryptoOnrampManager"); @@ -36726,7 +36723,7 @@ var init_CryptoOnrampManager = __esmMin((() => { return this.getProvider(selectedProviderId).getMetadata(); } catch (error) { log$11.error("Failed to get crypto onramp metadata", { error }); - throw error; + throw toDefiError(error, "Failed to get crypto onramp metadata"); } } /** @@ -36758,7 +36755,7 @@ var init_CryptoOnrampManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to get crypto onramp quote"); } } /** @@ -36787,7 +36784,7 @@ var init_CryptoOnrampManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to create crypto onramp deposit"); } } /** @@ -36811,7 +36808,7 @@ var init_CryptoOnrampManager = __esmMin((() => { error, params }); - throw error; + throw toDefiError(error, "Failed to get crypto onramp deposit status"); } } /** @@ -36825,12 +36822,9 @@ var init_CryptoOnrampManager = __esmMin((() => { return await this.getProvider(selectedProviderId).getSupportedCurrencies(); } catch (error) { log$11.error("Failed to discover crypto onramp supported currencies", { error }); - throw error; + throw toDefiError(error, "Failed to discover crypto onramp supported currencies"); } } - createError(message, code, details) { - return new CryptoOnrampError(message, code, details); - } }; })); //#endregion @@ -41057,6 +41051,7 @@ init_models(); init_Logger(); init_StakingProvider(); init_errors$2(); +init_errors$4(); init_ApiClientTonApi(); init_units(); var log$3 = globalLogger.createChild("TonStakersStakingProvider"); @@ -41121,7 +41116,7 @@ var TonStakersStakingProvider = class TonStakersStakingProvider extends StakingP if (!hasDefaultContract && !hasCustomContract) continue; chainConfig[chainId] = perChain; } - if (Object.keys(chainConfig).length === 0) throw new Error("createTonstakersProvider: no eligible networks (add mainnet/testnet or pass metadata.contractAddress in overrides)"); + if (Object.keys(chainConfig).length === 0) throw new DefiError("createTonstakersProvider: no eligible networks (add mainnet/testnet or pass metadata.contractAddress in overrides)", DefiErrorCode.InvalidParams); return new TonStakersStakingProvider(ctx.networkManager, chainConfig); } /** @@ -41380,7 +41375,7 @@ var TonStakersStakingProvider = class TonStakersStakingProvider extends StakingP network, apiKey: token }).getJson(`/v2/staking/pool/${address}`); - if (!poolInfo?.pool?.apy) throw new Error("Invalid APY data from TonAPI"); + if (!poolInfo?.pool?.apy) throw new StakingError("Invalid APY data from TonAPI", StakingErrorCode.InvalidParams); return Number(poolInfo.pool.apy); } static isValidTokenInfo(token) { @@ -46229,6 +46224,7 @@ init_Logger(); init_retry(); init_errors$1(); init_GaslessProvider(); +init_errors$4(); var log = globalLogger.createChild("TonApiGaslessProvider"); /** * Gasless provider implementation backed by the public TonAPI REST API. @@ -46299,7 +46295,7 @@ var TonApiGaslessProvider = class TonApiGaslessProvider extends GaslessProvider chainConfig[chainId] = perChain; } else for (const chainId of configuredChains) chainConfig[chainId] = {}; - if (Object.keys(chainConfig).length === 0) throw new Error("createTonApiGaslessProvider: no eligible networks (configure at least one network in the kit, or pass `chains` matching a configured network)"); + if (Object.keys(chainConfig).length === 0) throw new DefiError("createTonApiGaslessProvider: no eligible networks (configure at least one network in the kit, or pass `chains` matching a configured network)", DefiErrorCode.InvalidParams); return new TonApiGaslessProvider(chainConfig, config); } getSupportedNetworks() { diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/BridgeDispatchException.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/BridgeDispatchException.kt new file mode 100644 index 00000000..7bf5f5fe --- /dev/null +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/BridgeDispatchException.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.bridge + +import io.ton.walletkit.WalletKitBridgeException + +internal sealed class BridgeDispatchException(message: String) : WalletKitBridgeException(message) { + class UnknownMethod(method: String) : + BridgeDispatchException("Unknown bridge method: $method") + + class UnknownReverseRpcMethod(method: String) : + BridgeDispatchException("Unknown reverse-RPC method: $method") + + class MissingParameter(method: String, parameter: String) : + BridgeDispatchException("$method: missing $parameter") + + class AdapterNotFound(adapterId: String) : + BridgeDispatchException("Adapter not found: $adapterId") + + class SignerNotFound(signerId: String) : + BridgeDispatchException("Custom signer not found: $signerId") + + class SessionManagerNotConfigured : + BridgeDispatchException("Session manager not configured") + + class ApiClientNotConfigured(chainId: String) : + BridgeDispatchException("No API client configured for chainId=$chainId") + + class ProviderNotRegistered(providerId: String) : + BridgeDispatchException("No Kotlin provider registered for id=$providerId") + + class WrappedFunctionNotRegistered(reference: String) : + BridgeDispatchException("No wrapped function registered for reference: $reference") +} diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/BridgeRequestRegistry.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/BridgeRequestRegistry.kt index 4a3a8ac2..9659abff 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/BridgeRequestRegistry.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/BridgeRequestRegistry.kt @@ -21,6 +21,7 @@ */ package io.ton.walletkit.bridge.dispatch +import io.ton.walletkit.bridge.BridgeDispatchException import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.serializer @@ -58,7 +59,7 @@ internal class BridgeRequestRegistry(private val json: Json) { suspend fun dispatch(method: String, params: JsonElement): String { val handler = handlers[method] - ?: throw IllegalArgumentException("Unknown reverse-RPC method: $method") + ?: throw BridgeDispatchException.UnknownReverseRpcMethod(method) return handler(params) } } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/WrappedFunctionRegistry.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/WrappedFunctionRegistry.kt index 09c9c3b8..2d111719 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/WrappedFunctionRegistry.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/bridge/dispatch/WrappedFunctionRegistry.kt @@ -21,6 +21,7 @@ */ package io.ton.walletkit.bridge.dispatch +import io.ton.walletkit.bridge.BridgeDispatchException import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.serializer @@ -71,7 +72,7 @@ internal class WrappedFunctionRegistry( /** Invokes the callback bound to [refId]. The returned String is already JSON-encoded. */ suspend fun invoke(refId: String, args: JsonArray): String { val fn = functions[refId] - ?: throw IllegalArgumentException("No wrapped function registered for reference: $refId") + ?: throw BridgeDispatchException.WrappedFunctionNotRegistered(refId) return fn(args) } } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt index 4dfdcc3c..fe99e822 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/core/TONWalletKit.kt @@ -25,6 +25,7 @@ import android.content.Context import android.webkit.WebView import io.ton.walletkit.ITONWallet import io.ton.walletkit.ITONWalletKit +import io.ton.walletkit.TONWalletKitException import io.ton.walletkit.WebViewTonConnectInjector import io.ton.walletkit.api.TONTonStakersProviderConfig import io.ton.walletkit.api.WalletVersions @@ -225,7 +226,7 @@ internal class TONWalletKit private constructor( private fun checkNotDestroyed() { if (isDestroyed) { - throw IllegalStateException("TONWalletKit instance has been destroyed. Create a new instance.") + throw TONWalletKitException.Destroyed() } } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt index 92536f73..1973079a 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/WebViewWalletKitEngine.kt @@ -22,6 +22,7 @@ package io.ton.walletkit.engine import android.content.Context +import io.ton.walletkit.TONWalletKitException import io.ton.walletkit.WalletKitBridgeException import io.ton.walletkit.api.generated.TONAccountState import io.ton.walletkit.api.generated.TONConnectionApprovalResponse @@ -319,7 +320,7 @@ internal class WebViewWalletKitEngine private constructor( private suspend fun call(method: String, params: Any? = null): JsonObject { if (isDestroyed) { - throw WalletKitBridgeException("Cannot call method '$method' - SDK has been destroyed") + throw TONWalletKitException.Destroyed(method) } return rpcClient.call(method, params) } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/InitializationManager.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/InitializationManager.kt index e43f5f91..a48512c0 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/InitializationManager.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/InitializationManager.kt @@ -22,7 +22,7 @@ package io.ton.walletkit.engine.infrastructure import android.content.Context -import io.ton.walletkit.WalletKitBridgeException +import io.ton.walletkit.TONWalletKitException import io.ton.walletkit.api.MAINNET import io.ton.walletkit.api.TESTNET import io.ton.walletkit.api.generated.TONNetwork @@ -96,7 +96,7 @@ internal class InitializationManager( return@withLock } - val effectiveConfig = configuration ?: pendingInitConfig ?: throw WalletKitBridgeException(ERROR_INIT_CONFIG_REQUIRED) + val effectiveConfig = configuration ?: pendingInitConfig ?: throw TONWalletKitException.NotInitialized() pendingInitConfig = null Logger.d(TAG, "Auto-initializing WalletKit with config: network=${resolveNetworkName(effectiveConfig)}") @@ -107,7 +107,7 @@ internal class InitializationManager( Logger.d(TAG, "WalletKit auto-initialization completed successfully") } catch (err: Throwable) { Logger.e(TAG, ERROR_WALLETKIT_AUTO_INIT_FAILED, err) - throw WalletKitBridgeException(ERROR_FAILED_AUTO_INIT_WALLETKIT + err.message) + throw TONWalletKitException.AutoInitializationFailed(err) } } } @@ -333,10 +333,8 @@ internal class InitializationManager( private companion object { private const val TAG = LogConstants.TAG_WEBVIEW_ENGINE private const val ERROR_WALLETKIT_AUTO_INIT_FAILED = "WalletKit auto-initialization failed" - private const val ERROR_FAILED_AUTO_INIT_WALLETKIT = "Failed to auto-initialize WalletKit: " private const val ERROR_FAILED_GET_APP_VERSION = "Failed to get app version, using default" private const val ERROR_FAILED_GET_APP_NAME = "Failed to get app name, using package name" - private const val ERROR_INIT_CONFIG_REQUIRED = "TONWalletKit.initialize() must be called before using the SDK." private const val DEFAULT_MAX_MESSAGES = 4 private val DEFAULT_SIGN_TYPES = listOf(SignDataType.TEXT, SignDataType.BINARY, SignDataType.CELL) } diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/MessageDispatcher.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/MessageDispatcher.kt index 7da95ce5..9346b6b9 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/MessageDispatcher.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/MessageDispatcher.kt @@ -31,6 +31,7 @@ import io.ton.walletkit.api.generated.TONStakingQuoteParams import io.ton.walletkit.api.generated.TONSwapParams import io.ton.walletkit.api.generated.TONSwapQuoteParams import io.ton.walletkit.api.generated.TONTransactionRequest +import io.ton.walletkit.bridge.BridgeDispatchException import io.ton.walletkit.bridge.dispatch.AdapterByIdRequest import io.ton.walletkit.bridge.dispatch.AdapterSignDataRequest import io.ton.walletkit.bridge.dispatch.AdapterSignTonProofRequest @@ -115,7 +116,7 @@ internal class MessageDispatcher( private val requestRegistry: BridgeRequestRegistry = BridgeRequestRegistry(json).apply { registerTypedJson(REQUEST_METHOD_SIGN_WITH_CUSTOM_SIGNER) { req -> val signer = signerManager.getSigner(req.signerId) - ?: throw IllegalArgumentException("Custom signer not found: ${req.signerId}") + ?: throw BridgeDispatchException.SignerNotFound(req.signerId) signer.sign(req.data).value } @@ -298,7 +299,7 @@ internal class MessageDispatcher( private fun requireAdapter(adapterId: String) = adapterManager.getAdapter(adapterId) - ?: throw IllegalArgumentException("Adapter not found: $adapterId") + ?: throw BridgeDispatchException.AdapterNotFound(adapterId) private fun respondToJs(id: String, result: String?, errorMessage: String?) { val envelope = buildJsonObject { diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/WebViewManager.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/WebViewManager.kt index 806c486e..f34ede6f 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/WebViewManager.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/infrastructure/WebViewManager.kt @@ -43,6 +43,7 @@ import io.ton.walletkit.api.generated.TONNetwork import io.ton.walletkit.api.generated.TONRawStackItem import io.ton.walletkit.api.generated.TONUserNFTsRequest import io.ton.walletkit.api.isTestnet +import io.ton.walletkit.bridge.BridgeDispatchException import io.ton.walletkit.bridge.BuildConfig import io.ton.walletkit.bridge.optString import io.ton.walletkit.bridge.optStringOrNull @@ -302,7 +303,7 @@ internal class WebViewManager( private fun requireAdapter(params: JsonObject) = adapterManager.getAdapter(params.optString("adapterId")) - ?: throw IllegalArgumentException("Adapter not found: ${params.optString("adapterId")}") + ?: throw BridgeDispatchException.AdapterNotFound(params.optString("adapterId")) private suspend fun dispatch(method: String, params: JsonObject): String { // Lazy so adapter-only calls (which carry no chainId) never trigger client resolution. @@ -346,19 +347,19 @@ internal class WebViewManager( } "api.getAccountStates" -> { val addresses = json.decodeFromJsonElement>( - params["addresses"] ?: throw IllegalArgumentException("api.getAccountStates: missing addresses"), + params["addresses"] ?: throw BridgeDispatchException.MissingParameter("api.getAccountStates", "addresses"), ).map { TONUserFriendlyAddress.parse(it) } json.encodeToString(client.accountStates(addresses).mapKeys { it.key.value }) } "api.nftItemsByAddress" -> { val request = json.decodeFromJsonElement( - params["request"] ?: throw IllegalArgumentException("api.nftItemsByAddress: missing request"), + params["request"] ?: throw BridgeDispatchException.MissingParameter("api.nftItemsByAddress", "request"), ) json.encodeToString(client.nftItemsByAddress(request)) } "api.nftItemsByOwner" -> { val request = json.decodeFromJsonElement( - params["request"] ?: throw IllegalArgumentException("api.nftItemsByOwner: missing request"), + params["request"] ?: throw BridgeDispatchException.MissingParameter("api.nftItemsByOwner", "request"), ) json.encodeToString(client.nftItemsByOwner(request)) } @@ -366,14 +367,14 @@ internal class WebViewManager( "api.resolveDnsWallet" -> client.resolveDnsWallet(params.optString("domain")) ?: "" "api.backResolveDnsWallet" -> client.backResolveDnsWallet(TONUserFriendlyAddress.parse(params.optString("address"))) ?: "" - else -> throw IllegalArgumentException("Unknown bridge method: $method") + else -> throw BridgeDispatchException.UnknownMethod(method) } } private fun clientForParams(params: JsonObject): TONAPIClient { val chainId = params.optString("chainId") return apiClients[TONNetwork(chainId)] - ?: throw IllegalArgumentException("No API client configured for chainId=$chainId") + ?: throw BridgeDispatchException.ApiClientNotConfigured(chainId) } // ======== Session Manager Methods ======== @@ -392,7 +393,7 @@ internal class WebViewManager( isJsBridge: Boolean, ): String { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() return runBlocking { try { @@ -425,7 +426,7 @@ internal class WebViewManager( @JavascriptInterface fun sessionGet(sessionId: String): String? { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() return runBlocking { try { @@ -442,7 +443,7 @@ internal class WebViewManager( @JavascriptInterface fun sessionGetFiltered(filterJson: String): String { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() return runBlocking { try { @@ -459,7 +460,7 @@ internal class WebViewManager( @JavascriptInterface fun sessionRemove(sessionId: String) { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() runBlocking { try { @@ -473,7 +474,7 @@ internal class WebViewManager( @JavascriptInterface fun sessionRemoveFiltered(filterJson: String) { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() runBlocking { try { @@ -488,7 +489,7 @@ internal class WebViewManager( @JavascriptInterface fun sessionClear() { val manager = sessionManager - ?: throw IllegalStateException("Session manager not configured") + ?: throw BridgeDispatchException.SessionManagerNotConfigured() runBlocking { try { diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/EventParser.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/EventParser.kt index c2c41e38..a71cc474 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/EventParser.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/EventParser.kt @@ -107,7 +107,7 @@ internal class EventParser( private inline fun decode(data: JsonObject): T = json.decodeFromBridge(data) private inline fun decodeUpdate(type: String, data: JsonObject): T? = try { - val update = data.optJsonObject("update") ?: error("Missing 'update' field") + val update = data.optJsonObject("update") ?: throw TONBridgeEventException.MissingField("update") json.decodeFromBridge(update) } catch (e: Exception) { Logger.e(TAG, "Failed to parse $type: ${e.message}", e) diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/TONBridgeEventException.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/TONBridgeEventException.kt new file mode 100644 index 00000000..f1fb1626 --- /dev/null +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/parsing/TONBridgeEventException.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 TonTech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.ton.walletkit.engine.parsing + +internal sealed class TONBridgeEventException(message: String) : Exception(message) { + class MissingField(field: String) : TONBridgeEventException("Missing '$field' field") +} diff --git a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/state/KotlinProviderRegistry.kt b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/state/KotlinProviderRegistry.kt index 34c56c9b..8caa2523 100644 --- a/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/state/KotlinProviderRegistry.kt +++ b/TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/engine/state/KotlinProviderRegistry.kt @@ -21,6 +21,7 @@ */ package io.ton.walletkit.engine.state +import io.ton.walletkit.bridge.BridgeDispatchException import io.ton.walletkit.internal.util.Logger import java.util.concurrent.ConcurrentHashMap @@ -56,6 +57,6 @@ internal abstract class KotlinProviderRegistry { */ protected fun require(providerId: String): T = providers[providerId] ?: run { Logger.w(tag, "No Kotlin provider registered for id=$providerId") - throw IllegalStateException("No Kotlin provider registered for id=$providerId") + throw BridgeDispatchException.ProviderNotRegistered(providerId) } }