Skip to content

Commit 6f96303

Browse files
committed
ux: include token balance in my portfolio tool
1 parent f488056 commit 6f96303

File tree

6 files changed

+79
-144
lines changed

6 files changed

+79
-144
lines changed

projects/pendle/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ More information on Pendle Finance can be found on their own academy page: https
1515
- Show my pools on Pendle
1616
- Value of my stETH position on Base chain on Pendle
1717

18-
Please note that most portfolio tools return positions across all chains, without the need to specify the chain.
18+
Please note that:
19+
20+
- portfolio tools return positions across all chains, without the need to specify the chain
21+
- for each position both the token balance and dollar valuations will be shown
1922

2023
### Available markets & pools
2124

projects/pendle/examples/flattenPositions.ts

Lines changed: 0 additions & 105 deletions
This file was deleted.

projects/pendle/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const MIN_LIQUIDITY_FOR_MARKET = 100000;
4141
/**
4242
* Default slippage tolerance
4343
*/
44-
export const DEFAULT_SLIPPAGE_TOLERANCE = 0.02;
44+
export const DEFAULT_SLIPPAGE_TOLERANCE = 0.01;
4545

4646
/**
4747
* The address used by Pendle API to identify the native token

projects/pendle/src/functions/getMyPositionsPortfolio.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { flattenAndSortPositions, formatFlattenedPositions } from '../helpers/po
44
import { MAX_POSITIONS_IN_RESULTS } from '../constants';
55
import { getChainNameFromChainId } from '../helpers/chains';
66
import { fetchTokenInfoFromAddress } from '../helpers/tokens';
7-
import { toHumanReadableAmount } from '../helpers/format';
7+
import { to$$$ } from '../helpers/format';
8+
import { formatUnits } from 'viem';
89

910
interface Props {}
1011

@@ -26,7 +27,7 @@ export async function getMyPositionsPortfolio(_props: Props, { notify, evm: { ge
2627

2728
// Build and return output string
2829
const parts = [
29-
`Found ${flattenedResult.totalPositions} positions in your portfolio, worth a total of $${flattenedResult.totalValuation.toFixed(2)}`,
30+
`Found ${flattenedResult.totalPositions} positions in your portfolio, worth a total of ${to$$$(flattenedResult.totalValuation)}`,
3031
firstNPositions.length !== flattenedResult.totalPositions ? `Showing the top ${MAX_POSITIONS_IN_RESULTS} positions by value:` : '',
3132
formattedOutput,
3233
];
@@ -42,11 +43,13 @@ export async function getMyPositionsPortfolio(_props: Props, { notify, evm: { ge
4243
const chainName = getChainNameFromChainId(chain.chainId);
4344
const subparts = [];
4445
for (const syPosition of chain.syPositions) {
46+
if (syPosition.balance === '0') {
47+
continue;
48+
}
4549
const tokenAddress = syPosition.syId.split('-')[1] as `0x${string}`;
4650
const provider = getProvider(chain.chainId);
4751
const tokenInfo = await fetchTokenInfoFromAddress(provider, tokenAddress);
48-
const humanReadableBalance = toHumanReadableAmount(BigInt(syPosition.balance), tokenInfo.decimals);
49-
subparts.push(`${humanReadableBalance} ${tokenInfo.symbol}`);
52+
subparts.push(`${formatUnits(BigInt(syPosition.balance), tokenInfo.decimals)} ${tokenInfo.symbol}`);
5053
}
5154
parts.push(` - ${chainName} chain: ${subparts.join(', ')}`);
5255
}

projects/pendle/src/helpers/markets.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ export function formatMarketData(marketData: GetMarketDataResponse, market: Mark
4343
parts.push(` - The underlying asset earns an APY of ${(marketData.underlyingApy * 100).toFixed(2)}%`);
4444
parts.push(` - Providing liquidity earns you from ${(market.details.aggregatedApy * 100).toFixed(2)}% to ${(market.details.maxBoostedApy * 100).toFixed(2)}% APY (max boost)`);
4545
if (includeTokensAddresses) {
46-
parts.push(` - PT address: ${market.pt}`);
47-
parts.push(` - YT address: ${market.yt}`);
48-
parts.push(` - SY address: ${market.sy}`);
46+
parts.push(` - PT address: ${market.pt.split('-')[1]}`);
47+
parts.push(` - YT address: ${market.yt.split('-')[1]}`);
48+
parts.push(` - SY address: ${market.sy.split('-')[1]}`);
4949
parts.push(` - LP address: ${market.address}`);
5050
parts.push(` - Underlying asset address: ${market.underlyingAsset}`);
5151
}

projects/pendle/src/helpers/positions.ts

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { getChainNameFromChainId } from './chains';
2-
import { MarketCompactData, ChainPositions, PendleClient, MarketPosition } from './client';
2+
import { MarketCompactData, ChainPositions, PendleClient, MarketPosition, PendleAsset } from './client';
3+
import { to$$$ } from './format';
4+
import { formatUnits } from 'viem';
35

46
/**
57
* Represents a single token position (PT, YT, or LP) with its context
@@ -11,6 +13,8 @@ export type FlattenedTokenPosition = {
1113
marketId: string;
1214
/** Token type: PT, YT, or LP */
1315
tokenType: 'PT' | 'YT' | 'LP';
16+
/** Token address */
17+
tokenAddress?: `0x${string}`;
1418
/** Position type: open or closed */
1519
positionStatus: 'open' | 'closed';
1620
/** Valuation in USD */
@@ -31,8 +35,10 @@ export type FlattenedTokenPosition = {
3135
marketLpNonBoostedApy?: number;
3236
/** Market implied APY, that is, the APY at which the market trades (different from underlying APY) */
3337
marketImpliedApy?: number;
34-
/** Full market details (optional) */
38+
/** Full market details */
3539
market?: MarketCompactData;
40+
/** Full asset details */
41+
asset?: PendleAsset;
3642
};
3743

