Managing Positions
Once you've added liquidity, the SDK gives you three operations: read your position and claimable fees, claim fees without removing principal, and remove liquidity (partially or fully).
Reading a Position
import { readPosition } from '@taurusswap/sdk';
const pool = await readPoolState(algod, POOL_APP_ID);
const tick = pool.ticks.find((t) => t.id === tickId);
// Returns null if the address has no position in this tick
const position = await readPosition(
algod,
POOL_APP_ID,
'OWNER_ADDRESS',
tickId,
pool.n,
pool.feeGrowth,
tick!,
);
if (position) {
console.log('Shares:', position.shares);
console.log('Position r:', position.positionR); // AMOUNT_SCALE units
console.log('Claimable fees:', position.claimableFees.map((f, i) =>
`Token ${i}: ${Number(f) / 1e6} USD`
));
}Via TaurusClient (simpler — handles pool state internally):
const position = await client.getPosition('OWNER_ADDRESS', tickId);
// → PositionInfo | nullPositionInfo Type
interface PositionInfo {
tickId: number; // Which tick
shares: bigint; // Your share count in this tick
positionR: bigint; // tick.r × shares / tick.totalShares (AMOUNT_SCALE units)
// = your proportional contribution to total liquidity
claimableFees: bigint[]; // [n] per-token fees in raw microunits — ready for display
}How Fee Accounting Works
Each tick position stores a feeGrowthCheckpoint[i] per token. When a swap collects fees, the global pool.feeGrowth[i] accumulator grows. Your claimable fee for token i is:
claimable[i] = positionR × (feeGrowth[i] − checkpoint[i]) / PRECISIONAfter claiming, the contract resets your checkpoint to the current feeGrowth[i], so you can't double-claim. When you remove liquidity, fees are claimed atomically as part of the same transaction — you never need a separate claim before removing.
Estimating What You'll Receive
Before building a removal transaction, preview the amounts:
const { receivePerTokenRaw, claimableFeesRaw } = await client.estimateRemoval(
'OWNER_ADDRESS',
tickId,
shares, // how many shares to remove (pass tick.totalShares for full exit)
);
receivePerTokenRaw.forEach((amt, i) => {
console.log(`Token ${i} principal: ${Number(amt) / 1e6} USD`);
});
claimableFeesRaw.forEach((fee, i) => {
console.log(`Token ${i} fees: ${Number(fee) / 1e6} USD`);
});Claiming Fees
Claim all accrued fees without touching your principal:
// Via TaurusClient
const txns = await client.buildClaimFeesTxns({ sender: 'YOUR_ADDRESS', tickId });
// Via low-level function
import { claimFees } from '@taurusswap/sdk';
const { txId } = await claimFees({
client: algod,
poolAppId: POOL_APP_ID,
sender: account.addr,
tickId,
signer: async (txns) => txns.map((t) => t.signTxn(account.sk)),
});
// Sign and submit (wallet path)
const signedTxns = await wallet.signTransaction(txns);
const { txid } = await client.algod.sendRawTransaction(signedTxns).do();
await algosdk.waitForConfirmation(client.algod, txid, 4);
console.log('Fees claimed:', txid);Claim Transaction Group Layout
┌──────────────────────────────────────────┐
│ Tx 0: App call claim_fees(tickId) │
│ inner txs: pool → user (n fee tokens) │
└──────────────────────────────────────────┘Removing Liquidity
Full Exit
// Via TaurusClient
const pool = await client.getPoolState();
const tick = pool.ticks.find((t) => t.id === tickId)!;
const txns = await client.buildRemoveLiquidityTxns({
sender: 'YOUR_ADDRESS',
tickId,
shares: tick.totalShares, // full exit — remove all of your shares
});
// Via low-level function
import { removeLiquidity } from '@taurusswap/sdk';
const { txId } = await removeLiquidity({
client: algod,
poolAppId: POOL_APP_ID,
sender: account.addr,
tickId,
shares: tick.totalShares,
signer: async (txns) => txns.map((t) => t.signTxn(account.sk)),
});Partial Exit
const position = await client.getPosition('YOUR_ADDRESS', tickId);
if (!position) throw new Error('No position');
// Remove 50% of your shares
const sharesToRemove = position.shares / 2n;
const txns = await client.buildRemoveLiquidityTxns({
sender: 'YOUR_ADDRESS',
tickId,
shares: sharesToRemove,
});
// Your remaining shares stay in the pos: box — fees continue accruingRemove Transaction Group Layout
┌──────────────────────────────────────────┐
│ Tx 0: App call remove_liquidity( │
│ tickId, shares) │
│ inner txs: pool → user (n tokens) │
│ = pro-rata reserves + accrued fees │
└──────────────────────────────────────────┘What You Receive on Removal
For each token i:
principal[i] = positionR × actualReserves[i] / pool.totalR (raw microunits)
fees[i] = positionR × (feeGrowth[i] − checkpoint[i]) / PRECISION
total[i] = principal[i] + fees[i]The contract sends all n token amounts atomically as inner transactions. You do not need to claim fees separately before removing.
React Hook: Position Dashboard
import { useState, useEffect } from 'react';
import { type PositionInfo, TickNotFoundError } from '@taurusswap/sdk';
import { taurusClient } from '@/lib/taurus';
export function usePosition(address: string | null, tickId: number | null) {
const [position, setPosition] = useState<PositionInfo | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!address || tickId == null) return;
let active = true;
setLoading(true);
taurusClient.getPosition(address, tickId)
.then((p) => { if (active) setPosition(p); })
.catch((err) => {
if (err instanceof TickNotFoundError) setPosition(null);
else console.error(err);
})
.finally(() => { if (active) setLoading(false); });
return () => { active = false; };
}, [address, tickId]);
return { position, loading };
}
// Usage
function PositionCard({ tickId }: { tickId: number }) {
const { activeAddress } = useWallet();
const { position, loading } = usePosition(activeAddress, tickId);
if (loading) return <span>Loading...</span>;
if (!position) return <span>No position</span>;
return (
<div>
<div>Shares: {position.shares.toString()}</div>
<div>Claimable fees:</div>
{position.claimableFees.map((fee, i) => (
<div key={i}>Token {i}: {(Number(fee) / 1e6).toFixed(4)} USD</div>
))}
</div>
);
}