Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const mapCryptoOnrampError = (error: unknown): string => {
return 'cryptoOnramp.quoteError';
case CryptoOnrampErrorCode.ProviderError:
return 'cryptoOnramp.providerError';
case CryptoOnrampErrorCode.DepositFailed:
return 'cryptoOnramp.depositFailed';
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/appkit-react/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ 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',
Expand Down
15 changes: 5 additions & 10 deletions packages/walletkit/src/defi/DefiManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<E>;

constructor(createFactoryContext: () => ProviderFactoryContext<E>) {
Expand All @@ -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);
Expand Down Expand Up @@ -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),
});
Expand All @@ -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),
});
Expand Down
14 changes: 7 additions & 7 deletions packages/walletkit/src/defi/crypto-onramp/CryptoOnrampManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ import type {
CryptoOnrampStatusParams,
CryptoOnrampSupportedCurrencies,
} from '../../api/models';
import type { CryptoOnrampErrorCode } from './errors';
import { CryptoOnrampError } from './errors';
import { CryptoOnrampError, CryptoOnrampErrorCode } from './errors';
import { globalLogger } from '../../core/Logger';
import { DefiManager } from '../DefiManager';

Expand Down Expand Up @@ -113,7 +112,12 @@ export class CryptoOnrampManager extends DefiManager<CryptoOnrampProviderInterfa
return deposit;
} catch (error) {
log.error('Failed to create crypto onramp deposit', { error, params });
throw error;
if (error instanceof CryptoOnrampError) throw error;
throw new CryptoOnrampError(
'Failed to create crypto onramp deposit',
CryptoOnrampErrorCode.DepositFailed,
error,
);
}
}

Expand Down Expand Up @@ -160,8 +164,4 @@ export class CryptoOnrampManager extends DefiManager<CryptoOnrampProviderInterfa
throw error;
}
}

protected createError(message: string, code: CryptoOnrampErrorCode, details?: unknown): CryptoOnrampError {
return new CryptoOnrampError(message, code, details);
}
}
1 change: 1 addition & 0 deletions packages/walletkit/src/defi/crypto-onramp/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DefiError } from '../errors';
export enum CryptoOnrampErrorCode {
ProviderError = 'PROVIDER_ERROR',
QuoteFailed = 'QUOTE_FAILED',
DepositFailed = 'DEPOSIT_FAILED',
RefundAddressRequired = 'REFUND_ADDRESS_REQUIRED',
InvalidRefundAddress = 'INVALID_REFUND_ADDRESS',
ReversedAmountNotSupported = 'REVERSED_AMOUNT_NOT_SUPPORTED',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ import {
DEFAULT_LAYERSWAP_SUPPORTED_CHAINS,
LAYERSWAP_DESTINATION_NETWORK,
LAYERSWAP_DESTINATION_TOKENS,
formatBaseUnits,
isErrorResponse,
mapLayerswapErrorCode,
mapStatus,
parseBaseUnits,
} from './utils';
import type { LayerswapChainConfig } from './utils';
import { formatUnits, parseUnits } from '../../../utils/units';

const LAYERSWAP_API_URL = 'https://api.layerswap.io/api/v2';

Expand Down Expand Up @@ -145,7 +144,16 @@ export class LayerswapCryptoOnrampProvider extends CryptoOnrampProvider<undefine
);
}

const amountDecimal = formatBaseUnits(params.amount, sourceCurrency.decimals);
const baseUnits = BigInt(params.amount);

if (baseUnits < 0n) {
throw new CryptoOnrampError(
`Layerswap: amount "${params.amount}" is not a non-negative integer in base units`,
CryptoOnrampErrorCode.InvalidParams,
);
}

const amountDecimal = formatUnits(baseUnits, sourceCurrency.decimals);

const body = {
amount: amountDecimal,
Expand Down Expand Up @@ -198,7 +206,11 @@ export class LayerswapCryptoOnrampProvider extends CryptoOnrampProvider<undefine
);
}

const targetAmountBaseUnits = parseBaseUnits(data.quote.receive_amount, targetCurrency.decimals);
const targetAmountBaseUnits = parseUnits(
data.quote.receive_amount.toString(),
targetCurrency.decimals,
).toString();

