Skip to content
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
2 changes: 1 addition & 1 deletion src/apps/key-wallet/utils/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export const formatBalance = (
export const formatUsdValue = (value: number): string => {
if (value === 0) return '$0.00';
if (value < 0.01) return '<$0.01';
return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};

export const shortenAddress = (address: string, chars: number = 4): string => {
Expand Down
16 changes: 13 additions & 3 deletions src/apps/pulse/components/App/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export default function HomeScreen(props: HomeScreenProps) {
const [maxStableCoinBalance, setMaxStableCoinBalance] = useState<{
chainId: number;
balance: number;
price?: number;
tokenAmount: number;
}>();
const [transactionData, setTransactionData] = useState<{
sellToken: SelectedToken | null;
Expand Down Expand Up @@ -168,6 +168,8 @@ export default function HomeScreen(props: HomeScreenProps) {
const [tokenAmount, setTokenAmount] = useState<string>('');
const [isRefreshingHome, setIsRefreshingHome] = useState(false);
const [usdAmount, setUsdAmount] = useState<string>('');
const [isMaxSelected, setIsMaxSelected] = useState<boolean>(false);
const [maxTokenAmount, setMaxTokenAmount] = useState<number | undefined>();
const [dispensableAssets, setDispensableAssets] = useState<
DispensableAsset[]
>([]);
Expand Down Expand Up @@ -402,7 +404,6 @@ export default function HomeScreen(props: HomeScreenProps) {
(key) => stableBalance[Number(key)].balance === maxStableBalance
) || '1'
);

// Set USDC price from the chain with max stable balance
const usdcPriceForMaxChain =
stableBalance[chainIdOfMaxStableBalance]?.price;
Expand All @@ -413,6 +414,7 @@ export default function HomeScreen(props: HomeScreenProps) {
setMaxStableCoinBalance({
chainId: chainIdOfMaxStableBalance,
balance: maxStableBalance,
tokenAmount: stableBalance[chainIdOfMaxStableBalance]?.tokenAmount ?? 0,
});
}, [portfolioTokens, walletPortfolioData]);

Expand Down Expand Up @@ -1205,6 +1207,8 @@ export default function HomeScreen(props: HomeScreenProps) {
userPortfolio={portfolioTokens}
gasTankBalance={gasTankBalance}
usdcPrice={usdcPrice}
isMaxSelected={isMaxSelected}
maxTokenAmount={maxTokenAmount}
/>
</div>
);
Expand Down Expand Up @@ -1361,7 +1365,11 @@ export default function HomeScreen(props: HomeScreenProps) {
payingTokens={payingTokens}
portfolioTokens={portfolioTokens}
maxStableCoinBalance={
maxStableCoinBalance ?? { chainId: 1, balance: 2 }
maxStableCoinBalance ?? {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea to have default value for chainId and balance here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its a react render so as soon as the maxStableCoinBalance changes it will get reflected on the pulse buy/sell page its just a matter of milliseconds

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm ok

chainId: 1,
balance: 2,
tokenAmount: 0,
}
}
customBuyAmounts={[...customBuyAmounts, 'MAX']}
setPreviewBuy={setPreviewBuy}
Expand All @@ -1374,6 +1382,8 @@ export default function HomeScreen(props: HomeScreenProps) {
setChains={setChains}
usdcPrice={usdcPrice}
isRefreshing={isRefreshingHome}
setIsMaxSelected={setIsMaxSelected}
setMaxTokenAmount={setMaxTokenAmount}
/>
) : (
<Sell
Expand Down
123 changes: 108 additions & 15 deletions src/apps/pulse/components/Buy/Buy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ interface BuyProps {
maxStableCoinBalance: {
chainId: number;
balance: number;
tokenAmount: number;
};
customBuyAmounts: string[];
setPreviewBuy: Dispatch<SetStateAction<boolean>>;
Expand All @@ -84,6 +85,10 @@ interface BuyProps {
setChains: Dispatch<SetStateAction<MobulaChainNames>>;
usdcPrice?: number; // For Relay Buy: USDC price from portfolio (passed from HomeScreen)
isRefreshing?: boolean;
isMaxSelected?: boolean; // Whether MAX was selected
maxTokenAmount?: number; // Balance amount when MAX is selected
setIsMaxSelected?: Dispatch<SetStateAction<boolean>>; // Update parent MAX selected state
setMaxTokenAmount?: Dispatch<SetStateAction<number | undefined>>; // Update parent max token amount state
}

export default function Buy(props: BuyProps) {
Expand All @@ -105,6 +110,10 @@ export default function Buy(props: BuyProps) {
customBuyAmounts,
usdcPrice,
isRefreshing = false,
isMaxSelected = false,
maxTokenAmount,
setIsMaxSelected: setParentIsMaxSelected,
setMaxTokenAmount: setParentMaxTokenAmount,
} = props;
const [usdAmount, setUsdAmount] = useState<string>('');
const [debouncedUsdAmount, setDebouncedUsdAmount] = useState<string>('');
Expand Down Expand Up @@ -268,6 +277,8 @@ export default function Buy(props: BuyProps) {
if (!input || !Number.isNaN(parseFloat(input))) {
setInputPlaceholder('0.00');
setUsdAmount(input);
setParentIsMaxSelected?.(false); // Reset MAX flag when user manually types
setParentMaxTokenAmount?.(undefined);
setBelowMinimumAmount(false);
setNoEnoughLiquidity(false);
setInsufficientWalletBalance(false);
Expand All @@ -293,7 +304,56 @@ export default function Buy(props: BuyProps) {

useEffect(() => {
const timer = setTimeout(() => {
if (usdAmount && !Number.isNaN(parseFloat(usdAmount))) {
// Handle MAX case - use maxTokenAmount directly
if (isMaxSelected && maxTokenAmount && maxTokenAmount > 0) {
const amount = maxTokenAmount;

if (amount < 2) {
setBelowMinimumAmount(true);
setNoEnoughLiquidity(false);
setInsufficientWalletBalance(false);
return;
}

setBelowMinimumAmount(false);
setNoEnoughLiquidity(false);
setInsufficientWalletBalance(false);
// Pass the balance as a string for dispens able assets calculation
setDebouncedUsdAmount(maxTokenAmount.toString());
const [dAssets, pChains, pTokens] = getDispensableAssets(
maxTokenAmount.toString(),
walletPortfolioData?.result.data,
maxStableCoinBalance.chainId
);

// For MAX selection, skip validation errors and let getBestOffer handle the full amount
if (
pChains.length === 0 ||
dAssets.length === 0 ||
pTokens.length === 0
) {
if (!isMaxSelected) {
// Only show error for non-MAX selections
setNoEnoughLiquidity(true);
return;
}
// For MAX: proceed without dispensable assets validation
// getBestOffer will handle the full balance
setParentUsdAmount(maxTokenAmount.toString());
return;
}

// Always update payingTokens to ensure correct USD amounts are passed to PreviewBuy
setDispensableAssets(dAssets);
setPermittedChains(pChains);
setPayingTokens(pTokens);
setParentDispensableAssets(dAssets);
setParentUsdAmount(maxTokenAmount.toString());
} else if (
usdAmount &&
usdAmount !== 'MAX' &&
!Number.isNaN(parseFloat(usdAmount))
) {
const amount = parseFloat(usdAmount);

if (amount < 2) {
Expand Down Expand Up @@ -335,6 +395,8 @@ export default function Buy(props: BuyProps) {
}, [
sumOfStableBalance,
usdAmount,
isMaxSelected,
maxTokenAmount,
setPayingTokens,
walletPortfolioData?.result.data,
dispensableAssets.length,
Expand All @@ -350,14 +412,19 @@ export default function Buy(props: BuyProps) {
) {
setIsLoading(true);
try {
// For Relay Buy with EXACT_INPUT, we pass the USD amount directly
// The quote will tell us how many tokens we'll receive
// For Relay Buy with EXACT_INPUT, we pass the USDC amount directly
// When MAX is selected, use maxTokenAmount to pass the balance directly
// Otherwise use the debouncedUsdAmount as USD amount that will be converted to USDC
const offer = await getBestOffer({
fromAmount: debouncedUsdAmount,
toTokenAddress: token.address,
toChainId: token.chainId,
fromChainId: maxStableCoinBalance.chainId,
usdcPrice,
maxTokenAmount:
isMaxSelected && maxTokenAmount
? maxTokenAmount.toString()
: undefined,
});

setBuyOffer(offer);
Expand All @@ -377,7 +444,10 @@ export default function Buy(props: BuyProps) {
{
operation: 'fetch_relay_buy_offer',
buyToken: token.symbol,
amount: debouncedUsdAmount,
amount:
isMaxSelected && maxTokenAmount
? maxTokenAmount.toString()
: debouncedUsdAmount,
toChainId: token.chainId,
fromChainId: maxStableCoinBalance.chainId,
},
Expand All @@ -394,6 +464,8 @@ export default function Buy(props: BuyProps) {
debouncedUsdAmount,
token,
isRelayInitialized,
isMaxSelected,
maxTokenAmount,
getBestOffer,
maxStableCoinBalance.chainId,
usdcPrice,
Expand Down Expand Up @@ -744,16 +816,22 @@ export default function Buy(props: BuyProps) {
</button>
<div className="flex max-w-60 desktop:w-60 tablet:w-60 mobile:w-56 xs:w-44 items-right overflow-hidden">
<div className="flex items-center max-w-60 desktop:w-60 tablet:w-60 mobile:w-56 xs:w-44 text-right justify-end bg-transparent outline-none pr-0 h-9">
<input
className="no-spinner flex mobile:text-4xl xs:text-4xl desktop:text-4xl tablet:text-4xl desktop:w-40 tablet:w-40 mobile:w-36 xs:w-24 font-medium text-right"
placeholder={inputPlaceholder}
onChange={handleUsdAmountChange}
value={usdAmount}
type="text"
disabled={isLoading}
onFocus={() => setInputPlaceholder('')}
data-testid="pulse-buy-amount-input"
/>
{isMaxSelected ? (
<div className="mobile:text-4xl xs:text-4xl desktop:text-4xl tablet:text-4xl desktop:w-40 tablet:w-40 mobile:w-36 xs:w-24 font-medium text-right text-white">
{maxStableCoinBalance.balance.toFixed(2)}
</div>
) : (
<input
className="no-spinner flex mobile:text-4xl xs:text-4xl desktop:text-4xl tablet:text-4xl desktop:w-40 tablet:w-40 mobile:w-36 xs:w-24 font-medium text-right"
placeholder={inputPlaceholder}
onChange={handleUsdAmountChange}
value={usdAmount}
type="text"
disabled={isLoading}
onFocus={() => setInputPlaceholder('')}
data-testid="pulse-buy-amount-input"
/>
)}
<span className="mobile:text-4xl xs:text-4xl desktop:text-4xl tablet:text-4xl desktop:w-20 tablet:w-20 mobile:w-20 xs:w-20 font-medium overflow-hidden text-[#FFFFFF4D]">
USD
</span>
Expand Down Expand Up @@ -860,9 +938,24 @@ export default function Buy(props: BuyProps) {
onClick={() => {
if (!isDisabled) {
if (isMax) {
setUsdAmount(sumOfStableBalance.toFixed(2));
// Use full balance for MAX display
const fullBalance = maxStableCoinBalance.tokenAmount;
const balanceStr = fullBalance.toString();
setUsdAmount(balanceStr); // Store full balance for display

// For API calls, calculate amount after 1% platform fee
const maxAmount = fullBalance * 0.99;
// Proper rounding: round down to be conservative with fee calculation
const roundedAmount = Math.floor(maxAmount * 100) / 100;

// Update parent state for PreviewBuy
setParentIsMaxSelected?.(true);
setParentMaxTokenAmount?.(roundedAmount);
} else {
setUsdAmount(item);
// Reset parent state
setParentIsMaxSelected?.(false);
setParentMaxTokenAmount?.(undefined);
}
}
}}
Expand Down
11 changes: 11 additions & 0 deletions src/apps/pulse/components/Buy/PreviewBuy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ interface PreviewBuyProps {
userPortfolio?: Token[]; // For Relay Buy: user's token portfolio
gasTankBalance?: number; // For Relay Buy: gas tank balance to validate transaction
usdcPrice?: number; // For Relay Buy: USDC price in USD (e.g., 0.9998)
isMaxSelected?: boolean; // For Relay Buy: whether MAX was selected
maxTokenAmount?: number; // For Relay Buy: actual balance amount when MAX is selected
}

export default function PreviewBuy(props: PreviewBuyProps) {
Expand All @@ -87,6 +89,8 @@ export default function PreviewBuy(props: PreviewBuyProps) {
userPortfolio,
gasTankBalance = 0,
usdcPrice,
isMaxSelected = false,
maxTokenAmount,
} = props;

const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -615,12 +619,17 @@ export default function PreviewBuy(props: PreviewBuyProps) {
try {
// For Relay Buy with EXACT_INPUT, we pass the USD amount directly
// The quote will tell us how many tokens we'll receive
// When MAX is selected, use maxTokenAmount to pass the balance directly
const newOffer = await getBestOffer({
fromAmount: usdAmount,
toTokenAddress: buyToken.address,
toChainId: buyToken.chainId,
fromChainId,
usdcPrice,
maxTokenAmount:
isMaxSelected && maxTokenAmount
? maxTokenAmount.toString()
: undefined,
});

onBuyOfferUpdate(newOffer);
Expand Down Expand Up @@ -703,6 +712,8 @@ export default function PreviewBuy(props: PreviewBuyProps) {
setExpressIntentResponse,
clearError,
isRelayInitialized,
isMaxSelected,
maxTokenAmount,
onBuyOfferUpdate,
getBestOffer,
fromChainId,
Expand Down
Loading