ContractCall in `custom` subvariant requires `toAmount`, causing UX issues and logical conflicts
## Issue Description
When using `subvariant: 'custom'` with dynamically generated `contractCalls`, the Widget enforces the requirement of a `toAmount` field to trigger the `getContractCallsQuote` API call. This conflicts with the regular route design and creates implementation difficulties for Deposit/Checkout scenarios.
## Steps to Reproduce
1. Configure Widget with `subvariant: 'custom'` and `subvariantOptions: { custom: 'deposit' }`
2. Dynamically generate `contractCalls` in `contractComponent`
3. User inputs `fromAmount` in the form (e.g., 100 USDC)
4. Attempt to set `toAmount = fromAmount` (assuming 1:1 rate)
5. Widget calls `getContractCallsQuote` and either errors or returns incorrect routes
## Code Example
```typescript
// DepositCard.tsx
const contractCalls: ContractCall[] = useMemo(() => {
if (!fromAmount || !address || !fromToken || !toChain) {
return []
}
// Generate contractCall
const contractCall: ContractCall = {
fromAmount: parseUnits(fromAmount, token.decimals).toString(),
fromTokenAddress: fromToken,
toContractAddress: LOGGER_CONTRACT,
toContractCallData: callData,
toContractGasLimit: '300000',
toTokenAddress: token.address,
}
return [contractCall]
}, [fromAmount, fromToken, toChain, token, address])
useEffect(() => {
if (token) {
setFieldValue('toChain', token.chainId, { isTouched: true })
setFieldValue('toToken', token.address, { isTouched: true })
// ⚠️ Issue: Setting toAmount = fromAmount causes API calculation errors
setFieldValue('toAmount', fromAmount, { isTouched: true })
}
if (contractCalls?.length > 0) {
setFieldValue('contractCalls', contractCalls, { isTouched: true })
}
}, [contractCalls, token, fromAmount])
Source Code Analysis
1. Regular Routes Support Both Modes
File: packages/widget/src/hooks/useRoutes.ts Line 101
const hasAmount = Number(fromTokenAmount) > 0 || Number(toTokenAmount) > 0
This indicates regular route calculation supports:
- ✅
fromAmountmode (user inputs source amount) - ✅
toAmountmode (user inputs destination amount)
2. ContractCall Routes Require toAmount
File: packages/widget/src/hooks/useRoutes.ts Line 284
if (subvariant === 'custom' && contractCalls && toAmount) {
const contractCallQuote = await getContractCallsQuote({
fromAddress: fromAddress as string,
fromChain: fromChainId,
fromToken: fromTokenAddress,
toAmount: toAmount.toString(), // ⚠️ toAmount is mandatory
toChain: toChainId,
toToken: toTokenAddress,
contractCalls,
// ...
})
}
Problem Analysis
Core Contradiction
-
User Input Pattern: Users naturally input
fromAmount("How much do I want to pay/deposit?") -
Widget Requirement: ContractCall functionality mandates
toAmount("How much should arrive?") -
Calculation Dilemma: Developers cannot accurately calculate
toAmountwithout calling APIs (need to consider bridge fees, slippage, etc.)
The Problem with 1:1 Setting
If we simply set toAmount = fromAmount:
User inputs: 100 USDC (Polygon)
Setting: toAmount = 100 USDC (Optimism)
Widget calls API:
"To receive 100 USDC, user needs to pay ~102 USDC"
Actual situation:
User only inputted 100 USDC
Result: ❌ Insufficient amount or route calculation error
SDK Supports But Widget Doesn't Implement
While @lifi/sdk's ContractCallsQuoteRequest type supports both modes:
// @lifi/types/src/api.ts
export type ContractCallsQuoteRequestFromAmount = {
fromAmount: string // ✅ SDK supports
// ...
}
export type ContractCallsQuoteRequestToAmount = {
toAmount: string // ✅ Widget uses this
// ...
}
export type ContractCallsQuoteRequest =
| ContractCallsQuoteRequestFromAmount
| ContractCallsQuoteRequestToAmount
The Widget implementation only uses the toAmount mode.
Expected Behavior
Option 1: Support fromAmount Mode (Recommended)
Modify useRoutes.ts to support ContractCall based on fromAmount:
if (subvariant === 'custom' && contractCalls) {
// Check which mode to use
const hasToAmount = toAmount && Number(toTokenAmount) > 0
const hasFromAmount = fromAmount && Number(fromTokenAmount) > 0
if (hasToAmount) {
// Existing logic: use toAmount mode
const contractCallQuote = await getContractCallsQuote({
toAmount: toAmount.toString(),
// ...
})
} else if (hasFromAmount) {
// New logic: use fromAmount mode
const contractCallQuote = await getContractCallsQuote({
fromAmount: fromAmount.toString(),
// ...
})
}
}
Option 2: Provide Documentation
If fromAmount mode cannot be technically supported, suggest documenting:
- How to correctly calculate
toAmountin ContractCall scenarios - Provide example code for two-phase calculation
- Explain why the design enforces
toAmountmode
Current Workaround
We currently use a two-phase calculation:
- Phase 1: Let Widget calculate routes in regular mode (without
contractCalls) - Monitor route results, extract
routes[0].toAmount - Phase 2: Use calculated
toAmountto setcontractCalls - Widget recalculates (using
getContractCallsQuote)
However, this leads to:
- Two API calls
- Complex state management
- Potential UI flickering
Impact Scope
This issue affects all scenarios using the following combination:
-
subvariant: 'custom' -
subvariantOptions: { custom: 'deposit' }or'checkout' - Dynamically generated
contractCalls - Users primarily input
fromAmount
Typical scenarios include:
- Protocol Deposits
- NFT Checkout
- Token Staking
- Custom Contract Interactions
Environment Information
- Widget Version: 3.x
- SDK Version: @lifi/sdk 3.x
- Browsers: Chrome/Safari/Firefox (all browsers)
Related Discussions
- ContractCall official documentation: https://docs.li.fi/
- Similar issues: (link if any related issues exist)
Thank you for considering this enhancement request! Happy to provide more information or test cases if needed.