const rate =
data.quote.requested_amount > 0
? (data.quote.receive_amount / data.quote.requested_amount).toString()
Expand Down Expand Up @@ -320,8 +332,10 @@ export class LayerswapCryptoOnrampProvider extends CryptoOnrampProvider<undefine
);

const sourceMap = new Map<string, CryptoOnrampSourceCurrency>();
let anyFulfilled = false;
for (const result of results) {
if (result.status !== 'fulfilled') continue;
anyFulfilled = true;
for (const network of result.value) {
const caip2 = slugToCaip2[network.name];
if (!caip2) continue;
Expand All @@ -334,6 +348,17 @@ export class LayerswapCryptoOnrampProvider extends CryptoOnrampProvider<undefine
}
}

if (!anyFulfilled && results.length > 0) {
const firstRejection = results.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
);
throw new CryptoOnrampError(
'Layerswap: failed to fetch supported sources for all destinations',
CryptoOnrampErrorCode.ProviderError,
firstRejection?.reason,
);
}

return { source: Array.from(sourceMap.values()), destination };
}

Expand Down
30 changes: 0 additions & 30 deletions packages/walletkit/src/defi/crypto-onramp/layerswap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,33 +129,3 @@ export const mapStatus = (status: LayerswapSwapStatus | string): CryptoOnrampSta
return 'pending';
}
};

/**
* Format a base-units integer string into a decimal token-units string.
* e.g. formatBaseUnits('2000000', 6) === '2'
*/
export const formatBaseUnits = (base: string, decimals: number): string => {
if (!/^\d+$/.test(base)) {
throw new Error(`formatBaseUnits: not a non-negative integer string: "${base}"`);
}
if (decimals === 0) return base;
const padded = base.padStart(decimals + 1, '0');
const whole = padded.slice(0, padded.length - decimals);
const frac = padded.slice(padded.length - decimals).replace(/0+$/, '');
return frac.length > 0 ? `${whole}.${frac}` : whole;
};

/**
* Scale a decimal token-units string by 10^decimals and return the integer
* base-units string, truncating any excess fractional digits.
*/
export const parseBaseUnits = (value: number | string, decimals: number): string => {
const str = typeof value === 'number' ? value.toString() : value;
if (!/^\d+(\.\d+)?$/.test(str)) {
throw new Error(`parseBaseUnits: not a non-negative decimal: "${str}"`);
}
const [whole, frac = ''] = str.split('.');
const truncated = frac.slice(0, decimals).padEnd(decimals, '0');
const combined = `${whole}${truncated}`.replace(/^0+/, '');
return combined.length > 0 ? combined : '0';
};
5 changes: 0 additions & 5 deletions packages/walletkit/src/defi/onramp/OnrampManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import type { OnrampAPI, OnrampProviderInterface } from '../../api/interfaces';
import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../api/models';
import { OnrampError } from './errors';
import { globalLogger } from '../../core/Logger';
import { DefiManager } from '../DefiManager';

Expand Down Expand Up @@ -124,8 +123,4 @@ export class OnrampManager extends DefiManager<OnrampProviderInterface> implemen
throw error;
}
}

protected createError(message: string, code: string, details?: unknown): OnrampError {
return new OnrampError(message, code, details);
}
}
15 changes: 10 additions & 5 deletions packages/walletkit/src/defi/onramp/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@

import { DefiError } from '../errors';

export enum OnrampErrorCode {
ProviderError = 'PROVIDER_ERROR',
InvalidParams = 'INVALID_ONRAMP_PARAMS',
QuoteFailed = 'QUOTE_FAILED',
UrlBuildFailed = 'URL_BUILD_FAILED',
}