3844
/**
@@ -67,7 +73,7 @@ export async function flattenAndSortPositions(
6773

6874
// Helper function to process a single PT, YT, or
6975
// LP market position
70-
const processMarketPosition = (
76+
const processMarketPosition = async (
7177
market: MarketPosition,
7278
chain: ChainPositions,
7379
chainName: string,
@@ -81,31 +87,46 @@ export async function flattenAndSortPositions(
8187
throw new Error(`Market data not found for market ${marketAddress}`);
8288
}
8389

90+
// Get all assets
91+
const assets = await pendleClient.getAllAssets(chain.chainId);
92+
8493
// Process each token type (PT, YT, LP)
8594
const tokenTypes: Array<'pt' | 'yt' | 'lp'> = ['pt', 'yt', 'lp'];
8695

8796
for (const tokenKey of tokenTypes) {
88-
const token = market[tokenKey];
89-
if (token && (includeZeroPositions || token.balance !== '0')) {
97+
const pos = market[tokenKey];
98+
if (pos && (includeZeroPositions || pos.balance !== '0')) {
99+
// Extract token address of position
100+
let tokenAddress: `0x${string}`;
101+
if (tokenKey === 'lp') {
102+
tokenAddress = marketData.address;
103+
} else {
104+
tokenAddress = marketData[tokenKey].split('-')[1] as `0x${string}`;
105+
}
106+
// Get asset details
107+
const asset = assets.find((a) => a.address === tokenAddress);
108+
// Build flattened position row
90109
flattenedPositions.push({
91110
chainId: chain.chainId,
92111
chainName,
93112
marketId: market.marketId,
94113
tokenType: tokenKey.toUpperCase() as 'PT' | 'YT' | 'LP',
114+
tokenAddress,
95115
positionStatus,
96-
valuation: token.valuation || 0,
97-
balance: token.balance,
98-
activeBalance: token.activeBalance,
99-
claimTokenAmounts: token.claimTokenAmounts,
116+
valuation: pos.valuation || 0,
117+
balance: pos.balance,
118+
activeBalance: pos.activeBalance,
119+
claimTokenAmounts: pos.claimTokenAmounts,
100120
marketName: marketData.name,
101121
marketExpiry: marketData.expiry,
102122
marketLpNonBoostedApy: marketData.details.aggregatedApy,
103123
marketImpliedApy: marketData.details.impliedApy,
104124
market: marketData,
125+
asset,
105126
});
106127

107-
if (token.valuation > 0) {
108-
totalValuation += token.valuation;
128+
if (pos.valuation) {
129+
totalValuation += pos.valuation;
109130
}
110131
}
111132
}
@@ -123,14 +144,14 @@ export async function flattenAndSortPositions(
123144
// Process open positions
124145
if (chain.openPositions) {
125146
for (const market of chain.openPositions) {
126-
processMarketPosition(market, chain, chainName, 'open', marketDetailsMap);
147+
await processMarketPosition(market, chain, chainName, 'open', marketDetailsMap);
127148
}
128149
}
129150

130151
// Process closed positions if requested
131152
if (includeClosedPositions && chain.closedPositions) {
132153
for (const market of chain.closedPositions) {
133-
processMarketPosition(market, chain, chainName, 'closed', marketDetailsMap);
154+
await processMarketPosition(market, chain, chainName, 'closed', marketDetailsMap);
134155
}
135156
}
136157
}
@@ -169,39 +190,52 @@ export function formatFlattenedPositions(flattenedPositions: FlattenedTokenPosit
169190
switch (position.tokenType) {
170191
case 'LP':
171192
if (position.marketLpNonBoostedApy) {
172-
apyString = `(unboosted APY: ${(100 * position.marketLpNonBoostedApy).toFixed(2)}%)`;
193+
apyString = `unboosted APY: ${(100 * position.marketLpNonBoostedApy).toFixed(2)}%`;
173194
}
174195
break;
175196
case 'YT':
176197
if (position.marketImpliedApy) {
177-
apyString = `(implied APY: ${(100 * position.marketImpliedApy).toFixed(2)}%)`;
198+
apyString = `implied APY: ${(100 * position.marketImpliedApy).toFixed(2)}%`;
178199
}
179200
break;
180201
case 'PT':
181202
break;
182203
}
183204

184-
let parts: string[] = [
185-
`$${position.valuation.toFixed(2)}`,
186-
`${position.tokenType}`,
187-
`${position.positionStatus === 'closed' ? '(closed)' : ''}`,
188-
`position`,
189-
`on ${position.marketName} market`,
190-
`on ${position.chainName} chain`,
191-
`${apyString}`,
192-
].filter(Boolean); // Remove empty strings
193-
194-
// Add expiry if available
205+
let expiryString = '';
195206
if (position.marketExpiry) {
196-
parts.push(`expires ${position.marketExpiry}`);
207+
expiryString = `expires on ${position.marketExpiry}`;
197208
}
198209

199-
// Add claimable yield indicator
210+
let claimableYieldString = '';
200211
if (position.claimTokenAmounts && position.claimTokenAmounts.length > 0) {
201-
parts.push(`(has claimable yield)`);
212+
claimableYieldString = `has claimable yield`;
213+
}
214+
215+
let balanceString = '';
216+
if (position.asset?.decimals) {
217+
balanceString = `${formatUnits(BigInt(position.balance), position.asset.decimals)} ${position.tokenType} tokens`;
218+
}
219+
220+
let parts: string[] = [
221+
`${to$$$(position.valuation)}`,
222+
`${position.tokenType}`,
223+
`${position.positionStatus === 'closed' ? 'closed' : ''}`,
224+
`position`,
225+
`on ${position.marketName} market`,
226+
`on ${position.chainName} chain:`,
227+
`${balanceString},`,
228+
`${apyString ? `${apyString},` : ''}`,
229+
`${expiryString ? `${expiryString},` : ''}`,
230+
`${claimableYieldString ? `${claimableYieldString},` : ''}`,
231+
];
232+
233+
// Remove last comma if it exists
234+
if (parts[parts.length - 1] === ',') {
235+
parts.pop();
202236
}
203237

204-
formattedParts.push(parts.join(' '));
238+
formattedParts.push(parts.filter(Boolean).join(' '));
205239
}
206240
return formattedParts.join('\n').replace(/^/gm, linePrefix);
207241
}

0 commit comments

Comments
 (0)