Skip to content

Commit

Permalink
adjust amount when there is a fee error
Browse files Browse the repository at this point in the history
  • Loading branch information
atn4z7 committed Nov 26, 2024
1 parent 47a00de commit af8152e
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 47 deletions.
86 changes: 68 additions & 18 deletions packages/core-mobile/app/hooks/earn/useClaimFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import { Avalanche } from '@avalabs/core-wallets-sdk'
import { Network } from '@avalabs/core-chains-sdk'
import { pvm } from '@avalabs/avalanchejs'
import { useAvalancheXpProvider } from 'hooks/networks/networkProviderHooks'
import { getAssetId } from 'services/wallet/utils'
import { SendErrorMessage } from 'screens/send/utils/types'
import { usePChainBalance } from './usePChainBalance'
import { useGetFeeState } from './useGetFeeState'
import { extractNeededAmount } from './utils/extractNeededAmount'

/**
* a hook to calculate the fees needed to do a cross chain transfer from P to C chain
Expand All @@ -38,15 +41,16 @@ export const useClaimFees = (
exportPFee?: TokenUnit
totalClaimable?: TokenUnit
defaultTxFee?: TokenUnit
feeCalculationError?: Error
feeCalculationError?: SendErrorMessage
} => {
const isDevMode = useSelector(selectIsDeveloperMode)
const activeAccount = useSelector(selectActiveAccount)
const activeNetwork = useSelector(selectActiveNetwork)
const [totalFees, setTotalFees] = useState<TokenUnit>()
const [exportFee, setExportFee] = useState<TokenUnit>()
const [defaultTxFee, setDefaultTxFee] = useState<TokenUnit>()
const [feeCalculationError, setFeeCalculationError] = useState<Error>()
const [feeCalculationError, setFeeCalculationError] =
useState<SendErrorMessage>()
const { getFeeState, defaultFeeState } = useGetFeeState()
const pChainBalance = usePChainBalance()
const xpProvider = useAvalancheXpProvider()
Expand Down Expand Up @@ -83,7 +87,7 @@ export const useClaimFees = (
) {
return
}
const txFee = await getExportFeeFromDummyTx({
const txFee = await getExportPFee({
amountInNAvax: totalClaimable,
activeAccount,
avaxXPNetwork,
Expand Down Expand Up @@ -126,7 +130,8 @@ export const useClaimFees = (
})

const importCFee = calculateCChainFee(instantBaseFee, unsignedTx)
const exportPFee = await getExportFeeFromDummyTx({

const exportPFee = await getExportPFee({
amountInNAvax: totalClaimable,
activeAccount,
avaxXPNetwork,
Expand All @@ -136,7 +141,14 @@ export const useClaimFees = (

Logger.info('importCFee', importCFee.toDisplay())
Logger.info('exportPFee', exportPFee.toDisplay())
setTotalFees(importCFee.add(exportPFee))

const allFees = importCFee.add(exportPFee)

if (allFees.gt(totalClaimable)) {
throw SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE
}

setTotalFees(allFees)
setExportFee(exportPFee)
}

Expand All @@ -145,8 +157,16 @@ export const useClaimFees = (
setFeeCalculationError(undefined)
})
.catch(err => {
setFeeCalculationError(err)
Logger.warn(err)
if (
(err instanceof Error &&
err.message
.toLowerCase()
.includes('insufficient funds: provided utxos need')) ||
err === SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE
) {
setFeeCalculationError(SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE)
}
})
}, [
activeAccount,
Expand All @@ -167,7 +187,7 @@ export const useClaimFees = (
}
}

const getExportFeeFromDummyTx = async ({
const getExportPFee = async ({
amountInNAvax,
activeAccount,
avaxXPNetwork,
Expand All @@ -179,19 +199,49 @@ const getExportFeeFromDummyTx = async ({
avaxXPNetwork: Network
provider: Avalanche.JsonRpcProvider
feeState?: pvm.FeeState
missingAvax?: bigint
}): Promise<TokenUnit> => {
if (provider.isEtnaEnabled()) {
const unsignedTxP = await WalletService.createExportPTx({
// intentionally dividing the amount by 2 to make sure
// the export fee is not greater than the available balance in p-chain,
// so we can get the correct txFee for the export
amountInNAvax: amountInNAvax.div(2n).toSubUnit(),
accountIndex: activeAccount.index,
avaxXPNetwork,
destinationAddress: activeAccount.addressPVM,
destinationChain: 'C',
feeState
})
let unsignedTxP
try {
unsignedTxP = await WalletService.createExportPTx({
amountInNAvax: amountInNAvax.toSubUnit(),
accountIndex: activeAccount.index,
avaxXPNetwork,
destinationAddress: activeAccount.addressPVM,
destinationChain: 'C',
feeState
})
} catch (error) {
Logger.warn('unable to create export p tx', error)

const missingAmount = extractNeededAmount(
(error as Error).message,
getAssetId(avaxXPNetwork)
)

if (missingAmount) {
const amountAvailableToClaim = amountInNAvax.toSubUnit() - missingAmount

if (amountAvailableToClaim <= 0) {
// rethrow insufficient funds error when balance is not enough to cover fee
throw error
}

unsignedTxP = await WalletService.createExportPTx({
amountInNAvax: amountAvailableToClaim,
accountIndex: activeAccount.index,
avaxXPNetwork,
destinationAddress: activeAccount.addressPVM,
destinationChain: 'C',
feeState
})
} else {
// rethrow error if it's not an insufficient funds error
throw error
}
}

const tx = await Avalanche.parseAvalancheTx(
unsignedTxP,
provider,
Expand Down
3 changes: 2 additions & 1 deletion packages/core-mobile/app/hooks/earn/useClaimRewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { selectActiveNetwork } from 'store/network'
import { isDevnet } from 'utils/isDevnet'
import { TokenUnit } from '@avalabs/core-utils-sdk'
import { useMemo } from 'react'
import { SendErrorMessage } from 'screens/send/utils/types'
import { useClaimFees } from './useClaimFees'
import { useGetFeeState } from './useGetFeeState'

Expand All @@ -34,7 +35,7 @@ export const useClaimRewards = (
mutation: UseMutationResult<void, Error, void, unknown>
defaultTxFee?: TokenUnit
totalFees?: TokenUnit
feeCalculationError?: Error
feeCalculationError?: SendErrorMessage
// eslint-disable-next-line max-params
} => {
const queryClient = useQueryClient()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { extractNeededAmount } from './extractNeededAmount'

it('should return the correct BigInt amount for a matching error message', () => {
const errorMessage =
'Insufficient funds: provided UTXOs need 10057 more nAVAX (asset id: U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK)'
const assetId = 'U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK'

const result = extractNeededAmount(errorMessage, assetId)
expect(result).toBe(BigInt(10057))
})

it('should return null for an error message that does not match the regex', () => {
const errorMessage = 'Some random error message without needed amount'
const assetId = 'U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK'

const result = extractNeededAmount(errorMessage, assetId)
expect(result).toBeNull()
})

it('should return null if the asset ID in the error message does not match the provided asset ID', () => {
const errorMessage =
'Insufficient funds: provided UTXOs need 10057 more nAVAX (asset id: U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK)'
const assetId = 'DifferentAssetId'

const result = extractNeededAmount(errorMessage, assetId)
expect(result).toBeNull()
})

it('should return null if the needed amount is not present in the error message', () => {
const errorMessage =
'Insufficient funds: provided UTXOs need more nAVAX (asset id: U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK)'
const assetId = 'U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK'

const result = extractNeededAmount(errorMessage, assetId)
expect(result).toBeNull()
})

it('should handle edge cases like empty error messages gracefully', () => {
const errorMessage = ''
const assetId = 'U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK'

const result = extractNeededAmount(errorMessage, assetId)
expect(result).toBeNull()
})
16 changes: 16 additions & 0 deletions packages/core-mobile/app/hooks/earn/utils/extractNeededAmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// use a regex to match "Insufficient funds" and capture the missing amount
export const extractNeededAmount = (
errorMessage: string,
assetId: string
): bigint | null => {
const regex = new RegExp(
`Insufficient funds.*need (\\d+) more nAVAX \\(asset id: ${assetId}\\)`
)
const match = errorMessage.match(regex)

if (match && match[1]) {
return BigInt(match[1]) // convert to BigInt and return
}

return null // return null if no match is found
}
28 changes: 14 additions & 14 deletions packages/core-mobile/app/screens/earn/ClaimRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,18 @@ const ClaimRewards = (): JSX.Element | null => {
isFocused && unableToGetFees, // re-enable this checking whenever this screen is focused
timeToShowNetworkFeeError
)
const insufficientBalanceForFee =
feeCalculationError === SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE

const shouldDisableClaimButton =
unableToGetFees || excessiveNetworkFee || Boolean(feeCalculationError)
unableToGetFees || excessiveNetworkFee || insufficientBalanceForFee

useEffect(() => {
if (showFeeError) {
if (showFeeError && !insufficientBalanceForFee) {
navigate(AppNavigation.Earn.FeeUnavailable)
}
}, [navigate, showFeeError])
}, [navigate, showFeeError, insufficientBalanceForFee])

const [claimableAmountInAvax, claimableAmountInCurrency] = useMemo(() => {
if (data?.balancePerType.unlockedUnstaked) {
const unlockedInUnit = new TokenUnit(
Expand Down Expand Up @@ -234,19 +237,16 @@ const ClaimRewards = (): JSX.Element | null => {
{SendErrorMessage.EXCESSIVE_NETWORK_FEE}
</Text>
)}
{feeCalculationError &&
feeCalculationError.message
.toLowerCase()
.includes('insufficient funds: provided utxos need') && (
<Text
testID="insufficent_balance_error_msg"
variant="body2"
sx={{ color: '$dangerMain' }}>
{SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE}
</Text>
)}
</>
)}
{insufficientBalanceForFee && (
<Text
testID="insufficent_balance_error_msg"
variant="body2"
sx={{ color: '$dangerMain' }}>
{SendErrorMessage.INSUFFICIENT_BALANCE_FOR_FEE}
</Text>
)}
</ConfirmScreen>
)
}
Expand Down
21 changes: 7 additions & 14 deletions packages/core-mobile/app/services/wallet/WalletService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ import {
import WalletInitializer from './WalletInitializer'
import WalletFactory from './WalletFactory'
import MnemonicWalletInstance from './MnemonicWallet'
import {
getAssetId,
TESTNET_AVAX_ASSET_ID,
MAINNET_AVAX_ASSET_ID
} from './utils'

type InitProps = {
mnemonic: string
Expand All @@ -57,10 +62,6 @@ const EVM_FEE_TOLERANCE = 50
// We increase C chain base fee by 20% for instant speed
const BASE_FEE_MULTIPLIER = 0.2

const MAINNET_AVAX_ASSET_ID = Avalanche.MainnetContext.avaxAssetID
const TESTNET_AVAX_ASSET_ID = Avalanche.FujiContext.avaxAssetID
const DEVNET_AVAX_ASSET_ID = Avalanche.DevnetContext.avaxAssetID

class WalletService {
#walletType: WalletType = WalletType.UNSET

Expand Down Expand Up @@ -457,11 +458,7 @@ class WalletService {
chain: 'P',
toAddress: destinationAddress,
amountsPerAsset: {
[isDevnet(avaxXPNetwork)
? DEVNET_AVAX_ASSET_ID
: avaxXPNetwork.isTestnet
? TESTNET_AVAX_ASSET_ID
: MAINNET_AVAX_ASSET_ID]: amountInNAvax
[getAssetId(avaxXPNetwork)]: amountInNAvax
},
options: {
changeAddresses: [changeAddress]
Expand Down Expand Up @@ -693,11 +690,7 @@ class WalletService {
accountIndex,
avaxXPNetwork
)
const assetId = isDevnet(avaxXPNetwork)
? DEVNET_AVAX_ASSET_ID
: avaxXPNetwork.isTestnet
? TESTNET_AVAX_ASSET_ID
: MAINNET_AVAX_ASSET_ID
const assetId = getAssetId(avaxXPNetwork)

const utxos = getTransferOutputUtxos({
amt: stakingAmount,
Expand Down
15 changes: 15 additions & 0 deletions packages/core-mobile/app/services/wallet/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import {
TransferOutput,
Utxo
} from '@avalabs/avalanchejs'
import { Avalanche } from '@avalabs/core-wallets-sdk'
import { isDevnet } from 'utils/isDevnet'
import { Network } from '@avalabs/core-chains-sdk'
import {
AvalancheTransactionRequest,
BtcTransactionRequest,
SignTransactionRequest
} from './types'

export const MAINNET_AVAX_ASSET_ID = Avalanche.MainnetContext.avaxAssetID
export const TESTNET_AVAX_ASSET_ID = Avalanche.FujiContext.avaxAssetID
export const DEVNET_AVAX_ASSET_ID = Avalanche.DevnetContext.avaxAssetID

export const isBtcTransactionRequest = (
request: SignTransactionRequest
): request is BtcTransactionRequest => {
Expand Down Expand Up @@ -69,3 +76,11 @@ export const getStakeableOutUtxos = ({
)
)
)

export const getAssetId = (avaxXPNetwork: Network): string => {
return isDevnet(avaxXPNetwork)
? DEVNET_AVAX_ASSET_ID
: avaxXPNetwork.isTestnet
? TESTNET_AVAX_ASSET_ID
: MAINNET_AVAX_ASSET_ID
}

0 comments on commit af8152e

Please sign in to comment.