Skip to content

Commit 22f69d4

Browse files
Refactor useFees hook (velocitylabs-org#272)
1 parent 4589e5c commit 22f69d4

File tree

8 files changed

+3895
-1465
lines changed

8 files changed

+3895
-1465
lines changed

app/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@hookform/resolvers": "3.9.0",
2020
"@nextui-org/react": "^2.4.8",
2121
"@number-flow/react": "^0.4.1",
22-
"@paraspell/sdk": "^8.2.1",
22+
"@paraspell/sdk": "^8.3.1",
2323
"@polkadot/api": "^14.0.1",
2424
"@polkadot/api-base": "^14.0.1",
2525
"@polkadot/apps-config": "^0.145.1",

app/pnpm-lock.yaml

+32-32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/src/hooks/useFees.tsx

+75-132
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ import { Chain } from '@/models/chain'
33
import { NotificationSeverity } from '@/models/notification'
44
import { Token } from '@/models/token'
55
import { AmountInfo } from '@/models/transfer'
6-
import { EthereumTokens, PolkadotTokens } from '@/registry/mainnet/tokens'
76
import { getCachedTokenPrice } from '@/services/balance'
87
import { Direction, resolveDirection } from '@/services/transfer'
98
import { getPlaceholderAddress } from '@/utils/address'
10-
import { getCurrencyId, getNativeToken, getRelayNode } from '@/utils/paraspell'
11-
import { safeConvertAmount, toHuman } from '@/utils/transfer'
12-
import { getOriginFeeDetails, getTNode } from '@paraspell/sdk'
9+
import { getCurrencyId, getNativeToken, getRelayNode, getParaSpellNode } from '@/utils/paraspell'
10+
import { toHuman } from '@/utils/transfer'
11+
import { getOriginFeeDetails, TNodeDotKsmWithRelayChains } from '@paraspell/sdk'
1312
import { captureException } from '@sentry/nextjs'
14-
import { Context, toEthereum, toPolkadot } from '@snowbridge/api'
1513
import { useCallback, useEffect, useState } from 'react'
1614
import useEnvironment from './useEnvironment'
1715
import useSnowbridgeContext from './useSnowbridgeContext'
18-
import { ContractTransaction } from 'ethers'
16+
import { getRoute } from '@/utils/routes'
17+
import { getFeeEstimate } from '@/utils/snowbridge'
18+
19+
export type Fee =
20+
| { origin: 'Ethereum'; bridging: AmountInfo; execution: AmountInfo | null }
21+
| { origin: 'Polkadot'; fee: AmountInfo }
1922

2023
const useFees = (
2124
sourceChain?: Chain | null,
@@ -41,123 +44,93 @@ const useFees = (
4144
return
4245
}
4346

44-
const direction = resolveDirection(sourceChain, destinationChain)
45-
// TODO: this should be the fee token, not necessarily the native token. Also adjust the USD value accordingly below.
46-
const nativeToken = getNativeToken(sourceChain)
47+
const route = getRoute(env, sourceChain, destinationChain)
48+
if (!route) throw new Error('Route not supported')
49+
50+
// TODO: this should be the fee token, not necessarily the native token.
51+
const feeToken = getNativeToken(sourceChain)
4752

4853
try {
4954
setLoading(true)
50-
let fees: string
51-
let tokenUSDValue: number = 0
52-
53-
if (
54-
(direction === Direction.ToEthereum || direction === Direction.ToPolkadot) &&
55-
isSnowbridgeContextLoading
56-
) {
57-
setFees(null)
58-
setEthereumTxFees(null)
59-
return
60-
}
6155

62-
switch (direction) {
63-
case Direction.ToEthereum: {
64-
if (!snowbridgeContext || snowbridgeContextError)
65-
throw snowbridgeContextError ?? new Error('Snowbridge context undefined')
66-
tokenUSDValue = (await getCachedTokenPrice(PolkadotTokens.DOT))?.usd ?? 0
67-
fees = (await toEthereum.getSendFee(snowbridgeContext)).toString()
56+
switch (route.sdk) {
57+
case 'ParaSpellApi': {
58+
const relay = getRelayNode(env)
59+
const sourceChainNode = getParaSpellNode(sourceChain, relay)
60+
if (!sourceChainNode) throw new Error('Source chain id not found')
61+
62+
const destinationChainNode = getParaSpellNode(destinationChain, relay)
63+
if (!destinationChainNode) throw new Error('Destination chain id not found')
64+
65+
const currency = getCurrencyId(env, sourceChainNode, sourceChain.uid, token)
66+
const info = await getOriginFeeDetails({
67+
origin: sourceChainNode as TNodeDotKsmWithRelayChains,
68+
destination: destinationChainNode,
69+
currency: { ...currency, amount: BigInt(10 ** token.decimals).toString() }, // hardcoded amount because the fee is usually independent of the amount
70+
account: getPlaceholderAddress(sourceChain.supportedAddressTypes[0]), // hardcode sender address because the fee is usually independent of the sender
71+
accountDestination: getPlaceholderAddress(destinationChain.supportedAddressTypes[0]), // hardcode recipient address because the fee is usually independent of the recipient
72+
api: sourceChain.rpcConnection,
73+
ahAccount: getPlaceholderAddress(sourceChain.supportedAddressTypes[0]),
74+
})
75+
76+
const feeTokenInDollars = (await getCachedTokenPrice(feeToken))?.usd ?? 0
77+
const fee = info.xcmFee
78+
setFees({
79+
amount: fee,
80+
token: feeToken,
81+
inDollars: feeTokenInDollars ? toHuman(fee, feeToken) * feeTokenInDollars : 0,
82+
})
83+
setCanPayFees(info.sufficientForXCM)
84+
6885
break
6986
}
7087

71-
case Direction.ToPolkadot: {
88+
case 'SnowbridgeApi': {
89+
const direction = resolveDirection(sourceChain, destinationChain)
90+
if (
91+
(direction === Direction.ToEthereum || direction === Direction.ToPolkadot) &&
92+
isSnowbridgeContextLoading
93+
) {
94+
setFees(null)
95+
setEthereumTxFees(null)
96+
return
97+
}
98+
7299
if (!snowbridgeContext || snowbridgeContextError)
73100
throw snowbridgeContextError ?? new Error('Snowbridge context undefined')
74-
tokenUSDValue = (await getCachedTokenPrice(EthereumTokens.ETH))?.usd ?? 0
75101

76-
const sendFee = await toPolkadot.getSendFee(
102+
const fee = await getFeeEstimate(
103+
token,
104+
destinationChain,
105+
direction,
77106
snowbridgeContext,
78-
token.address,
79-
destinationChain.chainId,
80-
BigInt(0),
107+
senderAddress,
108+
recipientAddress,
109+
amount,
81110
)
82-
fees = sendFee.toString()
111+
if (!fee) {
112+
setFees(null)
113+
setEthereumTxFees(null)
114+
return
115+
}
83116

84-
try {
85-
if (!senderAddress || !recipientAddress || !amount || !sendFee) {
86-
setEthereumTxFees(null)
117+
switch (fee.origin) {
118+
case 'Ethereum': {
119+
setFees(fee.bridging)
120+
setEthereumTxFees(fee.execution)
121+
break
122+
}
123+
case 'Polkadot': {
124+
setFees(fee.fee)
87125
break
88126
}
89-
// Sender, Recipient and amount can't be defaulted here since the Smart contract verify the ERC20 token allowance.
90-
const { tx } = await toPolkadot.createTx(
91-
snowbridgeContext.config.appContracts.gateway,
92-
senderAddress,
93-
recipientAddress,
94-
token.address,
95-
destinationChain.chainId,
96-
safeConvertAmount(amount, token) ?? 0n,
97-
sendFee,
98-
BigInt(0),
99-
)
100-
101-
const { txFees, txFeesInDollars } = await estimateTransactionFees(
102-
tx,
103-
snowbridgeContext,
104-
nativeToken,
105-
tokenUSDValue,
106-
)
107-
108-
setEthereumTxFees({
109-
amount: txFees,
110-
token: nativeToken,
111-
inDollars: txFeesInDollars ? txFeesInDollars : 0,
112-
})
113-
break
114-
} catch (error) {
115-
// Estimation can fail for multiple reasons, including errors such as insufficient token approval.
116-
console.log('Estimated Tx cost failed', error instanceof Error && { ...error })
117-
captureException(new Error('Estimated Tx cost failed'), {
118-
level: 'warning',
119-
tags: {
120-
useFeesHook:
121-
error instanceof Error && 'action' in error && typeof error.action === 'string'
122-
? error.action
123-
: 'estimateTransactionFees',
124-
},
125-
extra: { error },
126-
})
127-
break
128127
}
129-
}
130-
131-
case Direction.WithinPolkadot: {
132-
const relay = getRelayNode(env)
133-
const sourceChainNode = getTNode(sourceChain.chainId, relay)
134-
const destinationChainNode = getTNode(destinationChain.chainId, relay)
135-
if (!sourceChainNode || !destinationChainNode) throw new Error('Chain id not found')
136-
const currency = getCurrencyId(env, sourceChainNode, sourceChain.uid, token)
137-
138-
const info = await getOriginFeeDetails({
139-
origin: sourceChainNode,
140-
destination: destinationChainNode,
141-
currency: { ...currency, amount: BigInt(10 ** token.decimals).toString() }, // hardcoded amount because the fee is usually independent of the amount
142-
account: getPlaceholderAddress(sourceChain.supportedAddressTypes[0]), // hardcode sender address because the fee is usually independent of the sender
143-
accountDestination: getPlaceholderAddress(destinationChain.supportedAddressTypes[0]), // hardcode recipient address because the fee is usually independent of the recipient
144-
api: sourceChain.rpcConnection,
145-
})
146-
tokenUSDValue = (await getCachedTokenPrice(nativeToken))?.usd ?? 0
147-
fees = info.xcmFee.toString()
148-
setCanPayFees(info.sufficientForXCM)
149128
break
150129
}
151130

152131
default:
153132
throw new Error('Unsupported direction')
154133
}
155-
156-
setFees({
157-
amount: fees,
158-
token: nativeToken,
159-
inDollars: tokenUSDValue ? toHuman(fees, nativeToken) * tokenUSDValue : 0,
160-
})
161134
} catch (error) {
162135
setFees(null)
163136
setEthereumTxFees(null)
@@ -171,6 +144,7 @@ const useFees = (
171144
} finally {
172145
setLoading(false)
173146
}
147+
174148
// eslint-disable-next-line react-hooks/exhaustive-deps
175149
}, [
176150
env,
@@ -191,35 +165,4 @@ const useFees = (
191165
return { fees, ethereumTxfees, loading, refetch: fetchFees, canPayFees }
192166
}
193167

194-
/**
195-
* Estimates the gas cost for a given Ethereum transaction in both native token and USD value.
196-
*
197-
* @param tx - The contract transaction object.
198-
* @param snowbridgeContext - The Snowbridge context containing Ethereum API.
199-
* @param nativeToken - The native token.
200-
* @param nativeTokenUSDValue - The USD value of the native token.
201-
* @returns An object containing the tx estimate gas fee in native tokens and its USD value.
202-
*/
203-
const estimateTransactionFees = async (
204-
tx: ContractTransaction,
205-
snowbridgeContext: Context,
206-
nativeToken: Token,
207-
nativeTokenUSDValue: number,
208-
) => {
209-
// Fetch gas estimation and fee data
210-
const [txGas, { gasPrice, maxPriorityFeePerGas }] = await Promise.all([
211-
snowbridgeContext.ethereum().estimateGas(tx),
212-
snowbridgeContext.ethereum().getFeeData(),
213-
])
214-
215-
// Get effective fee per gas & get USD fee value
216-
const effectiveFeePerGas = (gasPrice ?? 0n) + (maxPriorityFeePerGas ?? 0n)
217-
const txFeesInToken = toHuman((txGas * effectiveFeePerGas).toString(), nativeToken)
218-
219-
return {
220-
txFees: txFeesInToken,
221-
txFeesInDollars: txFeesInToken * nativeTokenUSDValue,
222-
}
223-
}
224-
225168
export default useFees

0 commit comments

Comments
 (0)