Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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