Skip to content

Commit 863712a

Browse files
committed
SD-102: Split fee calculation logic into separate hooks for shielding and sending operations
1 parent 0e48fb8 commit 863712a

File tree

11 files changed

+273
-159
lines changed

11 files changed

+273
-159
lines changed

src/domains/misc/utils/getQueryKey.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NetworkEnvironment } from 'src/domains/chains/types/misc';
55
export const QUERY_KEYS = {
66
sendingFees: 'sendingFees',
77
shieldingFees: 'shieldingFees',
8+
allowanceCheck: 'allowance-check',
89
allowanceFeeEstimate: 'allowanceFeeEstimate',
910
shielderClient: 'shielder-client',
1011
shielderTransactions: 'shielder-transactions',
@@ -33,6 +34,13 @@ const getQueryKey = {
3334
chainId,
3435
tokenType,
3536
],
37+
allowanceCheck: (tokenAddress: Address, chainId: string, walletAddress: Address, amount: string) => [
38+
QUERY_KEYS.allowanceCheck,
39+
tokenAddress,
40+
chainId,
41+
walletAddress,
42+
amount,
43+
],
3644
allowanceFeeEstimate: (tokenAddress: Address, chainId: string, walletAddress: Address, amount: string) => [
3745
QUERY_KEYS.allowanceFeeEstimate,
3846
tokenAddress,

src/domains/shielder/components/TokenList/Modals/ConfirmPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const ConfirmPage = ({
5555
decimalsQuery: { data: nativeTokenDecimals },
5656
} = useTokenData({ isNative: true }, ['symbol', 'decimals']);
5757

58-
const feeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
58+
const openFeeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
5959

6060
const isButtonDisabled = amount <= 0n || !!hasInsufficientFees || !!isLoading;
6161

@@ -98,7 +98,7 @@ const ConfirmPage = ({
9898
label={
9999
<TotalFee>
100100
<p>Est. Total fee</p>
101-
<button onClick={() => void feeBreakdownModal()}>
101+
<button onClick={() => void openFeeBreakdownModal()}>
102102
<CIcon size={16} icon="InfoRegular" />
103103
</button>
104104
</TotalFee>

src/domains/shielder/components/TokenList/Modals/SendModal/SelectAmountPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const SelectAmountPage = ({ onContinue, token, hasInsufficientFees }: Props) =>
4747
decimalsQuery: { data: nativeTokenDecimals },
4848
} = useTokenData({ isNative: true }, ['symbol', 'decimals']);
4949

50-
const feeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
50+
const openFeeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
5151

5252
const maxAmountToSend = useMemo(() => {
5353
if (isNullish(token.balance)) return token.balance;
@@ -99,7 +99,7 @@ const SelectAmountPage = ({ onContinue, token, hasInsufficientFees }: Props) =>
9999
label={
100100
<TotalFeeLabel>
101101
<p>Est. Total fee</p>
102-
<button onClick={() => void feeBreakdownModal()}>
102+
<button onClick={() => void openFeeBreakdownModal()}>
103103
<CIcon size={16} icon="InfoRegular" />
104104
</button>
105105
</TotalFeeLabel>

src/domains/shielder/components/TokenList/Modals/ShieldModal/SelectAmountPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const SelectAmountPage = ({ onContinue, token, hasInsufficientFees }: Props) =>
5353
decimalsQuery: { data: nativeTokenDecimals },
5454
} = useTokenData({ isNative: true }, ['symbol', 'decimals']);
5555

56-
const feeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
56+
const openFeeBreakdownModal = useFeeBreakdownModal({ fees, totalFee });
5757

5858
const maxAmountToShield = useMemo(() => {
5959
if (isNullish(token.balance)) return token.balance;
@@ -104,7 +104,7 @@ const SelectAmountPage = ({ onContinue, token, hasInsufficientFees }: Props) =>
104104
label={
105105
<TotalFeeLabel>
106106
<p>Est. Total fee</p>
107-
<button onClick={() => void feeBreakdownModal()}>
107+
<button onClick={() => void openFeeBreakdownModal()}>
108108
<CIcon size={16} icon="InfoRegular" />
109109
</button>
110110
</TotalFeeLabel>

src/domains/shielder/components/TransactionDetailsModal/ActivityDetailsModal.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useMemo } from 'react';
21
import styled, { css } from 'styled-components';
32
import { Address } from 'viem';
43
import { useTransactionReceipt } from 'wagmi';
@@ -49,10 +48,9 @@ const ActivityDetailsModal = (props: Props) => {
4948
},
5049
});
5150

52-
const totalFee = useMemo(() => transactionReceipt && transaction?.txHash ?
51+
const totalFee = transactionReceipt && transaction?.txHash ?
5352
transactionReceipt.gasUsed * transactionReceipt.effectiveGasPrice :
54-
null,
55-
[transactionReceipt, transaction?.txHash]);
53+
null;
5654

5755
const token = transaction?.token?.type === 'erc20' ?
5856
{ address: transaction.token.address, isNative: false as const } :

src/domains/shielder/stores/getShielderIndexedDB.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ShielderTransaction } from '@cardinal-cryptography/shielder-sdk';
22
import { openDB, deleteDB, IDBPDatabase } from 'idb';
3-
import { Address, sha256 } from 'viem';
3+
import { Address, isAddress, sha256 } from 'viem';
44
import { z } from 'zod';
55

66
import isPresent from 'src/domains/misc/utils/isPresent';
@@ -9,12 +9,12 @@ const DB_INDEX = 3;
99
const DB_BASE_NAME = 'SHIELDER_STORAGE';
1010
const DB_NAME = `${DB_BASE_NAME}_V${DB_INDEX}`;
1111

12-
export const STORE_SHIELDER = 'shielder-store';
13-
export const STORE_TRANSACTIONS = 'shielder-transactions';
12+
const STORE_SHIELDER = 'shielder-store';
13+
const STORE_TRANSACTIONS = 'shielder-transactions';
1414

15-
const ethAddress = z.string().refine(
16-
(address): address is `0x${string}` => /^0x[a-fA-F0-9]{40}$/.test(address),
17-
{ message: 'Must be a valid Ethereum address' }
15+
const ethAddress = z.custom<`0x${string}`>(
16+
val => typeof val === 'string' ? isAddress(val, { strict: true }) : false,
17+
val => ({ message: `Invalid Ethereum address: "${val}".` }),
1818
);
1919

2020
const txHash = z.string().refine(

src/domains/shielder/utils/useEstimateAllowanceFee.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { skipToken, useQuery } from '@tanstack/react-query';
22
import { erc20Abi, encodeFunctionData } from 'viem';
3-
import { useEstimateGas, useGasPrice, usePublicClient } from 'wagmi';
3+
import { useEstimateFeesPerGas, useEstimateGas, usePublicClient } from 'wagmi';
44

55
import { useWallet } from 'src/domains/chains/components/WalletProvider.tsx';
66
import { Token } from 'src/domains/chains/types/misc';
77
import useChain from 'src/domains/chains/utils/useChain';
8+
import getQueryKey from 'src/domains/misc/utils/getQueryKey';
89

910
type Props = {
1011
token: Token,
1112
amount: bigint,
12-
enabled?: boolean,
13+
disabled?: boolean,
1314
};
1415

15-
const useEstimateAllowanceFee = ({ token, amount, enabled = true }: Props) => {
16+
const useEstimateAllowanceFee = ({ token, amount, disabled }: Props) => {
1617
const chainConfig = useChain();
1718
const publicClient = usePublicClient();
1819
const { address: walletAddress } = useWallet();
1920

20-
const shouldCheckAllowance = !token.isNative && amount > 0n && enabled;
21+
const shouldCheckAllowance = !token.isNative && amount > 0n && !disabled;
2122

2223
const { data: needsApproval } = useQuery({
2324
queryKey: chainConfig && walletAddress && shouldCheckAllowance ?
24-
['allowance-check', token.address, chainConfig.id.toString(), walletAddress, amount.toString()] : [],
25+
getQueryKey.allowanceCheck(token.address, chainConfig.id.toString(), walletAddress, amount.toString()) : [],
2526
queryFn: !publicClient || !chainConfig?.shielderConfig || !walletAddress || !shouldCheckAllowance ?
2627
skipToken :
2728
async (): Promise<boolean> => {
@@ -50,20 +51,35 @@ const useEstimateAllowanceFee = ({ token, amount, enabled = true }: Props) => {
5051
abi: erc20Abi,
5152
functionName: 'approve',
5253
args: [chainConfig.shielderConfig.shielderContractAddress, amount],
53-
}) : undefined,
54+
}) :
55+
undefined,
5456
account: walletAddress,
5557
query: {
56-
enabled: shouldCheckAllowance && !!needsApproval && !!chainConfig?.shielderConfig && !!walletAddress,
58+
enabled:
59+
shouldCheckAllowance &&
60+
!!needsApproval &&
61+
!!chainConfig?.shielderConfig &&
62+
!!walletAddress,
5763
},
5864
});
5965

60-
const { data: gasPrice } = useGasPrice({
66+
const { data: feeEstimate } = useEstimateFeesPerGas({
67+
chainId: chainConfig?.id,
6168
query: {
62-
enabled: shouldCheckAllowance && !!needsApproval && !!gasEstimate,
69+
enabled:
70+
shouldCheckAllowance &&
71+
!!needsApproval &&
72+
!!chainConfig?.shielderConfig &&
73+
!!walletAddress,
6374
},
6475
});
6576

66-
const allowanceFee = gasEstimate && gasPrice ? gasEstimate * gasPrice : null;
77+
const gasPrice = feeEstimate?.maxFeePerGas;
78+
79+
const allowanceFee =
80+
gasEstimate && feeEstimate?.maxFeePerGas ?
81+
gasEstimate * feeEstimate.maxFeePerGas :
82+
null;
6783

6884
if (!shouldCheckAllowance) {
6985
return {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { erc20Token, nativeToken } from '@cardinal-cryptography/shielder-sdk';
2+
import { skipToken, useQuery } from '@tanstack/react-query';
3+
import { useMemo } from 'react';
4+
5+
import { useWallet } from 'src/domains/chains/components/WalletProvider.tsx';
6+
import { Token } from 'src/domains/chains/types/misc';
7+
import useChain from 'src/domains/chains/utils/useChain';
8+
import getQueryKey from 'src/domains/misc/utils/getQueryKey';
9+
import useShielderClient from 'src/domains/shielder/utils/useShielderClient';
10+
11+
export type FeeItem = {
12+
type: 'network' | 'relayer' | 'allowance',
13+
amount: bigint,
14+
token: string,
15+
};
16+
17+
type Props = {
18+
token: Token,
19+
disabled?: boolean,
20+
};
21+
22+
const useSendingFees = ({ token, disabled }: Props) => {
23+
const chainConfig = useChain();
24+
const { data: shielderClient } = useShielderClient();
25+
const { address: walletAddress } = useWallet();
26+
27+
const {
28+
data: withdrawingFees,
29+
error: withdrawingFeesError,
30+
isLoading: areWithdrawingFeesLoading,
31+
} = useQuery({
32+
queryKey: chainConfig && walletAddress && !disabled ?
33+
getQueryKey.sendingFees(walletAddress, chainConfig.id.toString()) : [],
34+
queryFn: !shielderClient || disabled ?
35+
skipToken :
36+
async () => {
37+
const sdkToken = token.isNative ? nativeToken() : erc20Token(token.address);
38+
return await shielderClient.getWithdrawFees(sdkToken, 0n);
39+
},
40+
enabled: !!shielderClient && !!chainConfig && !!walletAddress && !disabled,
41+
});
42+
43+
const fees: FeeItem[] | undefined = useMemo(() => {
44+
if (!withdrawingFees) {
45+
return undefined;
46+
}
47+
48+
const { fee_details } = withdrawingFees;
49+
const hasCommission = fee_details.commission_fee_token > 0n;
50+
51+
const networkFee: FeeItem = {
52+
type: 'network',
53+
amount: fee_details.gas_cost_fee_token,
54+
token: 'native',
55+
};
56+
57+
if (hasCommission) {
58+
const relayerFee: FeeItem = {
59+
type: 'relayer',
60+
amount: fee_details.commission_fee_token,
61+
token: 'native',
62+
};
63+
return [networkFee, relayerFee];
64+
}
65+
66+
return [networkFee];
67+
}, [withdrawingFees]);
68+
69+
return {
70+
fees,
71+
isLoading: areWithdrawingFeesLoading,
72+
error: withdrawingFeesError,
73+
};
74+
};
75+
76+
export default useSendingFees;

src/domains/shielder/utils/useShield.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { erc20Token, nativeToken } from '@cardinal-cryptography/shielder-sdk';
22
import { useMutation, useQueryClient } from '@tanstack/react-query';
33
import styled from 'styled-components';
44
import { v4 } from 'uuid';
5-
import { useAccount, useSendTransaction } from 'wagmi';
5+
import { erc20Abi } from 'viem';
6+
import { useAccount, useSendTransaction, usePublicClient, useWalletClient } from 'wagmi';
67

78
import { Token } from 'src/domains/chains/types/misc';
9+
import useChain from 'src/domains/chains/utils/useChain';
810
import { useToast } from 'src/domains/misc/components/Toast';
911
import getQueryKey, { MUTATION_KEYS } from 'src/domains/misc/utils/getQueryKey';
1012
import { useActivityHistory } from 'src/domains/shielder/utils/useActivityHistory';
@@ -18,6 +20,9 @@ const useShield = () => {
1820
const { data: shielderClient } = useShielderClient();
1921
const { address: walletAddress, chainId } = useAccount();
2022
const { sendTransactionAsync } = useSendTransaction();
23+
const { data: walletClient } = useWalletClient();
24+
const publicClient = usePublicClient();
25+
const chainConfig = useChain();
2126
const queryClient = useQueryClient();
2227
const { showToast } = useToast();
2328
const { openTransactionModal } = useActivityModal();
@@ -89,6 +94,30 @@ const useShield = () => {
8994
if (!walletAddress) throw new Error('Address is not available');
9095
if (!chainId) throw new Error('Chain ID is not available');
9196

97+
if (!token.isNative) {
98+
if (!publicClient) throw new Error('Public client is not ready');
99+
if (!walletClient) throw new Error('Wallet client is not ready');
100+
if (!chainConfig?.shielderConfig) throw new Error('Shielder is not configured for this chain.');
101+
102+
const allowance = await publicClient.readContract({
103+
address: token.address,
104+
abi: erc20Abi,
105+
functionName: 'allowance',
106+
args: [walletAddress, chainConfig.shielderConfig.shielderContractAddress],
107+
});
108+
109+
if (allowance < amount) {
110+
const approveTxHash = await walletClient.writeContract({
111+
address: token.address,
112+
abi: erc20Abi,
113+
functionName: 'approve',
114+
args: [chainConfig.shielderConfig.shielderContractAddress, amount],
115+
});
116+
117+
await publicClient.waitForTransactionReceipt({ hash: approveTxHash });
118+
}
119+
}
120+
92121
const sdkToken = token.isNative ? nativeToken() : erc20Token(token.address);
93122

94123
await shielderClient.shield(

0 commit comments

Comments
 (0)