Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/alexSDK.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from './errors'
import { Currency } from './currency';
import { getLiquidityProviderFee } from './helpers/FeeHelper';
import { getYAmountFromXAmount } from './helpers/RateHelper';
Expand Down Expand Up @@ -166,7 +167,7 @@ export class AlexSDK {
async getRoute(from: Currency, to: Currency): Promise<AMMRoute> {
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];
}
Expand Down
36 changes: 36 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions src/helpers/FeeHelper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from '../errors'
import { unwrapResponse } from 'clarity-codegen';
import { readonlyCall } from '../utils/readonlyCallExecutor';
import type { Currency } from '../currency';
Expand All @@ -17,7 +18,7 @@ export async function getLiquidityProviderFee(
): Promise<bigint> {
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;
Expand Down Expand Up @@ -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');
}
5 changes: 3 additions & 2 deletions src/helpers/RateHelper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from '../errors'
import { unwrapResponse } from 'clarity-codegen';
import { readonlyCall } from '../utils/readonlyCallExecutor';
import type { Currency } from '../currency';
Expand All @@ -18,7 +19,7 @@ export const getYAmountFromXAmount = async (
): Promise<bigint> => {
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;
Expand Down Expand Up @@ -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');
};
7 changes: 4 additions & 3 deletions src/helpers/SponsorTxHelper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from '../errors'
import { FungibleConditionCode } from '@stacks/transactions';
import { configs } from '../config';
import type { Currency } from '../currency';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions src/helpers/SwapHelper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from '../errors'
import {
type ClarityValue,
FungibleConditionCode,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Currency } from './currency';
export * from './alexSDK';
export { TokenInfo } from './types';
export { AlexSDKError, AlexErrorType } from './errors'
14 changes: 9 additions & 5 deletions src/utils/fetchData.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<AlexSDKResponse> {
return fetch(configs.SDK_API_HOST)
.then((r): Promise<AlexSDKResponse> => {
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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion src/utils/postConditions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AlexSDKError, AlexErrorType } from '../errors'
import {
addressToString,
FungibleConditionCode,
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { AlexSDKError, AlexErrorType } from '../errors'
export function isNotNull<T>(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<K extends string, V>(
Expand Down