Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add alexSDK.getAllPossibleRoutesWithDetails #27

Merged
merged 1 commit into from
Mar 5, 2025
Merged
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
123 changes: 116 additions & 7 deletions src/alexSDK.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Currency } from './currency';
import { runSpot, type TxToBroadCast } from './helpers/SwapHelper';
import { getLiquidityProviderFee } from './helpers/FeeHelper';
import type { AlexSDKResponse, PoolData, PriceData, TokenInfo } from './types';
import {
deserializeAssetIdentifier,
type AlexSDKResponse,
type DetailedAMMRoutes as DetailedAMMRoute,
type DetailedAMMRoutes,
type PoolData,
type PriceData,
type StacksAssetContractAddress,
type TokenInfo,
} from './types';
import {
fetchBalanceForAccount,
getAlexSDKData,
getPrices,
} from './utils/fetchData';
import { getAllPossibleRoute } from './helpers/RouteHelper';
import { getYAmountFromXAmount } from './helpers/RateHelper';
import { fromEntries } from './utils/utils';
import { fromEntries, isNotNull } from './utils/utils';
import type { AMMRoute } from './utils/ammRouteResolver';
import {
broadcastSponsoredTx,
Expand All @@ -19,6 +28,8 @@ import {
SponsoredTxError,
SponsoredTxErrorCode,
} from './helpers/SponsorTxHelper';
import { props } from './utils/promiseHelpers';
import { announceAtLeastOne, hasLength } from './utils/arrayHelper';

/**
* The AlexSDK class provides methods for interacting with a decentralized exchange (DEX) system,
Expand Down Expand Up @@ -96,6 +107,104 @@ export class AlexSDK {
return await getAllPossibleRoute(from, to, await this.getPools());
}

/**
* This function returns all possible routes for swapping between two specified currencies,
* along with additional details for each route.
*
* @param {Currency} from - The currency to swap from.
* @param {Currency} to - The currency to swap to.
* @returns {Promise<DetailedAMMRoute[]>} - A promise that resolves to an array of DetailedAMMRoute objects,
* representing all possible swap routes between the two specified currencies with additional details.
*/
async getAllPossibleRoutesWithDetails(
from: Currency,
to: Currency
): Promise<DetailedAMMRoute[]> {
const routes = await this.getAllPossibleRoutes(from, to);
const detailedRoutes = await Promise.all(
routes.map((r) => this.getDetailedRoute(r))
);
return detailedRoutes.filter(isNotNull);
}

/**
* This function returns a detailed route for a given AMMRoute.
*
* @param {AMMRoute} route - The AMM route to get details for.
* @returns {Promise<undefined | DetailedAMMRoute>} - A promise that resolves to a DetailedAMMRoute object
* if the route details can be fetched, or undefined if any token information is missing.
*
* The function processes each segment of the route, fetching token information and constructing
* a detailed representation of the swap path. It includes information such as the from and to
* currencies, token addresses, and pool details for each step in the route.
*/
async getDetailedRoute(
route: AMMRoute
): Promise<undefined | DetailedAMMRoute> {
const detailRoute = await Promise.all(
route.map(
async (
segment
): Promise<
| undefined
| (DetailedAMMRoutes['swapPools'][number] & {
fromCurrency: Currency;
fromTokenAddress: StacksAssetContractAddress;
})
> => {
const [fromTokenInfo, toTokenInfo] = await Promise.all([
this.fetchTokenInfo(segment.from),
this.fetchTokenInfo(segment.neighbour),
]);
if (fromTokenInfo == null || toTokenInfo == null) {
return undefined;
}

const fromTokenAddress = deserializeAssetIdentifier(
fromTokenInfo.wrapToken
);
const toTokenAddress = deserializeAssetIdentifier(
toTokenInfo.wrapToken
);
if (fromTokenAddress == null || toTokenAddress == null) {
return undefined;
}

return {
fromCurrency: segment.from,
fromTokenAddress,
toCurrency: segment.neighbour,
toTokenAddress,
poolId: segment.pool.poolId,
pool: segment.pool,
};
}
)
);

if (detailRoute.some((x) => x == null)) return undefined;
const _detailRoute = detailRoute as NonNullable<
(typeof detailRoute)[number]
>[];

if (hasLength(_detailRoute, 0)) return undefined;

const firstSegment = _detailRoute[0];

return {
fromCurrency: firstSegment.fromCurrency,
fromTokenAddress: firstSegment.fromTokenAddress,
swapPools: announceAtLeastOne(
_detailRoute.map((segment) => ({
toCurrency: segment.toCurrency,
toTokenAddress: segment.toTokenAddress,
poolId: segment.poolId,
pool: segment.pool,
}))
),
};
}

/**
* Get the router path for swapping between two currencies.
*
Expand Down Expand Up @@ -177,7 +286,7 @@ export class AlexSDK {

/**
* Check if the sponsor service is available.
*
*
* @returns {Promise<boolean>} - A promise that resolves to true if the sponsor service is available, false otherwise.
*/
async isSponsoredTxServiceAvailable(): Promise<boolean> {
Expand Down Expand Up @@ -207,8 +316,8 @@ export class AlexSDK {
from === Currency.STX
? stxAmount
: await this.getAmountTo(Currency.STX, BigInt(1e8), from).then(
(x) => (x * stxAmount) / BigInt(1e8)
);
(x) => (x * stxAmount) / BigInt(1e8)
);
if (sponsorFeeAmount > fromAmount) {
return BigInt(0);
}
Expand Down Expand Up @@ -283,8 +392,8 @@ export class AlexSDK {
currencyX === Currency.STX
? stxAmount
: await this.getAmountTo(Currency.STX, BigInt(1e8), currencyX).then(
(x) => (x * stxAmount) / BigInt(1e8)
);
(x) => (x * stxAmount) / BigInt(1e8)
);
if (sponsorFeeAmount > fromAmount) {
throw new SponsoredTxError(
SponsoredTxErrorCode.insufficient_funds,
Expand Down
36 changes: 35 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Currency } from './currency';
import type { AtLeastOne } from "./utils/arrayHelper";

/**
* TokenInfo represents the details of a token that can be used in the AlexSDK.
Expand Down Expand Up @@ -51,7 +52,7 @@ export type PoolData = {
tokenX: Currency;
tokenY: Currency;
factor: bigint;
poolId: bigint
poolId: bigint;
};

export type PriceData = {
Expand All @@ -65,3 +66,36 @@ export type AlexSDKResponse = {
};

export type BackendAPIPriceResponse = PriceData[];

export interface StacksContractAddress {
deployerAddress: string;
contractName: string;
}

export interface StacksAssetContractAddress extends StacksContractAddress {
tokenId: string;
}
export function deserializeAssetIdentifier(
a: string
): undefined | StacksAssetContractAddress {
const step1Res = a.split('.');
if (step1Res.length !== 2) return undefined;

const [deployerAddress, contractNameAndRest] = step1Res;
const step2Res = contractNameAndRest.split('::');
if (!(step2Res.length > 1)) return undefined;

const [contractName, tokenId] = step2Res;
return { deployerAddress, contractName, tokenId };
}

export type DetailedAMMRoutes = {
fromCurrency: Currency;
fromTokenAddress: StacksAssetContractAddress;
swapPools: AtLeastOne<{
toCurrency: Currency;
toTokenAddress: StacksAssetContractAddress;
poolId: bigint;
pool: PoolData;
}>;
};
13 changes: 13 additions & 0 deletions src/utils/arrayHelper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type AtLeastOne<T> = readonly [T, ...T[]];

export function hasLength<T>(x: T[], length: 0): x is [];
export function hasLength<T>(x: T[], length: 1): x is [T];
export function hasLength<T>(x: T[], length: 2): x is [T, T];
Expand All @@ -7,3 +9,14 @@ export function hasLength<T>(x: T[], length: 5): x is [T, T, T, T, T];
export function hasLength<T>(x: T[], length: number): boolean {
return x.length === length;
}

export function zipObj<K extends string, V>(
keys: K[],
values: V[]
): Record<K, V> {
return Object.fromEntries(keys.map((key, i) => [key, values[i]])) as any;
}

export function announceAtLeastOne<T>(ary: readonly T[]): AtLeastOne<T> {
return ary as any;
}
8 changes: 8 additions & 0 deletions src/utils/promiseHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { zipObj } from './arrayHelper';

export async function props<I extends Record<string, any>>(
inputs: I
): Promise<{ [K in keyof I]: Awaited<I[K]> }> {
const res = await Promise.all(Object.values(inputs));
return zipObj(Object.keys(inputs), res) as any;
}
Loading