From 9c9b21f3ac12323f62f41b9d96c95643d7c50e10 Mon Sep 17 00:00:00 2001 From: vwinee21 Date: Wed, 13 May 2026 09:17:56 +0700 Subject: [PATCH] fix: replace generic Error with typed AlexSDKError and fix fetchData bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AlexSDKError class (RFC 9457-inspired) with machine-readable type URI, title, HTTP-equivalent status, and detail string. Replace all throw new Error(...) with typed AlexSDKError instances. Also fix two bugs in fetchData.ts: - getPrices() reported 'Failed to fetch token mappings' instead of 'Failed to fetch token prices' (wrong message, wrong function) - getPrices() used any for response shape — replaced with typed RawPriceEntry / RawPricesResponse - HTTP status codes from upstream now surfaced in error detail Consumers can now instanceof AlexSDKError and branch on error.type. --- src/alexSDK.ts | 3 ++- src/errors.ts | 36 ++++++++++++++++++++++++++++++++++ src/helpers/FeeHelper.ts | 5 +++-- src/helpers/RateHelper.ts | 5 +++-- src/helpers/SponsorTxHelper.ts | 7 ++++--- src/helpers/SwapHelper.ts | 7 ++++--- src/index.ts | 1 + src/utils/fetchData.ts | 14 ++++++++----- src/utils/postConditions.ts | 3 ++- src/utils/utils.ts | 3 ++- 10 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/errors.ts diff --git a/src/alexSDK.ts b/src/alexSDK.ts index 0d952ae..3d16794 100644 --- a/src/alexSDK.ts +++ b/src/alexSDK.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from './errors' import { Currency } from './currency'; import { getLiquidityProviderFee } from './helpers/FeeHelper'; import { getYAmountFromXAmount } from './helpers/RateHelper'; @@ -166,7 +167,7 @@ export class AlexSDK { async getRoute(from: Currency, to: Currency): Promise { const allPossibleRoutes = await this.getAllPossibleRoutes(from, to); if (allPossibleRoutes.length === 0) { - throw new Error("Can't find route"); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Route Not Found', 404, "Can't find route"); } return allPossibleRoutes[0]; } diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..063d29d --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,36 @@ +/** + * Structured error class for AlexSDK, inspired by RFC 9457 Problem Details. + * Each error carries a machine-readable `type` URI, a human-readable `title`, + * an HTTP-equivalent `status` code, and a `detail` string with specifics. + * + * Consumers can distinguish error categories with `instanceof AlexSDKError` + * and branch on `error.type` for fine-grained handling. + * + * @example + * try { + * await sdk.getRate(Currency.STX, Currency.ALEX, 1_000_000n) + * } catch (e) { + * if (e instanceof AlexSDKError && e.type === AlexErrorType.RouteNotFound) { + * // handle gracefully + * } + * } + */ +export class AlexSDKError extends Error { + constructor( + public readonly type: string, + public readonly title: string, + public readonly status: number, + public readonly detail: string, + ) { + super(detail) + this.name = 'AlexSDKError' + } +} + +/** Error type URI namespace for AlexSDK. */ +export const AlexErrorType = { + FetchFailed: 'https://alexgo.io/errors/fetch-failed', + RouteNotFound: 'https://alexgo.io/errors/route-not-found', + TooManyPools: 'https://alexgo.io/errors/too-many-pools', + TokenMappingNotFound: 'https://alexgo.io/errors/token-mapping-not-found', +} as const diff --git a/src/helpers/FeeHelper.ts b/src/helpers/FeeHelper.ts index ec4d978..85a6909 100644 --- a/src/helpers/FeeHelper.ts +++ b/src/helpers/FeeHelper.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { unwrapResponse } from 'clarity-codegen'; import { readonlyCall } from '../utils/readonlyCallExecutor'; import type { Currency } from '../currency'; @@ -17,7 +18,7 @@ export async function getLiquidityProviderFee( ): Promise { const ammRoute = customRoute ?? resolveAmmRoute(tokenX, tokenY, pools); if (ammRoute.length === 0) { - throw new Error('No AMM pools in route'); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Route Not Found', 404, 'No AMM pools in route'); } if (hasLength(ammRoute, 1)) { const [segment] = ammRoute; @@ -63,5 +64,5 @@ export async function getLiquidityProviderFee( 'factor-w': segment4.pool.factor, }).then(unwrapResponse); } - throw new Error('Too many AMM pools in route'); + throw new AlexSDKError(AlexErrorType.TooManyPools, 'Route Too Complex', 422, 'Too many AMM pools in route: maximum supported is 4'); } diff --git a/src/helpers/RateHelper.ts b/src/helpers/RateHelper.ts index 5fd0c10..b6b6bc9 100644 --- a/src/helpers/RateHelper.ts +++ b/src/helpers/RateHelper.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { unwrapResponse } from 'clarity-codegen'; import { readonlyCall } from '../utils/readonlyCallExecutor'; import type { Currency } from '../currency'; @@ -18,7 +19,7 @@ export const getYAmountFromXAmount = async ( ): Promise => { const ammRoute = customRoute ?? resolveAmmRoute(tokenX, tokenY, ammPools); if (ammRoute.length === 0) { - throw new Error('No AMM pool found for the given route'); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Route Not Found', 404, 'No AMM pool found for the given route'); } if (hasLength(ammRoute, 1)) { const [segment] = ammRoute; @@ -68,5 +69,5 @@ export const getYAmountFromXAmount = async ( dx: fromAmount, }).then(unwrapResponse); } - throw new Error('Too many AMM pools in route'); + throw new AlexSDKError(AlexErrorType.TooManyPools, 'Route Too Complex', 422, 'Too many AMM pools in route: maximum supported is 4'); }; diff --git a/src/helpers/SponsorTxHelper.ts b/src/helpers/SponsorTxHelper.ts index a61109d..2b6e72e 100644 --- a/src/helpers/SponsorTxHelper.ts +++ b/src/helpers/SponsorTxHelper.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { FungibleConditionCode } from '@stacks/transactions'; import { configs } from '../config'; import type { Currency } from '../currency'; @@ -62,13 +63,13 @@ export function runSponsoredSpotTx( const getContractId = (currency: Currency) => { const mapping = mappings.find((x) => x.id === currency); if (!mapping) { - throw new Error(`Token mapping not found for currency: ${currency}`); + throw new AlexSDKError(AlexErrorType.TokenMappingNotFound, 'Token Mapping Not Found', 404, `Token mapping not found for currency: ${currency}`); } return mapping.wrapToken.split('::')[0] as `${string}.${string}`; }; const AlexVault = `${configs.CONTRACT_DEPLOYER}.amm-vault-v2-01`; if (ammRoute.length === 0) { - throw new Error("Can't find AMM route"); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Route Not Found', 404, "Can't find AMM route"); } const transfer = transferFactory(mappings); @@ -267,7 +268,7 @@ export function runSponsoredSpotTx( ); } - throw new Error('Too many AMM pools in route'); + throw new AlexSDKError(AlexErrorType.TooManyPools, 'Route Too Complex', 422, 'Too many AMM pools in route: maximum supported is 4'); } export enum SponsoredTxErrorCode { diff --git a/src/helpers/SwapHelper.ts b/src/helpers/SwapHelper.ts index a8b0bd5..d717690 100644 --- a/src/helpers/SwapHelper.ts +++ b/src/helpers/SwapHelper.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { type ClarityValue, FungibleConditionCode, @@ -73,13 +74,13 @@ export function runSpot( const getContractId = (currency: Currency) => { const mapping = mappings.find((x) => x.id === currency); if (!mapping) { - throw new Error(`Token mapping not found for currency: ${currency}`); + throw new AlexSDKError(AlexErrorType.TokenMappingNotFound, 'Token Mapping Not Found', 404, `Token mapping not found for currency: ${currency}`); } return mapping.wrapToken.split('::')[0] as `${string}.${string}`; }; const AlexVault = `${configs.CONTRACT_DEPLOYER}.amm-vault-v2-01`; if (ammRoute.length === 0) { - throw new Error("Can't find AMM route"); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Route Not Found', 404, "Can't find AMM route"); } const transfer = transferFactory(mappings); @@ -264,5 +265,5 @@ export function runSpot( ); } - throw new Error('Too many AMM pools in route'); + throw new AlexSDKError(AlexErrorType.TooManyPools, 'Route Too Complex', 422, 'Too many AMM pools in route: maximum supported is 4'); } diff --git a/src/index.ts b/src/index.ts index 654e7fa..ddc7354 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export { Currency } from './currency'; export * from './alexSDK'; export { TokenInfo } from './types'; +export { AlexSDKError, AlexErrorType } from './errors' diff --git a/src/utils/fetchData.ts b/src/utils/fetchData.ts index 5a5b892..c3d49c3 100644 --- a/src/utils/fetchData.ts +++ b/src/utils/fetchData.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { Currency } from '../currency'; import type { AddressBalanceResponse } from '@stacks/stacks-blockchain-api-types'; import { configs } from '../config'; @@ -17,13 +18,16 @@ import { unwrapResponse, } from 'clarity-codegen'; +type RawPriceEntry = { contract_id: string; last_price_usd: number } +type RawPricesResponse = { data: RawPriceEntry[] } + export async function getAlexSDKData(): Promise { return fetch(configs.SDK_API_HOST) .then((r): Promise => { if (r.ok) { return r.json(); } - throw new Error('Failed to fetch token mappings'); + throw new AlexSDKError(AlexErrorType.FetchFailed, 'SDK Data Fetch Failed', r.status, `Failed to fetch SDK data: HTTP ${r.status}`); }) .then((x) => { for (const a of x.pools) { @@ -42,11 +46,11 @@ export async function getPrices( if (r.ok) { return r.json(); } - throw new Error('Failed to fetch token mappings'); + throw new AlexSDKError(AlexErrorType.FetchFailed, 'Token Prices Fetch Failed', r.status, `Failed to fetch token prices: HTTP ${r.status}`); }) - .then((x: any) => + .then((x: RawPricesResponse) => x.data - .map((a: any): PriceData | null => { + .map((a: RawPriceEntry): PriceData | null => { if (a.contract_id === 'STX') { return { token: Currency.STX, @@ -76,7 +80,7 @@ export async function fetchBalanceForAccount( `${configs.STACKS_API_HOST}/extended/v1/address/${stxAddress}/balances` ); if (!response.ok) { - throw new Error('Failed to fetch account balances'); + throw new AlexSDKError(AlexErrorType.FetchFailed, 'Account Balances Fetch Failed', response.status, `Failed to fetch account balances: HTTP ${response.status}`); } const balanceData: AddressBalanceResponse = await response.json(); return fromEntries( diff --git a/src/utils/postConditions.ts b/src/utils/postConditions.ts index 6b94547..6d0b757 100644 --- a/src/utils/postConditions.ts +++ b/src/utils/postConditions.ts @@ -1,3 +1,4 @@ +import { AlexSDKError, AlexErrorType } from '../errors' import { addressToString, FungibleConditionCode, @@ -32,7 +33,7 @@ export const transferFactory = ): PostCondition[] => { const mapping = tokenMapping.find((m) => m.id === currency); if (!mapping) { - throw new Error('Token mapping not found'); + throw new AlexSDKError(AlexErrorType.TokenMappingNotFound, 'Token Mapping Not Found', 404, 'Token mapping not found'); } const scale = BigInt(10 ** mapping.underlyingTokenDecimals); const nativeAmount = (amount * BigInt(scale)) / BigInt(1e8); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 44e8bd3..a5ca155 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,9 +1,10 @@ +import { AlexSDKError, AlexErrorType } from '../errors' export function isNotNull(input: T | undefined | null): input is T { return input != null; } export function assertNever(x: never): never { - throw new Error('Unexpected object: ' + x); + throw new AlexSDKError(AlexErrorType.RouteNotFound, 'Unexpected Value', 500, 'Unexpected object: ' + x); } export function fromEntries(