Skip to content

ContractCall in custom subvariant requires toAmount, causing UX issues and logical conflicts #575

@smartchainark

Description

@smartchainark
## 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:

  • fromAmount mode (user inputs source amount)
  • toAmount mode (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

  1. User Input Pattern: Users naturally input fromAmount ("How much do I want to pay/deposit?")
  2. Widget Requirement: ContractCall functionality mandates toAmount ("How much should arrive?")
  3. Calculation Dilemma: Developers cannot accurately calculate toAmount without 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:

  1. How to correctly calculate toAmount in ContractCall scenarios
  2. Provide example code for two-phase calculation
  3. Explain why the design enforces toAmount mode

Current Workaround

We currently use a two-phase calculation:

  1. Phase 1: Let Widget calculate routes in regular mode (without contractCalls)
  2. Monitor route results, extract routes[0].toAmount
  3. Phase 2: Use calculated toAmount to set contractCalls
  4. 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


Thank you for considering this enhancement request! Happy to provide more information or test cases if needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions