diff --git a/.changeset/defi-error-handling.md b/.changeset/defi-error-handling.md new file mode 100644 index 000000000..27895b6a7 --- /dev/null +++ b/.changeset/defi-error-handling.md @@ -0,0 +1,7 @@ +--- +'@ton/walletkit': patch +'@ton/appkit': patch +'@ton/appkit-react': patch +--- + +DeFi operations (swap, staking, crypto-onramp, gasless) now always throw a typed `DefiError` on failure — a specific subclass (`SwapError`, `StakingError`, `CryptoOnrampError`, `GaslessError`) when available, otherwise a `DefiError` with the new `DefiErrorCode.Unknown` code (the original error is preserved in `details`). Failures can be reliably caught and branched on by `code`. diff --git a/packages/appkit-react/src/features/onramp/widgets/crypto-onramp/utils/map-crypto-onramp-error.ts b/packages/appkit-react/src/features/onramp/widgets/crypto-onramp/utils/map-crypto-onramp-error.ts index 07fa28d87..226a2268c 100644 --- a/packages/appkit-react/src/features/onramp/widgets/crypto-onramp/utils/map-crypto-onramp-error.ts +++ b/packages/appkit-react/src/features/onramp/widgets/crypto-onramp/utils/map-crypto-onramp-error.ts @@ -12,8 +12,8 @@ import { mapDefiError } from '../../../../../utils/map-defi-error'; /** * Map a thrown crypto-onramp error to an i18n key. Tries crypto-onramp-specific codes first, - * falls back to the shared {@link mapDefiError} for base DeFi codes, and finally to a generic - * `cryptoOnramp.genericError`. + * falls back to the shared {@link mapDefiError} for base DeFi codes, and finally to the shared + * `defi.genericError`. */ export const mapCryptoOnrampError = (error: unknown): string => { if (error instanceof CryptoOnrampError) { @@ -40,8 +40,10 @@ export const mapCryptoOnrampError = (error: unknown): string => { return 'cryptoOnramp.quoteError'; case CryptoOnrampErrorCode.ProviderError: return 'cryptoOnramp.providerError'; + case CryptoOnrampErrorCode.DepositFailed: + return 'cryptoOnramp.depositFailed'; } } - return mapDefiError(error) ?? 'cryptoOnramp.genericError'; + return mapDefiError(error) ?? 'defi.genericError'; }; diff --git a/packages/appkit-react/src/locales/en.ts b/packages/appkit-react/src/locales/en.ts index 4ec926731..a5bb1c2fb 100644 --- a/packages/appkit-react/src/locales/en.ts +++ b/packages/appkit-react/src/locales/en.ts @@ -64,6 +64,7 @@ export default { noDefaultProvider: 'No provider configured', invalidProvider: 'Invalid provider', invalidParams: 'Invalid parameters', + genericError: 'Something went wrong', }, // Swap @@ -137,9 +138,9 @@ export default { selectMethod: 'Select payment method', searchMethod: 'Search', quoteError: 'Failed to get a quote', + depositFailed: 'Failed to create deposit', tooManyDecimals: 'Too many decimals', providerError: 'Provider error', - genericError: 'Something went wrong', addressTab: 'Address', memoTab: 'Memo', youGet: 'You get', diff --git a/packages/appkit-react/src/utils/map-defi-error.ts b/packages/appkit-react/src/utils/map-defi-error.ts index f46fec947..03b11d1bd 100644 --- a/packages/appkit-react/src/utils/map-defi-error.ts +++ b/packages/appkit-react/src/utils/map-defi-error.ts @@ -29,6 +29,8 @@ export const mapDefiError = (error: unknown): string | null => { return 'defi.invalidProvider'; case DefiErrorCode.InvalidParams: return 'defi.invalidParams'; + case DefiErrorCode.Unknown: + return 'defi.genericError'; default: return null; } diff --git a/packages/walletkit/src/defi/DefiManager.ts b/packages/walletkit/src/defi/DefiManager.ts index e94dc46aa..647dd463c 100644 --- a/packages/walletkit/src/defi/DefiManager.ts +++ b/packages/walletkit/src/defi/DefiManager.ts @@ -11,8 +11,7 @@ import type { DefiProvider } from '../api/interfaces'; import { resolveProvider } from '../types'; import type { ProviderInput } from '../types'; import type { ProviderFactoryContext } from '../types/factory'; -import type { DefiError } from './errors'; -import { DefiErrorCode } from './errors'; +import { DefiError, DefiErrorCode } from './errors'; import type { SharedKitEvents } from '../types/emitter'; import type { EventEmitter } from '../core/EventEmitter'; @@ -24,7 +23,6 @@ export abstract class DefiManager< protected providers: T[] = []; protected defaultProviderId?: string; - protected abstract createError(message: string, code: string, details?: unknown): DefiError; protected eventEmitter: EventEmitter; constructor(createFactoryContext: () => ProviderFactoryContext) { @@ -44,7 +42,7 @@ export abstract class DefiManager< const providerId = provider.providerId; if (!providerId) { - throw this.createError('Provider must have a providerId', DefiErrorCode.InvalidProvider); + throw new DefiError('Provider must have a providerId', DefiErrorCode.InvalidProvider); } const oldProvider = this.providers.find((p) => p.providerId === providerId); @@ -84,7 +82,7 @@ export abstract class DefiManager< const provider = this.providers.find((p) => p.providerId === providerId); if (!provider) { - throw this.createError(`Provider '${providerId}' not found`, DefiErrorCode.ProviderNotFound, { + throw new DefiError(`Provider '${providerId}' not found`, DefiErrorCode.ProviderNotFound, { provider: providerId, registered: this.providers.map((p) => p.providerId), }); @@ -104,15 +102,12 @@ export abstract class DefiManager< const providerName = providerId || this.defaultProviderId; if (!providerName) { - throw this.createError( - 'No default provider set. Register a provider first.', - DefiErrorCode.NoDefaultProvider, - ); + 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, { + throw new DefiError(`Provider '${providerName}' not found`, DefiErrorCode.ProviderNotFound, { provider: providerName, registered: this.providers.map((p) => p.providerId), }); diff --git a/packages/walletkit/src/defi/crypto-onramp/CryptoOnrampManager.ts b/packages/walletkit/src/defi/crypto-onramp/CryptoOnrampManager.ts index 59c239473..9140e477c 100644 --- a/packages/walletkit/src/defi/crypto-onramp/CryptoOnrampManager.ts +++ b/packages/walletkit/src/defi/crypto-onramp/CryptoOnrampManager.ts @@ -17,8 +17,7 @@ import type { CryptoOnrampStatusParams, CryptoOnrampSupportedCurrencies, } from '../../api/models'; -import type { CryptoOnrampErrorCode } from './errors'; -import { CryptoOnrampError } from './errors'; +import { toDefiError } from '../errors'; import { globalLogger } from '../../core/Logger'; import { DefiManager } from '../DefiManager'; @@ -43,7 +42,7 @@ export class CryptoOnrampManager extends DefiManager { expect(manager.getProvider('first')).toBe(first); }); - it('throws GaslessError when the providerId is not registered', () => { + it('throws DefiError when the providerId is not registered', () => { const { manager } = makeManager(); manager.registerProvider(makeProvider('first')); - expect(() => manager.setDefaultProvider('missing')).toThrow(GaslessError); + expect(() => manager.setDefaultProvider('missing')).toThrow(DefiError); }); }); diff --git a/packages/walletkit/src/defi/gasless/GaslessManager.ts b/packages/walletkit/src/defi/gasless/GaslessManager.ts index 550a579ae..6c61ed469 100644 --- a/packages/walletkit/src/defi/gasless/GaslessManager.ts +++ b/packages/walletkit/src/defi/gasless/GaslessManager.ts @@ -19,8 +19,7 @@ import type { import { globalLogger } from '../../core/Logger'; import type { ProviderFactoryContext } from '../../types/factory'; import { DefiManager } from '../DefiManager'; -import type { GaslessErrorCode } from './errors'; -import { GaslessError } from './errors'; +import { toDefiError } from '../errors'; const log = globalLogger.createChild('GaslessManager'); @@ -46,7 +45,7 @@ export class GaslessManager extends DefiManager implem return await this.getProvider(selectedProviderId).getMetadata(); } catch (error) { log.error('Failed to get gasless provider metadata', { error }); - throw error; + throw toDefiError(error, 'Failed to get gasless provider metadata'); } } @@ -67,7 +66,7 @@ export class GaslessManager extends DefiManager implem return await provider.getConfig(targetNetwork); } catch (error) { log.error('Failed to get gasless config', { error }); - throw error; + throw toDefiError(error, 'Failed to get gasless config'); } } @@ -87,7 +86,7 @@ export class GaslessManager extends DefiManager implem return await this.getProvider(providerId ?? this.defaultProviderId).getQuote(params); } catch (error) { log.error('Failed to quote gasless transaction', { error, params }); - throw error; + throw toDefiError(error, 'Failed to quote gasless transaction'); } } @@ -104,11 +103,7 @@ export class GaslessManager extends DefiManager implem return await this.getProvider(providerId ?? this.defaultProviderId).sendTransaction(params); } catch (error) { log.error('Failed to send gasless transaction', { error }); - throw error; + throw toDefiError(error, 'Failed to send gasless transaction'); } } - - protected createError(message: string, code: string, details?: unknown): GaslessError { - return new GaslessError(message, code as GaslessErrorCode, details); - } } diff --git a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts index c73bcb970..e08c94bf2 100644 --- a/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts +++ b/packages/walletkit/src/defi/gasless/tonapi/TonApiGaslessProvider.ts @@ -42,6 +42,7 @@ import type { TonApiGaslessChainConfig, TonApiGaslessProviderConfig } from './mo import type { TonApiGaslessConfig } from './types/config'; import type { TonApiGaslessEstimateResponse } from './types/estimate'; import type { TonApiGaslessSendResponse } from './types/send'; +import { DefiErrorCode, DefiError } from '../../errors'; const log = globalLogger.createChild('TonApiGaslessProvider'); @@ -135,8 +136,9 @@ export class TonApiGaslessProvider extends GaslessProvider { } if (Object.keys(chainConfig).length === 0) { - throw new Error( + throw new DefiError( 'createTonApiGaslessProvider: no eligible networks (configure at least one network in the kit, or pass `chains` matching a configured network)', + DefiErrorCode.InvalidParams, ); } diff --git a/packages/walletkit/src/defi/staking/StakingManager.ts b/packages/walletkit/src/defi/staking/StakingManager.ts index 9abb6799f..cb8714d65 100644 --- a/packages/walletkit/src/defi/staking/StakingManager.ts +++ b/packages/walletkit/src/defi/staking/StakingManager.ts @@ -16,9 +16,9 @@ import type { StakingQuote, } from '../../api/models'; import type { StakingAPI, StakingProviderInterface } from '../../api/interfaces'; -import { StakingError, StakingErrorCode } from './errors'; import { globalLogger } from '../../core/Logger'; import { DefiManager } from '../DefiManager'; +import { toDefiError } from '../errors'; import type { ProviderFactoryContext } from '../../types/factory'; const log = globalLogger.createChild('StakingManager'); @@ -47,7 +47,7 @@ export class StakingManager extends DefiManager implem return quote; } catch (error) { log.error('Failed to get staking quote', { error, params }); - throw error; + throw toDefiError(error, 'Failed to get staking quote'); } } @@ -62,7 +62,7 @@ export class StakingManager extends DefiManager implem return await this.getProvider(providerId).buildStakeTransaction(params); } catch (error) { log.error('Failed to build staking transaction', { error, params }); - throw error; + throw toDefiError(error, 'Failed to build staking transaction'); } } @@ -87,7 +87,7 @@ export class StakingManager extends DefiManager implem return await this.getProvider(providerId).getStakedBalance(userAddress, network); } catch (error) { log.error('Failed to get staking balance', { error, userAddress, network }); - throw error; + throw toDefiError(error, 'Failed to get staking balance'); } } @@ -106,7 +106,7 @@ export class StakingManager extends DefiManager implem return await this.getProvider(providerId).getStakingProviderInfo(network); } catch (error) { log.error('Failed to get staking info', { error, network }); - throw error; + throw toDefiError(error, 'Failed to get staking info'); } } @@ -125,15 +125,7 @@ export class StakingManager extends DefiManager implem return this.getProvider(providerId).getStakingProviderMetadata(network); } catch (error) { log.error('Failed to get staking metadata', { error, network }); - throw error; + throw toDefiError(error, 'Failed to get staking metadata'); } } - - protected createError(message: string, code: string, details?: unknown): StakingError { - const errorCode = Object.values(StakingErrorCode).includes(code as StakingErrorCode) - ? (code as StakingErrorCode) - : StakingErrorCode.InvalidParams; - log.error(message, { code, details }); - return new StakingError(message, errorCode, details); - } } diff --git a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts index ac1b91a48..cb1df81cb 100644 --- a/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts +++ b/packages/walletkit/src/defi/staking/tonstakers/TonStakersStakingProvider.ts @@ -20,6 +20,7 @@ import type { import { globalLogger } from '../../../core/Logger'; import { StakingProvider } from '../StakingProvider'; import { StakingError, StakingErrorCode } from '../errors'; +import { DefiError, DefiErrorCode } from '../../errors'; import type { TonStakersChainConfig, TonStakersProviderConfig } from './models/TonStakersProviderConfig'; import type { ProviderFactoryContext } from '../../../types/factory'; import type { NetworkManager } from '../../../core/NetworkManager'; @@ -109,8 +110,9 @@ export class TonStakersStakingProvider extends StakingProvider { } if (Object.keys(chainConfig).length === 0) { - throw new Error( + throw new DefiError( 'createTonstakersProvider: no eligible networks (add mainnet/testnet or pass metadata.contractAddress in overrides)', + DefiErrorCode.InvalidParams, ); } @@ -485,7 +487,7 @@ export class TonStakersStakingProvider extends StakingProvider { const poolInfo = await client.getJson<{ pool: { apy: number } }>(`/v2/staking/pool/${address}`); if (!poolInfo?.pool?.apy) { - throw new Error('Invalid APY data from TonAPI'); + throw new StakingError('Invalid APY data from TonAPI', StakingErrorCode.InvalidParams); } return Number(poolInfo.pool.apy); diff --git a/packages/walletkit/src/defi/swap/SwapManager.ts b/packages/walletkit/src/defi/swap/SwapManager.ts index 3ae1da73a..3bff0d0b4 100644 --- a/packages/walletkit/src/defi/swap/SwapManager.ts +++ b/packages/walletkit/src/defi/swap/SwapManager.ts @@ -9,10 +9,9 @@ import type { TransactionRequest } from '../../api/models'; import type { SwapAPI, SwapProviderInterface } from '../../api/interfaces'; import type { SwapQuoteParams, SwapQuote, SwapParams } from '../../api/models'; -import type { SwapErrorCode } from './errors'; -import { SwapError } from './errors'; import { globalLogger } from '../../core/Logger'; import { DefiManager } from '../DefiManager'; +import { toDefiError } from '../errors'; import type { ProviderFactoryContext } from '../../types/factory'; const log = globalLogger.createChild('SwapManager'); @@ -58,7 +57,7 @@ export class SwapManager extends DefiManager implements S return quote; } catch (error) { log.error('Failed to get swap quote', { error, params }); - throw error; + throw toDefiError(error, 'Failed to get swap quote'); } } @@ -85,11 +84,7 @@ export class SwapManager extends DefiManager implements S return transaction; } catch (error) { log.error('Failed to build swap transaction', { error, params }); - throw error; + throw toDefiError(error, 'Failed to build swap transaction'); } } - - protected createError(message: string, code: string, details?: unknown): SwapError { - return new SwapError(message, code as SwapErrorCode, details); - } }