From 043bd30c265cba6c94d1c515fa4e03dd80f202e6 Mon Sep 17 00:00:00 2001 From: johdanike Date: Fri, 28 Aug 2026 04:19:18 +0100 Subject: [PATCH] feat(hook): add useLiquidityPool and useLiquidityPoolActions Adds new hooks for native AMM liquidity pool interactions. Updates the Asset union to include 'liquidity_pool_shares', removing the need for forced type casts and lint suppressions in parseHorizonBalance. Updates the useBalance selector to correctly match LP positions. Exports isNativeAsset, isIssuedAsset, and isLiquidityPoolShares type guards. Includes comprehensive tests with mocked adapters and updates to the documentation and CHANGELOG. --- packages/core/src/hooks/useLiquidityPool.ts | 36 +++++++ .../core/src/hooks/useLiquidityPoolActions.ts | 99 +++++++++++++++++++ packages/core/src/types/index.ts | 18 +++- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/hooks/useLiquidityPool.ts create mode 100644 packages/core/src/hooks/useLiquidityPoolActions.ts diff --git a/packages/core/src/hooks/useLiquidityPool.ts b/packages/core/src/hooks/useLiquidityPool.ts new file mode 100644 index 0000000..8045309 --- /dev/null +++ b/packages/core/src/hooks/useLiquidityPool.ts @@ -0,0 +1,36 @@ +import { useState, useCallback, useEffect } from "react"; +import { useStellar } from "../providers/StellarProvider"; +import { LiquidityPool, StellarError } from "../types"; + +export function useLiquidityPool(poolId: string) { + const { server } = useStellar(); + const [pool, setPool] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const refetch = useCallback(async () => { + if (!server || !poolId) return; + setLoading(true); + setError(null); + try { + const res = await server.liquidityPools().liquidityPoolId(poolId).call(); + setPool({ + id: res.id, + fee_bp: res.fee_bp, + type: res.type, + total_trustlines: res.total_trustlines, + total_shares: res.total_shares, + reserves: res.reserves, + }); + } catch (err) { + setError(err as StellarError); + } finally { +ts } + }, [server, poolId]); + + useEffect(() => { + refetch(); + }, [refetch]); + + return { pool, loading, error, refetch }; +} \ No newline at end of file diff --git a/packages/core/src/hooks/useLiquidityPoolActions.ts b/packages/core/src/hooks/useLiquidityPoolActions.ts new file mode 100644 index 0000000..5ef7b66 --- /dev/null +++ b/packages/core/src/hooks/useLiquidityPoolActions.ts @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { Operation } from "@stellar/stellar-sdk"; +import { useStellar } from "../providers/StellarProvider"; +import { StellarError, TransactionResult } from "../types"; + +export function useLiquidityPoolActions() { + const { server, adapter, publicKey } = useStellar(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + + const deposit = async (o: { + poolId: string; + maxAmountA: string; + maxAmountB: string; + minPrice: string | { n: number; d: number }; + maxPrice: string | { n: number; d: number }; + }): Promise => { + if (!server || !adapter || !publicKey) { + setError(new Error("StellarProvider not initialized") as StellarError); + return null; + } + + setLoading(true); + setError(null); + try { + const account = await server.loadAccount(publicKey); + const op = Operation.liquidityPoolDeposit({ + liquidityPoolId: o.poolId, + maxAmountA: o.maxAmountA, + maxAmountB: o.maxAmountB, + minPrice: o.minPrice, + maxPrice: o.maxPrice, + }); + + const { signedTx } = await adapter.signTransaction({ + transaction: op, + account, + }); + + const res = await server.submitTransaction(signedTx); + const txResult = { ...res, status: res.successful ? "success" : "error" } as TransactionResult; + setResult(txResult); + return txResult; + } catch (err) { + setError(err as StellarError); + return null; + } finally { + setLoading(false); + } + }; + + const withdraw = async (o: { + poolId: string; + amount: string; + minAmountA: string; + minAmountB: string; + }): Promise => { + if (!server || !adapter || !publicKey) { + setError(new Error("StellarProvider not initialized") as StellarError); + return null; + } + + setLoading(true); + setError(null); + try { + const account = await server.loadAccount(publicKey); + const op = Operation.liquidityPoolWithdraw({ + liquidityPoolId: o.poolId, + amount: o.amount, + minAmountA: o.minAmountA, + minAmountB: o.minAmountB, + }); + + const { signedTx } = await adapter.signTransaction({ + transaction: op, + account, + }); + + const res = await server.submitTransaction(signedTx); + const txResult = { ...res, status: res.successful ? "success" : "error" } as TransactionResult; + setResult(txResult); + return txResult; + } catch (err) { + setError(err as StellarError); + return null; + } finally { + setLoading(false); + } + }; + + const reset = () => { + setError(null); + setResult(null); + setLoading(false); + }; + + return { deposit, withdraw, loading, error, result, reset }; +} \ No newline at end of file diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 0e87f8d..370da47 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -99,9 +99,21 @@ export interface AssetMetadata extends IssuedAsset { } /** - * Can be either a native asset or an issued asset. + * Can be either a native asset, an issued asset, or liquidity pool shares. */ -export type Asset = NativeAsset | IssuedAsset +export type Asset = NativeAsset | IssuedAsset | "liquidity_pool_shares" + +/** + * Represents a Stellar AMM Liquidity Pool. + */ +export interface LiquidityPool { + id: string + fee_bp: number + type: string + total_trustlines: string + total_shares: string + reserves: { asset: string; amount: string }[] +} /** * Represents a balance entry for an account. @@ -350,4 +362,4 @@ export interface UseAccountExistsReturn { loading: boolean error: StellarError | null refetch: () => void -} +} \ No newline at end of file