export class OnrampError extends DefiError {
static readonly PROVIDER_ERROR = 'PROVIDER_ERROR';
static readonly InvalidParams = 'INVALID_ONRAMP_PARAMS';
static readonly QUOTE_FAILED = 'QUOTE_FAILED';
static readonly URL_BUILD_FAILED = 'URL_BUILD_FAILED';
public readonly code: OnrampErrorCode;

constructor(message: string, code: string, details?: unknown) {
constructor(message: string, code: OnrampErrorCode, details?: unknown) {
super(message, code, details);
this.name = 'OnrampError';
this.code = code;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models';
import { Network } from '../../../api/models';
import { OnrampProvider } from '../OnrampProvider';
import { OnrampError } from '../errors';
import { OnrampError, OnrampErrorCode } from '../errors';

/**
* Custom options for Mercuryo requests
Expand Down Expand Up @@ -101,7 +101,7 @@ export class MercuryoProvider extends OnrampProvider<MercuryoQuoteOptions, Mercu
metadata: data.data,
};
} catch (error) {
throw new OnrampError('Failed to get Mercuryo quote', OnrampError.QUOTE_FAILED, error);
throw new OnrampError('Failed to get Mercuryo quote', OnrampErrorCode.QuoteFailed, error);
}
}

Expand Down Expand Up @@ -134,7 +134,7 @@ export class MercuryoProvider extends OnrampProvider<MercuryoQuoteOptions, Mercu

return url.toString();
} catch (error) {
throw new OnrampError('Failed to build Mercuryo URL', OnrampError.URL_BUILD_FAILED, error);
throw new OnrampError('Failed to build Mercuryo URL', OnrampErrorCode.UrlBuildFailed, error);
}
}
}
8 changes: 4 additions & 4 deletions packages/walletkit/src/defi/onramp/moonpay/MoonpayProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models';
import { Network } from '../../../api/models';
import { OnrampProvider } from '../OnrampProvider';
import { OnrampError } from '../errors';
import { OnrampError, OnrampErrorCode } from '../errors';

/**
* Custom options for Moonpay requests
Expand Down Expand Up @@ -48,7 +48,7 @@ export class MoonpayProvider extends OnrampProvider<MoonpayQuoteOptions, Moonpay
constructor(apiKey: string) {
super();
if (!apiKey) {
throw new OnrampError('Moonpay API key is required', OnrampError.PROVIDER_ERROR);
throw new OnrampError('Moonpay API key is required', OnrampErrorCode.ProviderError);
}
this.apiKey = apiKey;
}
Expand Down Expand Up @@ -89,7 +89,7 @@ export class MoonpayProvider extends OnrampProvider<MoonpayQuoteOptions, Moonpay
metadata: data,
};
} catch (error) {
throw new OnrampError('Failed to get Moonpay quote', OnrampError.QUOTE_FAILED, error);
throw new OnrampError('Failed to get Moonpay quote', OnrampErrorCode.QuoteFailed, error);
}
}

Expand All @@ -116,7 +116,7 @@ export class MoonpayProvider extends OnrampProvider<MoonpayQuoteOptions, Moonpay

return url.toString();
} catch (error) {
throw new OnrampError('Failed to build Moonpay URL', OnrampError.URL_BUILD_FAILED, error);
throw new OnrampError('Failed to build Moonpay URL', OnrampErrorCode.UrlBuildFailed, error);
}
}
}
6 changes: 3 additions & 3 deletions packages/walletkit/src/defi/onramp/ton-pay/TonPayProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type { OnrampParams, OnrampQuote, OnrampQuoteParams } from '../../../api/models';
import { Network } from '../../../api/models';
import { OnrampProvider } from '../OnrampProvider';
import { OnrampError } from '../errors';
import { OnrampError, OnrampErrorCode } from '../errors';

/**
* Custom options for TonPay requests
Expand Down Expand Up @@ -79,7 +79,7 @@ export class TonPayProvider extends OnrampProvider<TonPayQuoteOptions, TonPayOnr
metadata: data,
};
} catch (error) {
throw new OnrampError('Failed to get TonPay quote', OnrampError.QUOTE_FAILED, error);
throw new OnrampError('Failed to get TonPay quote', OnrampErrorCode.QuoteFailed, error);
}
}

Expand Down Expand Up @@ -117,7 +117,7 @@ export class TonPayProvider extends OnrampProvider<TonPayQuoteOptions, TonPayOnr

return data.link;
} catch (error) {
throw new OnrampError('Failed to build TonPay URL', OnrampError.URL_BUILD_FAILED, error);
throw new OnrampError('Failed to build TonPay URL', OnrampErrorCode.UrlBuildFailed, error);
}
}
}
Loading
Loading