diff --git a/apps/legacy/package.json b/apps/legacy/package.json
index 4d5af57de..02e4ed4ea 100644
--- a/apps/legacy/package.json
+++ b/apps/legacy/package.json
@@ -15,9 +15,9 @@
"typecheck": "yarn tsc --skipLibCheck --noEmit"
},
"dependencies": {
- "@avalabs/avalanche-module": "1.9.10",
+ "@avalabs/avalanche-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/avalanchejs": "5.1.0-alpha.2",
- "@avalabs/bitcoin-module": "1.9.10",
+ "@avalabs/bitcoin-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/bridge-unified": "4.0.3",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.60",
"@avalabs/core-chains-sdk": "3.1.0-alpha.60",
@@ -30,13 +30,13 @@
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.60",
"@avalabs/core-utils-sdk": "3.1.0-alpha.60",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.60",
- "@avalabs/evm-module": "1.9.10",
+ "@avalabs/evm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/glacier-sdk": "3.1.0-alpha.60",
- "@avalabs/hvm-module": "1.9.10",
+ "@avalabs/hvm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/hw-app-avalanche": "0.14.1",
- "@avalabs/svm-module": "1.9.10",
+ "@avalabs/svm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/types": "3.1.0-alpha.60",
- "@avalabs/vm-module-types": "1.9.10",
+ "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@blockaid/client": "0.10.0",
"@coinbase/cbpay-js": "1.6.0",
"@core/common": "workspace:*",
diff --git a/apps/legacy/src/localization/locales/en/translation.json b/apps/legacy/src/localization/locales/en/translation.json
index a2a909534..ae1db0e01 100644
--- a/apps/legacy/src/localization/locales/en/translation.json
+++ b/apps/legacy/src/localization/locales/en/translation.json
@@ -1046,7 +1046,7 @@
"Transaction has been cancelled": "Transaction has been cancelled",
"Transaction has been rejected": "Transaction has been rejected",
"Transaction has failed": "Transaction has failed",
- "Transaction pre-exution is unavailable. The displayed token list might be incomplete.": "Transaction pre-exution is unavailable. The displayed token list might be incomplete.",
+ "Transaction pre-execution is unavailable. The displayed token list might be incomplete.": "Transaction pre-execution is unavailable. The displayed token list might be incomplete.",
"Transaction rejected": "Transaction rejected",
"Transaction timed out": "Transaction timed out",
"Transfer": "Transfer",
diff --git a/apps/legacy/src/pages/SignTransaction/components/TxBalanceChange.tsx b/apps/legacy/src/pages/SignTransaction/components/TxBalanceChange.tsx
index d6d13c8fb..f79566151 100644
--- a/apps/legacy/src/pages/SignTransaction/components/TxBalanceChange.tsx
+++ b/apps/legacy/src/pages/SignTransaction/components/TxBalanceChange.tsx
@@ -42,7 +42,7 @@ export const TxBalanceChange = ({
tooltip={
showNoPreExecWarning
? t(
- 'Transaction pre-exution is unavailable. The displayed token list might be incomplete.',
+ 'Transaction pre-execution is unavailable. The displayed token list might be incomplete.',
)
: ''
}
diff --git a/apps/next/package.json b/apps/next/package.json
index 074bd6a03..344304d3c 100644
--- a/apps/next/package.json
+++ b/apps/next/package.json
@@ -22,7 +22,7 @@
"@avalabs/core-wallets-sdk": "3.1.0-alpha.60",
"@avalabs/k2-alpine": "1.228.0",
"@avalabs/types": "3.1.0-alpha.60",
- "@avalabs/vm-module-types": "1.9.10",
+ "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@core/common": "workspace:*",
"@core/messaging": "workspace:*",
"@core/service-worker": "workspace:*",
diff --git a/apps/next/src/config/constants.ts b/apps/next/src/config/constants.ts
index 7788594bb..8fea3fc3e 100644
--- a/apps/next/src/config/constants.ts
+++ b/apps/next/src/config/constants.ts
@@ -1,3 +1,5 @@
+import { TransactionPriority } from '@core/types';
+
export const CORE_WEB_BASE_URL =
process.env.CORE_WEB_BASE_URL ?? 'https://core.app';
export const BUG_BOUNTIES_URL =
@@ -9,3 +11,6 @@ export const CORE_FEEDBACK_URL =
'https://docs.google.com/forms/d/e/1FAIpQLSdUQiVnJoqQ1g_6XTREpkSB5vxKKK8ba5DRjhzQf1XVeET8Rw/viewform?usp=pp_url&entry.2070152111=Core%20browser%20extension&entry.903657115=${extensionVersion}&entry.1148340936=${os}';
export const DARK_THEME_SURFACE_COLOR = '#404046';
+
+export const DEFAULT_FEE_PRESET: TransactionPriority = 'low';
+export const DEFAULT_FEE_PRESET_C_CHAIN: TransactionPriority = 'high';
diff --git a/apps/next/src/localization/locales/en/translation.json b/apps/next/src/localization/locales/en/translation.json
index a5fd10ff1..1f407ac83 100644
--- a/apps/next/src/localization/locales/en/translation.json
+++ b/apps/next/src/localization/locales/en/translation.json
@@ -38,6 +38,7 @@
"All keys contained in this file are already imported.": "All keys contained in this file are already imported.",
"Allow Chrome to access your camera to scan the QR code.": "Allow Chrome to access your camera to scan the QR code.",
"Alternatively, open any authenticator app and enter this code:": "Alternatively, open any authenticator app and enter this code:",
+ "Amount": "Amount",
"Amount is too low": "Amount is too low",
"Amount is too small to proceed.": "Amount is too small to proceed.",
"An error occurred while computing the price.": "An error occurred while computing the price.",
@@ -47,6 +48,7 @@
"Anyone with this private key can access the account(s) associated with it": "Anyone with this private key can access the account(s) associated with it",
"Approval transaction failed": "Approval transaction failed",
"Approve": "Approve",
+ "Approves all {{token}}": "Approves all {{token}}",
"Approving will give this dApp access to your active account.": "Approving will give this dApp access to your active account.",
"Are you sure that you want to cancel this request?": "Are you sure that you want to cancel this request?",
"Are you sure you want to delete selected accounts?": "Are you sure you want to delete selected accounts?",
@@ -62,6 +64,7 @@
"Average password - this will do": "Average password - this will do",
"Awesome!": "Awesome!",
"Back": "Back",
+ "Balance change unavailable.": "Balance change unavailable.",
"Balances loading...": "Balances loading...",
"Bitcoin": "Bitcoin",
"Bridge initialization failed": "Bridge initialization failed",
@@ -75,6 +78,7 @@
"Cancel": "Cancel",
"Cancel Export": "Cancel Export",
"Cancel request": "Cancel request",
+ "Cancel transaction": "Cancel transaction",
"Cancellation Failed": "Cancellation Failed",
"Cancelling will require you to restart the 2 day waiting period": "Cancelling will require you to restart the 2 day waiting period",
"Cancelling...": "Cancelling...",
@@ -91,7 +95,6 @@
"Closing the settings menu will require you to restart the 2 day waiting period.": "Closing the settings menu will require you to restart the 2 day waiting period.",
"Code copied to clipboard": "Code copied to clipboard",
"Code verification error": "Code verification error",
- "Coming soon!": "Coming soon!",
"Confirm": "Confirm",
"Confirm Bridge": "Confirm Bridge",
"Confirm addresses": "Confirm addresses",
@@ -133,6 +136,7 @@
"Dark": "Dark",
"Decide what default view works best for you, either a floating interface or a sidebar docked to the side to show more content": "Decide what default view works best for you, either a floating interface or a sidebar docked to the side to show more content",
"Decrypt Recovery Phrase": "Decrypt Recovery Phrase",
+ "Default": "Default",
"Delete": "Delete",
"Delete selected": "Delete selected",
"Deleting these accounts is permanent and cannot be undone": "Deleting these accounts is permanent and cannot be undone",
@@ -148,6 +152,7 @@
"Download Ledger Live to update": "Download Ledger Live to update",
"Drop your file here to upload": "Drop your file here to upload",
"Edit network fee": "Edit network fee",
+ "Edit spend limit": "Edit spend limit",
"Email address": "Email address",
"Enable a sandbox environment for testing without using real funds": "Enable a sandbox environment for testing without using real funds",
"English": "English",
@@ -196,6 +201,7 @@
"French": "French",
"Gas fees paid by Core": "Gas fees paid by Core",
"Gas limit": "Gas limit",
+ "Gasless funding failed": "Gasless funding failed",
"General": "General",
"Generate a new account in your active wallet": "Generate a new account in your active wallet",
"German": "German",
@@ -297,6 +303,7 @@
"Name this contact": "Name this contact",
"Name your Passkey": "Name your Passkey",
"Name your Yubikey": "Name your Yubikey",
+ "Network": "Network",
"Network error": "Network error",
"Network fee amount": "Network fee amount",
"New password": "New password",
@@ -364,11 +371,14 @@
"Private Key Import Failed": "Private Key Import Failed",
"Private Key Imported": "Private Key Imported",
"Private key copied!": "Private key copied!",
+ "Proceed anyway": "Proceed anyway",
+ "Proceed with caution.": "Proceed with caution.",
"Protect your data and ensure the highest level of security for your Core wallet.": "Protect your data and ensure the highest level of security for your Core wallet.",
"Public key not found": "Public key not found",
"Receive crypto": "Receive crypto",
"Recent": "Recent",
"Recents": "Recents",
+ "Recipient": "Recipient",
"Recovery Phrase": "Recovery Phrase",
"Recovery Phrase {{number}}": "Recovery Phrase {{number}}",
"Recovery methods": "Recovery methods",
@@ -423,6 +433,7 @@
"Send to": "Send to",
"Sending this token is not supported yet.": "Sending this token is not supported yet.",
"Sending this type of token is not supported by Core": "Sending this type of token is not supported by Core",
+ "Set a limit that you will allow to automatically spend.": "Set a limit that you will allow to automatically spend.",
"Settings": "Settings",
"Show me Trending Tokens": "Show me Trending Tokens",
"Show password": "Show password",
@@ -440,6 +451,7 @@
"Something went wrong. Please try again.": "Something went wrong. Please try again.",
"Sort": "Sort",
"Spanish": "Spanish",
+ "Spend limit": "Spend limit",
"Staked": "Staked",
"Stay updated on latest airdrops, events and more! You can unsubscribe anytime. For more details, see our Privacy Policy": "Stay updated on latest airdrops, events and more! You can unsubscribe anytime. For more details, see our Privacy Policy",
"Storage update failed": "Storage update failed",
@@ -456,6 +468,7 @@
"The amount cannot be lower than the bridging fee": "The amount cannot be lower than the bridging fee",
"The base fee is set by the network and changes frequently. Any difference between the set max base fee and the actual base fee will be refunded.": "The base fee is set by the network and changes frequently. Any difference between the set max base fee and the actual base fee will be refunded.",
"The bridging fee is unknown": "The bridging fee is unknown",
+ "The displayed token list might be incomplete.": "The displayed token list might be incomplete.",
"The export process could not be completed. Please try again.": "The export process could not be completed. Please try again.",
"The field is required": "The field is required",
"The key you entered is invalid. Please try again": "The key you entered is invalid. Please try again",
@@ -474,6 +487,7 @@
"This asset cannot be bridged": "This asset cannot be bridged",
"This email looks invalid": "This email looks invalid",
"This file requires a password. This password was set when the file was created.": "This file requires a password. This password was set when the file was created.",
+ "This is a placeholder screen just to allow us to reject/approve some wallet-specific interactions (i.e. chain switching).": "This is a placeholder screen just to allow us to reject/approve some wallet-specific interactions (i.e. chain switching).",
"This is taking longer than expected. Please try again later.": "This is taking longer than expected. Please try again later.",
"This key gives access to your account’s addresses": "This key gives access to your account’s addresses",
"This operation requires {{total}} approvals.": "This operation requires {{total}} approvals.",
@@ -481,6 +495,7 @@
"This phrase is your access key to your wallet. Carefully write it down and store it in a safe location": "This phrase is your access key to your wallet. Carefully write it down and store it in a safe location",
"This recovery phrase is already imported.": "This recovery phrase is already imported.",
"This token contract is missing a required method.": "This token contract is missing a required method.",
+ "This transaction has been flagged as malicious. I understand the risk and want to proceed anyway.": "This transaction has been flagged as malicious. I understand the risk and want to proceed anyway.",
"This transaction would likely fail": "This transaction would likely fail",
"This wallet is already imported": "This wallet is already imported",
"This wallet is already imported.": "This wallet is already imported.",
@@ -497,6 +512,7 @@
"Transaction has been cancelled": "Transaction has been cancelled",
"Transaction has been rejected": "Transaction has been rejected",
"Transaction has failed": "Transaction has failed",
+ "Transaction pre-execution is unavailable.": "Transaction pre-execution is unavailable.",
"Transaction rejected": "Transaction rejected",
"Transaction successful": "Transaction successful",
"Transaction timed out": "Transaction timed out",
@@ -530,6 +546,8 @@
"Unknown network": "Unknown network",
"Unknown network fee": "Unknown network fee",
"Unknown transaction error": "Unknown transaction error",
+ "Unlimited": "Unlimited",
+ "Unlimited {{currency}}": "Unlimited {{currency}}",
"Unlock airdrops": "Unlock airdrops",
"Unnamed FIDO Device": "Unnamed FIDO Device",
"Unsupporetd secret type": "Unsupporetd secret type",
@@ -568,6 +586,7 @@
"Wallet renamed": "Wallet renamed",
"Wallet secrets not found for the requested ID": "Wallet secrets not found for the requested ID",
"We were not able to verify this code. Please try again.": "We were not able to verify this code. Please try again.",
+ "We're unable to cover the gas fees for your transaction at this time. As a result, this feature has been disabled.": "We're unable to cover the gas fees for your transaction at this time. As a result, this feature has been disabled.",
"Weak password! Try adding more characters": "Weak password! Try adding more characters",
"When you connect to a dApp, it will appear here": "When you connect to a dApp, it will appear here",
"Wrong quote provider": "Wrong quote provider",
diff --git a/apps/next/src/pages/Approve/ExtensionActionApprovalScreen.tsx b/apps/next/src/pages/Approve/ExtensionActionApprovalScreen.tsx
new file mode 100644
index 000000000..8df32bec4
--- /dev/null
+++ b/apps/next/src/pages/Approve/ExtensionActionApprovalScreen.tsx
@@ -0,0 +1,52 @@
+import { useCallback } from 'react';
+import { Typography } from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+
+import { ActionStatus } from '@core/types';
+import { useApproveAction, useGetRequestId } from '@core/ui';
+
+import {
+ ActionDrawer,
+ ApprovalScreenTitle,
+ LoadingScreen,
+ Styled,
+} from './components';
+
+export const ExtensionActionApprovalScreen = () => {
+ const { t } = useTranslation();
+
+ const requestId = useGetRequestId();
+ const { action, updateAction, cancelHandler } = useApproveAction(requestId);
+
+ const approve = useCallback(async () => {
+ updateAction({
+ status: ActionStatus.SUBMITTING,
+ id: requestId,
+ });
+ }, [updateAction, requestId]);
+
+ if (!action) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {t(
+ 'This is a placeholder screen just to allow us to reject/approve some wallet-specific interactions (i.e. chain switching).',
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/GenericApprovalScreen.tsx b/apps/next/src/pages/Approve/GenericApprovalScreen.tsx
index bca29e1ee..4400669d1 100644
--- a/apps/next/src/pages/Approve/GenericApprovalScreen.tsx
+++ b/apps/next/src/pages/Approve/GenericApprovalScreen.tsx
@@ -17,28 +17,41 @@ import {
LoadingScreen,
Styled,
UnsupportedNetworkScreen,
+ MaliciousTxOverlay,
+ NoteWarning,
} from './components';
+import { hasNoteWarning, hasOverlayWarning } from './lib';
+import { useGasless } from './hooks';
const POLLED_BALANCES = [TokenType.NATIVE, TokenType.ERC20]; // Approval screen should always have the latest balance
export const GenericApprovalScreen = () => {
useLiveBalance(POLLED_BALANCES);
-
const requestId = useGetRequestId();
const { getNetwork, networks } = useNetworkContext();
+
const { action, updateAction, cancelHandler, error } =
useApproveAction(requestId);
- // TODO: handle gasless
+ const { tryFunding, setGaslessDefaultValues } = useGasless({ action });
+
const approve = useCallback(async () => {
- updateAction(
- {
- status: ActionStatus.SUBMITTING,
- id: requestId,
- },
- false, // TODO: handle hardware wallets
- );
- }, [updateAction, requestId]);
+ tryFunding(() => {
+ updateAction(
+ {
+ status: ActionStatus.SUBMITTING,
+ id: requestId,
+ },
+ false, // TODO: handle hardware wallets
+ );
+ });
+ }, [updateAction, requestId, tryFunding]);
+
+ const cancel = useCallback(() => {
+ // Reset the gasless state
+ setGaslessDefaultValues();
+ cancelHandler();
+ }, [cancelHandler, setGaslessDefaultValues]);
const network = action ? getNetwork(action.scope) : undefined;
@@ -55,7 +68,7 @@ export const GenericApprovalScreen = () => {
// TODO: Should we still allow approvals?
return (
-
+
);
}
@@ -64,6 +77,9 @@ export const GenericApprovalScreen = () => {
+ {hasNoteWarning(action) && (
+
+ )}
{
error={error}
/>
-
+
+ {hasOverlayWarning(action) && (
+
+ )}
);
};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/ActionDetails.tsx b/apps/next/src/pages/Approve/components/ActionDetails/ActionDetails.tsx
index b4c4938d9..d3539ff70 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/ActionDetails.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/ActionDetails.tsx
@@ -1,9 +1,10 @@
-import { isEvmNetwork } from '@core/types';
+import { isBtcNetwork, isEvmNetwork } from '@core/types';
import { ActionDetailsProps } from '../../types';
import { EvmActionDetails } from './evm/EvmActionDetails';
import { UnknownActionDetails } from './UnknownActionDetails';
+import { BtcActionDetails } from './btc';
export const ActionDetails = ({
network,
@@ -22,6 +23,17 @@ export const ActionDetails = ({
);
}
+ if (isBtcNetwork(network)) {
+ return (
+
+ );
+ }
+
return (
& {
+ network: BtcNetwork;
+};
+
+export const BtcActionDetails = ({
+ action,
+ network,
+}: BtcActionDetailsProps) => {
+ const { t } = useTranslation();
+
+ const details = mapBitcoinDetails({ network, t })(action.displayData.details);
+
+ return (
+
+ {details.map((section) => (
+
+ {section.items.map((item, itemIndex) => (
+
+ ))}
+
+ ))}
+ {action.displayData.networkFeeSelector && (
+
+ )}
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/BtcNetworkFeeWidget.tsx b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/BtcNetworkFeeWidget.tsx
new file mode 100644
index 000000000..8007891cc
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/BtcNetworkFeeWidget.tsx
@@ -0,0 +1,64 @@
+import { FC } from 'react';
+import { DisplayData } from '@avalabs/vm-module-types';
+import { useTranslation } from 'react-i18next';
+import { Fade, Stack, Typography } from '@avalabs/k2-alpine';
+
+import { Action, BtcNetwork } from '@core/types';
+
+import { DetailsSection } from '../../../generic/DetailsSection';
+import { TotalFeeAmount } from '../../../generic/NetworkFee';
+
+import { useBtcTransactionFee } from './hooks/useBtcTransactionFee';
+import { FeePresetSelector } from './components';
+
+type BtcNetworkFeeWidget = {
+ action: Action;
+ network: BtcNetwork;
+};
+
+export const BtcNetworkFeeWidget: FC = ({
+ action,
+ network,
+}) => {
+ const { t } = useTranslation();
+ const {
+ fee,
+ feePreset,
+ choosePreset,
+ nativeToken,
+ isLoading,
+ hasEnoughForNetworkFee,
+ } = useBtcTransactionFee({
+ action,
+ network,
+ });
+
+ return (
+
+
+
+ {!isLoading && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ {t('Insufficient balance for fee')}
+
+
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/FeePresetSelector.tsx b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/FeePresetSelector.tsx
new file mode 100644
index 000000000..68b457f94
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/FeePresetSelector.tsx
@@ -0,0 +1,48 @@
+import { FC } from 'react';
+import { Stack } from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+
+import { FeePresetButton } from '../../../../generic/NetworkFee';
+import { BtcFeePreset } from '../types';
+
+type FeePresetSelectorProps = {
+ feePreset: BtcFeePreset;
+ choosePreset: (preset: BtcFeePreset) => void;
+};
+
+export const FeePresetSelector: FC = ({
+ feePreset,
+ choosePreset,
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+ choosePreset('low')}
+ >
+ {t('Slow')}
+
+ choosePreset('medium')}
+ >
+ {t('Normal')}
+
+ choosePreset('high')}
+ >
+ {t('Fast')}
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/index.ts
new file mode 100644
index 000000000..756feed5a
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/components/index.ts
@@ -0,0 +1 @@
+export * from './FeePresetSelector';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/index.ts
new file mode 100644
index 000000000..616f12751
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/index.ts
@@ -0,0 +1 @@
+export * from './useBtcTransactionFee';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getFeeInfo.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getFeeInfo.ts
new file mode 100644
index 000000000..8af2faed5
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getFeeInfo.ts
@@ -0,0 +1,6 @@
+import { BtcTxSigningData } from '../types';
+
+export const getFeeInfo = ({ data }: BtcTxSigningData) => ({
+ feeRate: BigInt(data.feeRate),
+ limit: Math.ceil(data.fee / data.feeRate) || 0,
+});
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getInitialFeeRate.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getInitialFeeRate.ts
new file mode 100644
index 000000000..d09d4d6aa
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/getInitialFeeRate.ts
@@ -0,0 +1,11 @@
+import { BtcTxSigningData } from '../types';
+
+export const getInitialFeeRate = (
+ data?: BtcTxSigningData,
+): bigint | undefined => {
+ if (!data) {
+ return undefined;
+ }
+
+ return data.data.feeRate ? BigInt(data.data.feeRate) : undefined;
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/hasEnoughForFee.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/hasEnoughForFee.ts
new file mode 100644
index 000000000..9e5943c72
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/hasEnoughForFee.ts
@@ -0,0 +1,16 @@
+import { NativeTokenBalance } from '@core/types';
+
+import { BtcTxSigningData } from '../types';
+import { getFeeInfo } from './getFeeInfo';
+
+export const hasEnoughForFee = (
+ data?: BtcTxSigningData,
+ nativeToken?: NativeTokenBalance,
+) => {
+ if (!data || !nativeToken) return false;
+
+ const info = getFeeInfo(data);
+ const need = info.feeRate * BigInt(info.limit);
+
+ return nativeToken.balance > need;
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/index.ts
new file mode 100644
index 000000000..f937ecaf7
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/lib/index.ts
@@ -0,0 +1,3 @@
+export * from './getFeeInfo';
+export * from './hasEnoughForFee';
+export * from './getInitialFeeRate';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/types.ts
new file mode 100644
index 000000000..acef60258
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/types.ts
@@ -0,0 +1,42 @@
+import { DisplayData, RpcMethod, SigningData } from '@avalabs/vm-module-types';
+
+import type { calculateGasAndFees } from '@core/common';
+import { Action, BtcNetwork, NativeTokenBalance } from '@core/types';
+
+import { BtcFeePreset } from '../../types';
+
+export type BtcTxSigningData = Extract<
+ SigningData,
+ {
+ type: RpcMethod.BITCOIN_SEND_TRANSACTION;
+ }
+>;
+
+type ResultBase = {
+ fee: ReturnType;
+ feePreset: BtcFeePreset;
+ choosePreset: (preset: BtcFeePreset) => void;
+ nativeToken: NativeTokenBalance;
+ hasEnoughForNetworkFee: boolean;
+};
+
+export type UseBtcTransactionFeeReadyResult = {
+ isLoading: false;
+} & ResultBase;
+
+export type UseBtcTransactionFeeLoadingResult = {
+ isLoading: true;
+} & Partial;
+
+export type UseBtcTransactionFeeArgs = {
+ action: Action;
+ network: BtcNetwork;
+};
+
+export type UseBtcTransactionFeeResult =
+ | UseBtcTransactionFeeReadyResult
+ | UseBtcTransactionFeeLoadingResult;
+
+export type UseBtcTransactionFee = (
+ args: UseBtcTransactionFeeArgs,
+) => UseBtcTransactionFeeResult;
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/useBtcTransactionFee.tsx b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/useBtcTransactionFee.tsx
new file mode 100644
index 000000000..d42043ab8
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/hooks/useBtcTransactionFee/useBtcTransactionFee.tsx
@@ -0,0 +1,90 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import { ExtensionRequest } from '@core/types';
+import { useConnectionContext } from '@core/ui';
+import { calculateGasAndFees } from '@core/common';
+import { type UpdateActionTxDataHandler } from '@core/service-worker';
+
+import { DEFAULT_FEE_PRESET } from '@/config';
+import { useNativeToken } from '@/hooks/useNativeToken';
+import { useUpdateAccountBalance } from '@/hooks/useUpdateAccountBalance';
+import { useCurrentFeesForNetwork } from '@/hooks/useCurrentFeesForNetwork';
+
+import { BtcFeePreset } from '../../types';
+import { BtcTxSigningData, UseBtcTransactionFee } from './types';
+import { getFeeInfo, getInitialFeeRate, hasEnoughForFee } from './lib';
+
+export const useBtcTransactionFee: UseBtcTransactionFee = ({
+ action,
+ network,
+}) => {
+ useUpdateAccountBalance(network);
+
+ const { request } = useConnectionContext();
+
+ const networkFee = useCurrentFeesForNetwork(network);
+ const nativeToken = useNativeToken({ network });
+ const signingData = action.signingData as BtcTxSigningData;
+
+ const [feePreset, setFeePreset] = useState(DEFAULT_FEE_PRESET);
+
+ const fee = calculateGasAndFees({
+ maxFeePerGas: getFeeInfo(signingData).feeRate,
+ tokenPrice: nativeToken?.priceInCurrency,
+ tokenDecimals: network?.networkToken.decimals,
+ gasLimit: getFeeInfo(signingData).limit,
+ });
+
+ const updateFee = useCallback(
+ async (feeRate: bigint) => {
+ if (!action.actionId) {
+ return;
+ }
+
+ await request({
+ method: ExtensionRequest.ACTION_UPDATE_TX_DATA,
+ params: [action.actionId, { feeRate: Number(feeRate) }],
+ });
+ },
+ [action?.actionId, request],
+ );
+
+ const choosePreset = useCallback(
+ async (preset: BtcFeePreset) => {
+ if (!networkFee) {
+ return;
+ }
+
+ setFeePreset(preset);
+
+ await updateFee(networkFee[preset].maxFeePerGas);
+ },
+ [networkFee, updateFee],
+ );
+
+ const initialFeeRate = useRef(
+ getInitialFeeRate(signingData),
+ );
+
+ useEffect(() => {
+ // If the dapp did not provide us any fee rate, we must initialize it ourselves.
+ if (!initialFeeRate.current) {
+ choosePreset(DEFAULT_FEE_PRESET);
+ }
+ }, [networkFee, choosePreset]);
+
+ if (!networkFee || !nativeToken || !signingData) {
+ return {
+ isLoading: true,
+ };
+ }
+
+ return {
+ hasEnoughForNetworkFee: hasEnoughForFee(signingData, nativeToken),
+ fee,
+ feePreset,
+ choosePreset,
+ nativeToken,
+ isLoading: false,
+ };
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/index.ts
new file mode 100644
index 000000000..8c9b06e44
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/index.ts
@@ -0,0 +1 @@
+export * from './BtcNetworkFeeWidget';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/types.ts
new file mode 100644
index 000000000..b5e2cb345
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/BtcNetworkFeeWidget/types.ts
@@ -0,0 +1,3 @@
+import { TransactionPriority } from '@core/types';
+
+export type BtcFeePreset = TransactionPriority;
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/components/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/index.ts
new file mode 100644
index 000000000..8c9b06e44
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/components/index.ts
@@ -0,0 +1 @@
+export * from './BtcNetworkFeeWidget';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/index.ts
new file mode 100644
index 000000000..d2d6e1a5c
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/index.ts
@@ -0,0 +1 @@
+export * from './BtcActionDetails';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/btcDetailsUtils.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/btcDetailsUtils.ts
new file mode 100644
index 000000000..723d0e1be
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/btcDetailsUtils.ts
@@ -0,0 +1,72 @@
+import { TFunction } from 'react-i18next';
+import { DetailItemType, DetailSection } from '@avalabs/vm-module-types';
+
+import { BtcNetwork } from '@core/types';
+
+type MapFunction = (
+ network: BtcNetwork,
+ t: TFunction,
+) => (section: DetailSection, index: number) => DetailSection;
+
+// The funds recipients section for NextGen looks different than from the legacy one.
+// To keep things backwards-compatible, we map the recipients items here instead of
+// updating the Bitcoin Module.
+// FIXME: This should be changed (in sync with Core Mobile) once we fully migrate
+// to the new NextGen UI.
+const mapRecipientsSection: MapFunction =
+ (_, t) =>
+ (section: DetailSection): DetailSection => {
+ // Even if this `title` changes, all we're gonna miss are
+ // the "Recipient" and "Amount" labels at the top of the section.
+ if (section.title !== 'Recipients') {
+ return section;
+ }
+
+ return {
+ ...section,
+ items: [
+ {
+ type: DetailItemType.TEXT,
+ label: t('Recipient'),
+ value: t('Amount'),
+ alignment: 'horizontal',
+ },
+ ...section.items,
+ ],
+ };
+ };
+
+// FIXME: Turns out there is a discrepancy between EvmModule and BtcModule,
+// where the former has a network detail item by default in the {displayData.details}
+// field, while the latter does not.
+const mapFirstSection: MapFunction =
+ (network, t) =>
+ (section: DetailSection, index: number): DetailSection => {
+ if (index !== 0) return section;
+
+ return {
+ ...section,
+ items: [
+ {
+ type: DetailItemType.NETWORK,
+ label: t('Network'),
+ value: {
+ name: network.chainName,
+ logoUri: network.logoUri,
+ },
+ },
+ ...section.items,
+ ],
+ };
+ };
+
+const mapFunctions: MapFunction[] = [mapRecipientsSection, mapFirstSection];
+
+export const mapBitcoinDetails =
+ ({ network, t }: { network: BtcNetwork; t: TFunction }) =>
+ (sections: DetailSection[]): DetailSection[] => {
+ return mapFunctions.reduce(
+ (acc, mapFunction) => acc.map(mapFunction(network, t)),
+ sections,
+ );
+ };
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/index.ts
new file mode 100644
index 000000000..48d29bfdb
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/btc/lib/index.ts
@@ -0,0 +1 @@
+export * from './btcDetailsUtils';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx
index e0ea9cb95..09ed0c83a 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmActionDetails.tsx
@@ -1,12 +1,15 @@
import { Stack } from '@avalabs/k2-alpine';
+import { DisplayData } from '@avalabs/vm-module-types';
-import { EvmNetwork } from '@core/types';
+import { Action, EnsureDefined, EvmNetwork } from '@core/types';
import { ActionDetailsProps } from '../../../types';
-import { DetailsSection } from '../generic/DetailsSection';
import { DetailsItem } from '../generic/DetailsItem';
-import { TransactionBalanceChange } from '../generic/TransactionBalanceChange/TransactionBalanceChange';
-import { EvmNetworkFeeWidget } from './EvmNetworkFeeWidget/EvmNetworkFeeWidget';
+import { DetailsSection } from '../generic/DetailsSection';
+import { TransactionBalanceChange } from '../generic/TransactionBalanceChange';
+
+import { EvmTokenApprovals } from './EvmTokenApprovals';
+import { EvmNetworkFeeWidget } from './EvmNetworkFeeWidget';
type EvmActionDetailsProps = Omit & {
network: EvmNetwork;
@@ -18,13 +21,12 @@ export const EvmActionDetails = ({
}: EvmActionDetailsProps) => {
return (
- {action.displayData.balanceChange && (
-
- )}
+
+ {hasTokenApprovals(action) && }
{action.displayData.details.map((section) => (
{section.items.map((item, index) => (
@@ -38,3 +40,9 @@ export const EvmActionDetails = ({
);
};
+
+const hasTokenApprovals = (
+ action: Action,
+): action is Action> => {
+ return action.displayData.tokenApprovals !== undefined;
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx
index 3a3e6afc8..c132272a5 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/EvmNetworkFeeWidget.tsx
@@ -1,15 +1,17 @@
+import { useCallback } from 'react';
import { DisplayData } from '@avalabs/vm-module-types';
-import { Fade, Stack, Typography } from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+import { Collapse, Fade, Stack, Typography } from '@avalabs/k2-alpine';
-import { Action, EvmNetwork } from '@core/types';
+import { Action, EvmNetwork, GaslessPhase } from '@core/types';
+import { useGasless } from '../../../../hooks';
import { DetailsSection } from '../../generic/DetailsSection';
import { TotalFeeAmount } from '../../generic/NetworkFee';
import { GaslessSwitchRow } from './components/GaslessSwitch';
import { useEvmTransactionFee } from './hooks/useEvmTransactionFee';
import { FeePresetSelector } from './components';
-import { useTranslation } from 'react-i18next';
type EvmNetworkFeeWidgetProps = {
action: Action;
@@ -36,29 +38,58 @@ export const EvmNetworkFeeWidget = ({
network,
});
+ const { isGaslessOn, setIsGaslessOn, gaslessPhase, isGaslessEligible } =
+ useGasless({ action });
+
+ const onGaslessChange = useCallback(
+ (_, checked: boolean) => {
+ // Do not allow changing the gasless switch if the gasless funding failed already
+ if (gaslessPhase === GaslessPhase.ERROR) {
+ return;
+ }
+ choosePreset?.('high');
+ setIsGaslessOn(checked);
+ },
+ [choosePreset, setIsGaslessOn, gaslessPhase],
+ );
+
return (
-
-
- {!isLoading && (
- <>
-
-
- >
- )}
-
+
+
+
+
+
+ {!isLoading && (
+ <>
+
+
+ >
+ )}
+
+
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx
index 479a1e9f7..416b7516d 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/FeePresetSelector.tsx
@@ -46,20 +46,20 @@ export const FeePresetSelector: FC = ({
px={2}
>
choosePreset('slow')}
+ color={feePreset === 'low' ? 'primary' : 'secondary'}
+ onClick={() => choosePreset('low')}
>
{t('Slow')}
choosePreset('normal')}
+ color={feePreset === 'medium' ? 'primary' : 'secondary'}
+ onClick={() => choosePreset('medium')}
>
{t('Normal')}
choosePreset('fast')}
+ color={feePreset === 'high' ? 'primary' : 'secondary'}
+ onClick={() => choosePreset('high')}
>
{t('Fast')}
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx
index 7aa51b79e..20517048a 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/components/GaslessSwitch.tsx
@@ -1,7 +1,26 @@
import { useTranslation } from 'react-i18next';
-import { Stack, Switch, Tooltip, Typography } from '@avalabs/k2-alpine';
+import { FC } from 'react';
+import {
+ Stack,
+ Switch,
+ SwitchProps,
+ Tooltip,
+ Typography,
+} from '@avalabs/k2-alpine';
-export const GaslessSwitchRow = () => {
+type GaslessSwitchRowProps = Pick<
+ SwitchProps,
+ 'checked' | 'onChange' | 'disabled'
+> & {
+ tooltip: string;
+};
+
+export const GaslessSwitchRow: FC = ({
+ checked,
+ onChange,
+ disabled,
+ tooltip,
+}) => {
const { t } = useTranslation();
return (
@@ -20,8 +39,16 @@ export const GaslessSwitchRow = () => {
{t('Gas fees paid by Core')}
-
-
+
+ {/* Gotta wrap in div because the "disabled" prop is preventing a tooltip from popping up*/}
+
+
+
);
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx
index f2e70a59d..a82ba303d 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/hooks/useEvmTransactionFee/useEvmTransactionFee.tsx
@@ -1,17 +1,18 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { useConnectionContext } from '@core/ui';
import { ExtensionRequest, FeeRate } from '@core/types';
import { type UpdateActionTxDataHandler } from '@core/service-worker';
-import { calculateGasAndFees, isAvalancheNetwork } from '@core/common';
+import { calculateGasAndFees } from '@core/common';
import { useNativeToken } from '@/hooks/useNativeToken';
import { useUpdateAccountBalance } from '@/hooks/useUpdateAccountBalance';
import { useCurrentFeesForNetwork } from '@/hooks/useCurrentFeesForNetwork';
import { EvmFeePreset } from '../../types';
-import { getFeeInfo, hasEnoughForFee } from './lib';
+import { getFeeInfo, getInitialFeeRate, hasEnoughForFee } from './lib';
import { EvmTxSigningData, UseEvmTransactionFee } from './types';
+import { getDefaultFeePreset } from '@/utils/getDefaultFeePreset';
export const useEvmTransactionFee: UseEvmTransactionFee = ({
action,
@@ -28,7 +29,7 @@ export const useEvmTransactionFee: UseEvmTransactionFee = ({
const [customPreset, setCustomPreset] = useState(networkFee?.high);
const [feePreset, setFeePreset] = useState(
- isAvalancheNetwork(network) ? 'fast' : 'slow',
+ getDefaultFeePreset(network),
);
const fee = calculateGasAndFees({
@@ -79,30 +80,25 @@ export const useEvmTransactionFee: UseEvmTransactionFee = ({
setFeePreset(preset);
- switch (preset) {
- case 'slow':
- await updateFee(
- networkFee.low.maxFeePerGas,
- networkFee.low.maxPriorityFeePerGas,
- );
- break;
- case 'normal':
- await updateFee(
- networkFee.medium.maxFeePerGas,
- networkFee.medium.maxPriorityFeePerGas,
- );
- break;
- case 'fast':
- await updateFee(
- networkFee.high.maxFeePerGas,
- networkFee.high.maxPriorityFeePerGas,
- );
- break;
- }
+ await updateFee(
+ networkFee[preset].maxFeePerGas,
+ networkFee[preset].maxPriorityFeePerGas,
+ );
},
[networkFee, updateFee],
);
+ const initialFeeRate = useRef(
+ getInitialFeeRate(signingData),
+ );
+
+ useEffect(() => {
+ // If the dapp did not give us any fee rate, we must initialize it ourselves.
+ if (!initialFeeRate.current) {
+ choosePreset(getDefaultFeePreset(network));
+ }
+ }, [networkFee, choosePreset, network]);
+
if (!networkFee || !nativeToken || !signingData || !customPreset) {
return {
isLoading: true,
@@ -115,9 +111,9 @@ export const useEvmTransactionFee: UseEvmTransactionFee = ({
feeDecimals: networkFee.displayDecimals,
feePreset,
presets: {
- slow: networkFee.low,
- normal: networkFee.medium,
- fast: networkFee.high,
+ low: networkFee.low,
+ medium: networkFee.medium,
+ high: networkFee.high,
custom: customPreset,
},
gasLimit: Number(signingData.data.gasLimit ?? 0),
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts
index 867299942..4d5d13e84 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmNetworkFeeWidget/types.ts
@@ -1 +1,3 @@
-export type EvmFeePreset = 'slow' | 'normal' | 'fast' | 'custom';
+import { TransactionPriority } from '@core/types';
+
+export type EvmFeePreset = TransactionPriority | 'custom';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/EvmTokenApprovals.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/EvmTokenApprovals.tsx
new file mode 100644
index 000000000..826a0ff8b
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/EvmTokenApprovals.tsx
@@ -0,0 +1,66 @@
+import { Stack } from '@avalabs/k2-alpine';
+import { FC, useRef } from 'react';
+import { DisplayData } from '@avalabs/vm-module-types';
+
+import { Action, EnsureDefined } from '@core/types';
+import { useBalancesContext } from '@core/ui';
+
+import { DetailsSection } from '../../generic/DetailsSection';
+import { getApprovalValue, isUnlimitedApproval } from './lib';
+import { CustomApprovalLimit, TokenSpendLimitCard } from './components';
+
+type EvmTokenApprovalsProps = {
+ action: Action>;
+};
+
+export const EvmTokenApprovals: FC = ({ action }) => {
+ const { getTokenPrice } = useBalancesContext();
+
+ const { tokenApprovals } = action.displayData;
+
+ const requestedApprovalRef = useRef(tokenApprovals.approvals[0]);
+ const hasOnlyOneEditableApproval =
+ tokenApprovals.approvals.length === 1 && tokenApprovals.isEditable;
+
+ const firstApproval = tokenApprovals.approvals[0];
+ const currentApprovalValue = firstApproval
+ ? getApprovalValue(firstApproval, getTokenPrice)
+ : null;
+ const requestedApproval = requestedApprovalRef.current;
+
+ if (!currentApprovalValue || !requestedApproval) {
+ return null;
+ }
+
+ const requestedApprovalValue = getApprovalValue(
+ requestedApproval,
+ getTokenPrice,
+ );
+
+ if (!requestedApprovalValue) {
+ return null;
+ }
+
+ return (
+
+
+ {tokenApprovals.approvals.map((approval) => (
+
+ ))}
+
+ {hasOnlyOneEditableApproval && firstApproval && requestedApproval && (
+
+ )}
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/AmountInput.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/AmountInput.tsx
new file mode 100644
index 000000000..78967758e
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/AmountInput.tsx
@@ -0,0 +1,19 @@
+import { styled } from '@avalabs/k2-alpine';
+
+export const AmountInput = styled('input')(({ theme }) => ({
+ background: 'transparent',
+ border: 0,
+ paddingInline: theme.spacing(0),
+ lineHeight: 1,
+ outline: 'none',
+ color: theme.palette.text.primary,
+ textAlign: 'end',
+ textOverflow: 'ellipsis',
+ '&::-webkit-outer-spin-button, &::-webkit-inner-spin-button': {
+ '-webkit-appearance': 'none',
+ margin: 0,
+ },
+ 'input[type=number]': {
+ '-moz-appearance': 'textfield',
+ },
+}));
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimit.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimit.tsx
new file mode 100644
index 000000000..5ce62aae8
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimit.tsx
@@ -0,0 +1,91 @@
+import { MaxUint256 } from 'ethers';
+import { useTranslation } from 'react-i18next';
+import { FC, useCallback, useState } from 'react';
+import { DisplayData, TokenApproval } from '@avalabs/vm-module-types';
+
+import { useConnectionContext } from '@core/ui';
+import type { UpdateActionTxDataHandler } from '@core/service-worker';
+import { Action, EnsureDefined, ExtensionRequest } from '@core/types';
+
+import { DetailsSection } from '../../../generic/DetailsSection';
+import { TxDetailsRow } from '../../../generic/DetailsItem/items/DetailRow';
+
+import { ApprovalValue, SpendLimit } from '../types';
+import { CustomLimitTrigger } from './CustomLimitTrigger';
+import { CustomApprovalLimitOverlay } from './CustomApprovalLimitOverlay';
+
+type CustomApprovalLimitProps = {
+ action: Action>;
+ approval: TokenApproval;
+ requestedValue: ApprovalValue;
+ approvalValue: ApprovalValue;
+ isUnlimitedRequested: boolean;
+};
+
+export const CustomApprovalLimit: FC = ({
+ action,
+ approval,
+ requestedValue,
+ approvalValue,
+ isUnlimitedRequested,
+}) => {
+ const { t } = useTranslation();
+ const { request } = useConnectionContext();
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ const { tokenValue } = approvalValue;
+
+ const [spendLimit, setSpendLimit] = useState({
+ type: isUnlimitedRequested ? 'unlimited' : 'requested',
+ value: tokenValue?.toSubUnit() ?? 0n,
+ });
+
+ const updateApprovalLimit = useCallback(() => {
+ if (!action.actionId) return;
+
+ const limitAmount =
+ spendLimit.type === 'unlimited' ? MaxUint256 : (spendLimit.value ?? 0n);
+
+ request({
+ method: ExtensionRequest.ACTION_UPDATE_TX_DATA,
+ params: [
+ action.actionId,
+ { approvalLimit: `0x${limitAmount.toString(16)}` },
+ ],
+ });
+ }, [request, action.actionId, spendLimit]);
+
+ const handleSave = () => {
+ setIsDialogOpen(false);
+ updateApprovalLimit();
+ };
+
+ if (!tokenValue) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ setIsDialogOpen(true)}
+ />
+
+ setIsDialogOpen(false)}
+ spendLimit={spendLimit}
+ setSpendLimit={setSpendLimit}
+ approval={approval}
+ requestedValue={requestedValue}
+ isUnlimitedRequested={isUnlimitedRequested}
+ onSave={handleSave}
+ />
+
+ >
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimitOverlay.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimitOverlay.tsx
new file mode 100644
index 000000000..8c8a3b54c
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomApprovalLimitOverlay.tsx
@@ -0,0 +1,198 @@
+import { Radio } from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+import { Dispatch, FC, SetStateAction } from 'react';
+import { TokenApproval, TokenType } from '@avalabs/vm-module-types';
+import { Button, Stack, Typography } from '@avalabs/k2-alpine';
+import { MaxUint256, parseUnits, formatUnits } from 'ethers';
+
+import { useKeyboardShortcuts } from '@core/ui';
+
+import { Page } from '@/components/Page';
+import { SlideUpDialog } from '@/components/Dialog';
+
+import { DetailsSection } from '../../../generic/DetailsSection';
+import { ApprovalValue, SpendLimit } from '../types';
+import { InfinitySymbol } from './InfinitySymbol';
+import { AmountInput } from './AmountInput';
+
+type CustomApprovalLimitOverlayProps = {
+ isDialogOpen: boolean;
+ onClose: () => void;
+ spendLimit: SpendLimit;
+ setSpendLimit: Dispatch>;
+ approval: TokenApproval;
+ isUnlimitedRequested: boolean;
+ requestedValue: ApprovalValue;
+ onSave: () => void;
+};
+
+export const CustomApprovalLimitOverlay: FC<
+ CustomApprovalLimitOverlayProps
+> = ({
+ isDialogOpen,
+ onClose,
+ spendLimit,
+ setSpendLimit,
+ approval,
+ isUnlimitedRequested,
+ requestedValue,
+ onSave,
+}) => {
+ const { t } = useTranslation();
+
+ const handleSave = () => {
+ onSave();
+ };
+
+ const keyboardShortcuts = useKeyboardShortcuts({
+ Enter: handleSave,
+ });
+
+ return (
+
+
+
+
+
+
+ setSpendLimit({
+ type: 'unlimited',
+ value: MaxUint256,
+ })
+ }
+ />
+ {t('Unlimited')}
+
+
+
+ {!isUnlimitedRequested && (
+
+
+
+ setSpendLimit({
+ type: 'requested',
+ value: requestedValue.tokenValue.toSubUnit(),
+ })
+ }
+ />
+ {t('Default')}
+
+
+ {requestedValue.tokenValue.toDisplay()} {approval.token.symbol}
+
+
+ )}
+
+
+
+ setSpendLimit((prev) => ({
+ type: 'custom',
+ value: prev.type !== 'unlimited' ? prev.value : 0n,
+ }))
+ }
+ />
+ {t('Custom')}
+
+ {
+ setSpendLimit({
+ type: 'custom',
+ value: parseUnits(
+ evt.currentTarget.value || '0',
+ approval.token.type === TokenType.ERC20
+ ? approval.token.decimals
+ : 1,
+ ),
+ });
+ }}
+ defaultValue={
+ spendLimit.type === 'unlimited'
+ ? '0'
+ : formatUnits(
+ spendLimit.value ?? 0n,
+ approval.token.type === TokenType.ERC20
+ ? approval.token.decimals
+ : 1,
+ )
+ }
+ onChange={(evt) => {
+ setSpendLimit({
+ type: 'custom',
+ value: parseUnits(
+ evt.currentTarget.value || '0',
+ approval.token.type === TokenType.ERC20
+ ? approval.token.decimals
+ : 1,
+ ),
+ });
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx
new file mode 100644
index 000000000..221b76b18
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/CustomLimitTrigger.tsx
@@ -0,0 +1,84 @@
+import {
+ ChevronRightIcon,
+ Stack,
+ StackProps,
+ Typography,
+} from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+import { TokenApproval } from '@avalabs/vm-module-types';
+
+import { useSettingsContext } from '@core/ui';
+
+import { CollapsedTokenAmount } from '@/components/CollapsedTokenAmount';
+
+import { ApprovalValue, SpendLimit } from '../types';
+import { InfinitySymbol } from './InfinitySymbol';
+
+type CustomLimitTriggerProps = StackProps & {
+ spendLimit: SpendLimit;
+ approval: TokenApproval;
+ approvalValue: ApprovalValue;
+};
+
+export const CustomLimitTrigger = ({
+ spendLimit,
+ approval,
+ approvalValue,
+ ...props
+}: CustomLimitTriggerProps) => {
+ const { t } = useTranslation();
+ const { currencyFormatter, currency } = useSettingsContext();
+
+ const { tokenValue, currencyValue, isUnlimited } = approvalValue;
+
+ return (
+
+
+
+ {spendLimit.type === 'unlimited' ? (
+
+ ) : (
+
+ )}
+
+ {approval.token.symbol}
+
+
+ {isUnlimited ? (
+
+ {t('Unlimited {{currency}}', {
+ currency,
+ })}
+
+ ) : (
+ currencyValue && (
+
+ {currencyFormatter(Number(currencyValue || '0'))}
+
+ )
+ )}
+
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx
new file mode 100644
index 000000000..6e9768b16
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/InfinitySymbol.tsx
@@ -0,0 +1,33 @@
+import { Typography, TypographyProps } from '@avalabs/k2-alpine';
+
+type InfinitySymbolProps = TypographyProps & {
+ symbolSize: 'small' | 'large';
+};
+
+const PROPS_BY_SIZE: Record<
+ InfinitySymbolProps['symbolSize'],
+ TypographyProps
+> = {
+ small: {
+ variant: 'h5',
+ fontWeight: 400,
+ },
+ large: {
+ variant: 'h2',
+ fontWeight: 400,
+ fontSize: 64,
+ lineHeight: '20px',
+ height: 36,
+ },
+};
+
+export const InfinitySymbol = ({
+ symbolSize,
+ ...props
+}: InfinitySymbolProps) => {
+ return (
+
+ ∞
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx
new file mode 100644
index 000000000..503c6a092
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/TokenSpendLimitCard.tsx
@@ -0,0 +1,148 @@
+import { FC } from 'react';
+import {
+ Avatar,
+ AvatarProps,
+ Stack,
+ styled,
+ Typography,
+} from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+import { TokenApproval, TokenType } from '@avalabs/vm-module-types';
+
+import { useSettingsContext } from '@core/ui';
+
+import { CollapsedTokenAmount } from '@/components/CollapsedTokenAmount';
+
+import { ApprovalValue } from '../types';
+import { InfinitySymbol } from './InfinitySymbol';
+import { TxDetailsRow } from '../../../generic/DetailsItem/items/DetailRow';
+
+type TokenSpendLimitCardProps = {
+ approval: TokenApproval;
+ approvalValue: ApprovalValue;
+};
+
+export const TokenSpendLimitCard: FC = ({
+ approval,
+ approvalValue,
+}) => {
+ return approval.token.type !== TokenType.ERC20 ? (
+
+ ) : (
+
+ );
+};
+
+const NFTApprovalHeader = ({
+ approval,
+ approvalValue,
+}: {
+ approval: TokenApproval;
+ approvalValue: ApprovalValue;
+}) => {
+ const { t } = useTranslation();
+ const { currencyFormatter } = useSettingsContext();
+
+ const { isUnlimited, tokenValue, currencyValue } = approvalValue;
+ const { logoUri } = approval;
+
+ if (!isUnlimited) {
+ return (
+
+
+
+
+
+ {approval.token.symbol}
+
+
+ {currencyValue && (
+
+ {currencyFormatter(Number(currencyValue || '0'))}
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+
+ {approval.token.name}
+
+ {t('Approves all {{token}}', { token: approval.token.name || '' })}
+
+
+ );
+};
+
+const ERC20ApprovalHeader = ({
+ approval,
+ approvalValue,
+}: {
+ approval: TokenApproval;
+ approvalValue: ApprovalValue;
+}) => {
+ const { t } = useTranslation();
+ const { currencyFormatter, currency } = useSettingsContext();
+
+ const { tokenValue, isUnlimited, currencyValue } = approvalValue;
+
+ return (
+
+
+ {isUnlimited ? (
+
+ ) : (
+
+ )}
+ {approval.token.symbol}
+
+ {isUnlimited ? (
+
+ {t('Unlimited {{currency}}', { currency })}
+
+ ) : (
+ currencyValue && (
+
+ {currencyFormatter(Number(currencyValue || '0'))}
+
+ )
+ )}
+
+ );
+};
+
+type SizedAvatarProps = AvatarProps & {
+ size: number;
+};
+const SizedAvatar = styled(Avatar)(({ size }) => ({
+ width: size,
+ height: size,
+ backgroundColor: 'transparent',
+}));
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts
new file mode 100644
index 000000000..39c2326cd
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/components/index.ts
@@ -0,0 +1,4 @@
+export * from './CustomApprovalLimit';
+export * from './InfinitySymbol';
+export * from './AmountInput';
+export * from './TokenSpendLimitCard';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts
new file mode 100644
index 000000000..0022a7de0
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/index.ts
@@ -0,0 +1 @@
+export * from './EvmTokenApprovals';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts
new file mode 100644
index 000000000..d5c13cc05
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/getApprovalValue.ts
@@ -0,0 +1,36 @@
+import { MaxUint256 } from 'ethers';
+import { TokenUnit } from '@avalabs/core-utils-sdk';
+import { TokenApproval, TokenType } from '@avalabs/vm-module-types';
+
+export const getApprovalValue = (
+ approval: TokenApproval,
+ getTokenPrice: (symbolOrAddress: string) => number | undefined,
+) => {
+ if (!approval.value) {
+ return null;
+ }
+
+ const isNFT =
+ approval.token.type === TokenType.ERC721 ||
+ approval.token.type === TokenType.ERC1155;
+ const tokenAmount = new TokenUnit(
+ typeof approval.value === 'string'
+ ? BigInt(approval.value)
+ : (approval.value ?? 0n),
+ approval.token.type !== TokenType.ERC20 ? 0 : approval.token.decimals,
+ '',
+ );
+ const tokenPrice = getTokenPrice(approval.token.address);
+ const isUnlimited = tokenAmount.toSubUnit() === MaxUint256;
+
+ return {
+ isNFT,
+ isUnlimited,
+ tokenValue: tokenAmount,
+ logoUri: approval.token.logoUri,
+ currencyValue:
+ typeof tokenPrice === 'number' && Number.isFinite(tokenPrice)
+ ? tokenAmount.toDisplay({ asNumber: true }) * tokenPrice
+ : undefined,
+ };
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts
new file mode 100644
index 000000000..d98f2850a
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/index.ts
@@ -0,0 +1,2 @@
+export * from './getApprovalValue';
+export * from './isUnlimitedApproval';
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts
new file mode 100644
index 000000000..32ee00054
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/lib/isUnlimitedApproval.ts
@@ -0,0 +1,8 @@
+import { MaxUint256 } from 'ethers';
+import { TokenApproval } from '@avalabs/vm-module-types';
+
+const MAX_UINT256_HEX = `0x${MaxUint256.toString(16)}`;
+
+export const isUnlimitedApproval = (approval: TokenApproval) => {
+ return approval.value === MAX_UINT256_HEX;
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts
new file mode 100644
index 000000000..08b7da559
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/evm/EvmTokenApprovals/types.ts
@@ -0,0 +1,10 @@
+import { getApprovalValue } from './lib';
+
+export type LimitType = 'requested' | 'unlimited' | 'custom';
+export type SpendLimit = {
+ type: LimitType;
+ value?: bigint;
+};
+
+export type NullableApprovalValue = ReturnType;
+export type ApprovalValue = NonNullable;
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx
index 2360fad30..cb9b85560 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/DetailsItem.tsx
@@ -6,6 +6,8 @@ import { AddressDetail } from './items/AddressDetail';
import { LinkDetail } from './items/LinkDetail';
import { NetworkDetail } from './items/NetworkDetail';
import { RawDataDetail } from './items/RawDataDetails/RawDataDetail';
+import { FundsRecipientDetail } from './items/FundsRecipientDetail';
+import { CurrencyDetail } from './items/CurrencyDetail';
type DetailsItemProps = {
item: DetailItem;
@@ -31,5 +33,11 @@ export const DetailsItem = ({ item }: DetailsItemProps) => {
case DetailItemType.DATA:
return ;
+
+ case DetailItemType.FUNDS_RECIPIENT:
+ return ;
+
+ case DetailItemType.CURRENCY:
+ return ;
}
};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx
new file mode 100644
index 000000000..12d97c1bc
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/CurrencyDetail.tsx
@@ -0,0 +1,36 @@
+import { CurrencyItem } from '@avalabs/vm-module-types';
+import { Stack, Typography } from '@avalabs/k2-alpine';
+
+import { TokenUnit } from '@avalabs/core-utils-sdk';
+import { useBalancesContext, useSettingsContext } from '@core/ui';
+
+import { TxDetailsRow } from './DetailRow';
+
+type CurrencyDetailProps = {
+ item: CurrencyItem;
+ customLabel?: React.ReactNode;
+};
+
+export const CurrencyDetail = ({ item, customLabel }: CurrencyDetailProps) => {
+ const { getTokenPrice } = useBalancesContext();
+ const { currencyFormatter } = useSettingsContext();
+ const { label, symbol, maxDecimals, value } = item;
+
+ const token = new TokenUnit(value, maxDecimals, symbol);
+ const price = getTokenPrice(symbol);
+
+ return (
+
+
+
+ {token.toDisplay()} {symbol}
+
+ {price && (
+
+ {currencyFormatter(price * token.toDisplay({ asNumber: true }))}
+
+ )}
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx
index 671f9ea5f..03a5fa156 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/DetailRow.tsx
@@ -8,7 +8,7 @@ import {
import { MdInfoOutline } from 'react-icons/md';
type TxDetailsRowProps = StackProps & {
- label: string;
+ label: React.ReactNode | string;
tooltip?: string;
};
@@ -29,9 +29,13 @@ export const TxDetailsRow = ({
{...rest}
>
-
- {label}
-
+ {typeof label === 'string' ? (
+
+ {label}
+
+ ) : (
+ label
+ )}
{tooltip && (
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx
new file mode 100644
index 000000000..4b687763b
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsItem/items/FundsRecipientDetail.tsx
@@ -0,0 +1,31 @@
+import { DetailItemType, FundsRecipientItem } from '@avalabs/vm-module-types';
+import { Tooltip, truncateAddress, Typography } from '@avalabs/k2-alpine';
+
+import { CurrencyDetail } from './CurrencyDetail';
+
+type FundsRecipientDetailProps = {
+ item: FundsRecipientItem;
+};
+
+export const FundsRecipientDetail = ({ item }: FundsRecipientDetailProps) => {
+ const { label: address, amount, symbol, maxDecimals } = item;
+
+ return (
+
+
+ {truncateAddress(address, 10)}
+
+
+ }
+ item={{
+ label: address,
+ value: amount,
+ symbol,
+ maxDecimals,
+ type: DetailItemType.CURRENCY,
+ }}
+ />
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx
index b09158609..4144d30bc 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/DetailsSection.tsx
@@ -2,16 +2,28 @@ import {
CardProps,
combineSx,
Divider,
+ DividerProps,
Stack,
styled,
} from '@avalabs/k2-alpine';
import { Card } from '@/components/Card';
-export const DetailsSection = ({ children, sx, ...props }: CardProps) => {
+type DetailsSectionProps = CardProps & {
+ dimmedDivider?: boolean;
+};
+
+export const DetailsSection = ({
+ children,
+ sx,
+ dimmedDivider = false,
+ ...props
+}: DetailsSectionProps) => {
return (
-
- }>{children}
+
+ }>
+ {children}
+
);
};
@@ -20,9 +32,15 @@ export const DetailsSection = ({ children, sx, ...props }: CardProps) => {
* Some children might be null, which breaks 's "divider" prop.
* We need to hide the dividers if they're either first, last or next to another.
*/
-const StyledDivider = styled(Divider)(({ theme }) => ({
+type StyledDividerProps = DividerProps & {
+ dimmed?: boolean;
+};
+const StyledDivider = styled(Divider, {
+ shouldForwardProp: (prop) => prop !== 'dimmed',
+})(({ theme, dimmed }) => ({
marginInline: theme.spacing(2),
'&:last-child, &:first-child, &+&': {
display: 'none',
},
+ opacity: dimmed ? 0.5 : 1,
}));
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx
index 8b0574378..bd529e4d0 100644
--- a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/TransactionBalanceChange.tsx
@@ -10,6 +10,8 @@ import {
BatchTokenBalanceChange,
SingleTokenBalanceChange,
} from './components';
+import { SimulationAlertBox } from './components/SimulationAlertBox';
+import { useTranslation } from 'react-i18next';
type TransactionBalanceChangeProps = BalanceChange & {
isSimulationSuccessful?: boolean;
@@ -18,51 +20,85 @@ type TransactionBalanceChangeProps = BalanceChange & {
export const TransactionBalanceChange: FC = ({
ins,
outs,
+ isSimulationSuccessful,
}) => {
- // TODO: Add the warnings below -- to be defined by the UX team.
- // const hasSentItems = outs.length > 0;
- // const hasReceivedItems = ins.length > 0;
- // const showNoPreExecWarning = isSimulationSuccessful === false; // may be undefined
- // const showNoDataWarning =
- // !hasSentItems && !hasReceivedItems && !isSimulationSuccessful;
+ const { t } = useTranslation();
+
+ const hasSentItems = outs.filter(({ items }) => items.length > 0).length > 0;
+ const hasReceivedItems =
+ ins.filter(({ items }) => items.length > 0).length > 0;
+ const hasSomeBalanceChangeInfo = hasSentItems || hasReceivedItems;
+ const showNoPreExecWarning = isSimulationSuccessful === false; // may be undefined
+ const showNoDataWarning =
+ !hasSomeBalanceChangeInfo && !isSimulationSuccessful;
return (
-
- {outs.map(({ token, items }) =>
- items.length === 1 ? (
-
- ) : (
-
- ),
+ <>
+ {hasSomeBalanceChangeInfo && (
+
+ {outs.map(({ token, items }) =>
+ items.length <= 1 ? (
+
+ ) : (
+
+ ),
+ )}
+ {ins
+ .filter(({ items }) => items.length > 0)
+ .map(({ token, items }) =>
+ items.length <= 1 ? (
+
+ ) : (
+
+ ),
+ )}
+
+ )}
+ {showNoPreExecWarning && (
+
)}
- {ins.map(({ token, items }) =>
- items.length === 1 ? (
-
- ) : (
-
- ),
+ {!showNoPreExecWarning && showNoDataWarning && (
+
)}
-
+ >
);
};
diff --git a/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx
new file mode 100644
index 000000000..86ee2d1e2
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/ActionDetails/generic/TransactionBalanceChange/components/SimulationAlertBox.tsx
@@ -0,0 +1,32 @@
+import { Box, Stack, StackProps, Typography } from '@avalabs/k2-alpine';
+import { FiAlertCircle } from 'react-icons/fi';
+
+type SimulationAlertBoxProps = StackProps & {
+ textLines: string[];
+};
+
+export const SimulationAlertBox = ({
+ textLines,
+ ...stackProps
+}: SimulationAlertBoxProps) => {
+ return (
+
+
+
+
+
+ {textLines.map((text) => (
+
+ {text}
+
+ ))}
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/ActionDrawer.tsx b/apps/next/src/pages/Approve/components/ActionDrawer.tsx
index 3c6ab5854..d67fc1c49 100644
--- a/apps/next/src/pages/Approve/components/ActionDrawer.tsx
+++ b/apps/next/src/pages/Approve/components/ActionDrawer.tsx
@@ -6,10 +6,15 @@ import {
StackProps,
styled,
} from '@avalabs/k2-alpine';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DisplayData } from '@avalabs/vm-module-types';
-import { Action, ActionStatus } from '@core/types';
+import { Action, ActionStatus, GaslessPhase } from '@core/types';
+
+import { hasOverlayWarning } from '../lib';
+import { InDrawerAlert } from './warnings/InDrawerAlert';
+import { useGasless } from '../hooks';
type ActionDrawerProps = StackProps & {
open: boolean;
@@ -27,39 +32,55 @@ export const ActionDrawer = ({
}: ActionDrawerProps) => {
const { t } = useTranslation();
+ const { gaslessPhase } = useGasless({ action });
+
+ const isMalicious = hasOverlayWarning(action);
+ const [userHasConfirmed, setUserHasConfirmed] = useState(false);
+
+ const isProcessing =
+ action.status === ActionStatus.SUBMITTING ||
+ gaslessPhase === GaslessPhase.FUNDING_IN_PROGRESS;
+
return (
- {approve && (
-
- )}
- {reject && (
-
+ {isMalicious && (
+
)}
+
+ {approve && (
+
+ )}
+ {reject && (
+
+ )}
+
);
};
-const Drawer = styled(Stack)(({ theme }) => ({
+export const Drawer = styled(Stack)(({ theme }) => ({
width: '100%',
position: 'sticky',
bottom: 0,
diff --git a/apps/next/src/pages/Approve/components/index.ts b/apps/next/src/pages/Approve/components/index.ts
index a2b347deb..0c0e57cad 100644
--- a/apps/next/src/pages/Approve/components/index.ts
+++ b/apps/next/src/pages/Approve/components/index.ts
@@ -4,3 +4,4 @@ export * from './ApprovalScreenTitle';
export * from './LoadingScreen';
export * from './UnsupportedNetworkScreen';
export * as Styled from './Styled';
+export * from './warnings';
diff --git a/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx b/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx
new file mode 100644
index 000000000..f7a95b013
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/warnings/InDrawerAlert.tsx
@@ -0,0 +1,60 @@
+import {
+ Box,
+ Stack,
+ styled,
+ Typography,
+ useTheme,
+ Switch,
+} from '@avalabs/k2-alpine';
+import { MdOutlineRemoveModerator } from 'react-icons/md';
+import { Trans } from 'react-i18next';
+import { Dispatch, FC, SetStateAction } from 'react';
+
+type InDrawerAlertProps = {
+ isConfirmed: boolean;
+ setIsConfirmed: Dispatch>;
+};
+
+export const InDrawerAlert: FC = ({
+ isConfirmed,
+ setIsConfirmed,
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+ ,
+ }}
+ />
+
+
+
+ setIsConfirmed(!isConfirmed)}
+ />
+
+
+ );
+};
+
+const Wrapper = styled(Stack)(({ theme }) => ({
+ flexDirection: 'row',
+ paddingInline: theme.spacing(2.5),
+ paddingTop: theme.spacing(2),
+ paddingBottom: theme.spacing(1),
+ marginInline: theme.spacing(-2),
+ gap: theme.spacing(4),
+ borderTop: `1px solid ${theme.palette.divider}`,
+ background:
+ theme.palette.mode === 'light'
+ ? theme.palette.common.white
+ : theme.palette.alphaMatch.backdropSolid,
+}));
diff --git a/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx b/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx
new file mode 100644
index 000000000..aa70929aa
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/warnings/MaliciousTxOverlay.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Alert } from '@avalabs/vm-module-types';
+import { MdOutlineRemoveModerator } from 'react-icons/md';
+import { Stack, Box, Typography, Button } from '@avalabs/k2-alpine';
+
+import { SlideUpDialog } from '@/components/Dialog';
+
+import { Drawer } from '../ActionDrawer';
+
+type MaliciousTxOverlayProps = {
+ open: boolean;
+ cancelHandler: () => void;
+ alert: Alert;
+};
+
+export const MaliciousTxOverlay = ({
+ open,
+ cancelHandler,
+ alert,
+}: MaliciousTxOverlayProps) => {
+ const { t } = useTranslation();
+ const [isAlertDialogOpen, setIsAlertDialogOpen] = useState(open);
+
+ return (
+ setIsAlertDialogOpen(false)}
+ >
+
+
+
+
+
+ {alert.details.title}
+
+
+ {alert.details.description}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx b/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx
new file mode 100644
index 000000000..49e40ef04
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/warnings/NoteWarning.tsx
@@ -0,0 +1,27 @@
+import { FC } from 'react';
+import { Alert } from '@avalabs/vm-module-types';
+import { FiAlertCircle } from 'react-icons/fi';
+import { Stack, Box, Typography } from '@avalabs/k2-alpine';
+
+type NoteWarningProps = {
+ alert: Alert;
+};
+
+export const NoteWarning: FC = ({ alert }) => (
+
+
+
+
+
+ {alert.details.title}. {alert.details.description}
+
+
+);
diff --git a/apps/next/src/pages/Approve/components/warnings/index.ts b/apps/next/src/pages/Approve/components/warnings/index.ts
new file mode 100644
index 000000000..7c550e096
--- /dev/null
+++ b/apps/next/src/pages/Approve/components/warnings/index.ts
@@ -0,0 +1,3 @@
+export * from './MaliciousTxOverlay';
+export * from './InDrawerAlert';
+export * from './NoteWarning';
diff --git a/apps/next/src/pages/Approve/hooks/index.ts b/apps/next/src/pages/Approve/hooks/index.ts
new file mode 100644
index 000000000..231bb3ad0
--- /dev/null
+++ b/apps/next/src/pages/Approve/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useGasless';
diff --git a/apps/next/src/pages/Approve/hooks/types.ts b/apps/next/src/pages/Approve/hooks/types.ts
new file mode 100644
index 000000000..8834f6ff1
--- /dev/null
+++ b/apps/next/src/pages/Approve/hooks/types.ts
@@ -0,0 +1,30 @@
+import { DisplayData } from '@avalabs/vm-module-types';
+import { Action } from '@core/types';
+import { useNetworkFeeContext } from '@core/ui';
+
+export type UseGaslessArgs = {
+ action: Action;
+};
+
+export type UseGaslessReturn = Pick<
+ ReturnType,
+ | 'fetchAndSolveGaslessChallange'
+ | 'gaslessFundTx'
+ | 'isGaslessOn'
+ | 'setIsGaslessOn'
+ | 'fundTxHex'
+ | 'setGaslessDefaultValues'
+ | 'gaslessPhase'
+ | 'setGaslessEligibility'
+ | 'isGaslessEligible'
+> & {
+ tryFunding: (approveCallback: () => void) => Promise;
+};
+
+export type UseGasless = (args: UseGaslessArgs) => UseGaslessReturn;
+
+export type GaslessEligibilityParams = [
+ chainId: number,
+ fromAddress: string | undefined,
+ nonce: number | undefined,
+];
diff --git a/apps/next/src/pages/Approve/hooks/useGasless.ts b/apps/next/src/pages/Approve/hooks/useGasless.ts
new file mode 100644
index 000000000..7ea23acce
--- /dev/null
+++ b/apps/next/src/pages/Approve/hooks/useGasless.ts
@@ -0,0 +1,118 @@
+import { DisplayData, RpcMethod } from '@avalabs/vm-module-types';
+import { caipToChainId } from '@core/common';
+import { Action, GaslessPhase } from '@core/types';
+import { useAnalyticsContext, useNetworkFeeContext } from '@core/ui';
+import { GaslessEligibilityParams, UseGasless } from './types';
+import { isUndefined } from 'lodash';
+import { useCallback, useEffect, useMemo } from 'react';
+import { toast } from '@avalabs/k2-alpine';
+import { useTranslation } from 'react-i18next';
+
+export const useGasless: UseGasless = ({ action }) => {
+ const { t } = useTranslation();
+ const {
+ isGaslessOn,
+ setIsGaslessOn,
+ gaslessFundTx,
+ fundTxHex,
+ setGaslessDefaultValues,
+ gaslessPhase,
+ setGaslessEligibility,
+ fetchAndSolveGaslessChallange,
+ isGaslessEligible,
+ } = useNetworkFeeContext();
+ const { captureEncrypted } = useAnalyticsContext();
+
+ const eligibilityParams = useMemo(
+ () => getEligibilityParams(action),
+ [action],
+ );
+
+ // First check if the action is elligible for gasless
+ useEffect(() => {
+ if (eligibilityParams) {
+ setGaslessEligibility(...eligibilityParams);
+ }
+ }, [eligibilityParams, setGaslessEligibility]);
+
+ // If we're eligible, fetch the gasless challenge
+ useEffect(() => {
+ if (isGaslessEligible && gaslessPhase === GaslessPhase.NOT_READY) {
+ fetchAndSolveGaslessChallange();
+ }
+ }, [isGaslessEligible, fetchAndSolveGaslessChallange, gaslessPhase]);
+
+ // Capture analytics events
+ useEffect(() => {
+ if (gaslessPhase === GaslessPhase.ERROR) {
+ captureEncrypted('GaslessFundFailed');
+ }
+ if (gaslessPhase === GaslessPhase.FUNDED && fundTxHex) {
+ captureEncrypted('GaslessFundSuccessful', {
+ fundTxHex,
+ });
+ }
+ }, [captureEncrypted, fundTxHex, gaslessPhase, setGaslessDefaultValues]);
+
+ // Wrapper around the approval screen's approve callback so we don't pollute it with gasless funding logic
+ const tryFunding = useCallback(
+ async (approveCallback: () => void) => {
+ if (isGaslessOn && isGaslessEligible) {
+ try {
+ await gaslessFundTx(action?.signingData);
+ } catch {
+ toast.error(t('Gasless funding failed'));
+ // Do not auto-submit if user wanted to fund the transaction, but it failed
+ return;
+ }
+ }
+ // Submit the transaction
+ approveCallback();
+ // Clear the gasless state
+ setGaslessDefaultValues();
+ },
+ [
+ isGaslessOn,
+ isGaslessEligible,
+ gaslessFundTx,
+ action?.signingData,
+ t,
+ setGaslessDefaultValues,
+ ],
+ );
+
+ return {
+ isGaslessOn,
+ setIsGaslessOn,
+ gaslessFundTx,
+ fundTxHex,
+ setGaslessDefaultValues,
+ gaslessPhase,
+ setGaslessEligibility,
+ fetchAndSolveGaslessChallange,
+ isGaslessEligible,
+ tryFunding,
+ };
+};
+
+const getEligibilityParams = (
+ action: Action,
+): GaslessEligibilityParams | null => {
+ if (!action) return null;
+
+ const { signingData } = action;
+ const evmChainId = caipToChainId(action.scope);
+
+ if (signingData?.type === RpcMethod.ETH_SEND_TRANSACTION) {
+ const fromAddress = isUndefined(signingData?.data.from)
+ ? undefined
+ : String(signingData?.data.from);
+ const nonce = isUndefined(signingData?.data.nonce)
+ ? undefined
+ : Number(signingData?.data.nonce);
+
+ return [evmChainId, fromAddress, nonce];
+ }
+
+ return [evmChainId, undefined, undefined];
+};
diff --git a/apps/next/src/pages/Approve/lib/hasNoteWarning.ts b/apps/next/src/pages/Approve/lib/hasNoteWarning.ts
new file mode 100644
index 000000000..d4d4d31a2
--- /dev/null
+++ b/apps/next/src/pages/Approve/lib/hasNoteWarning.ts
@@ -0,0 +1,7 @@
+import { Action, EnsureDefined } from '@core/types';
+import { AlertType, DisplayData } from '@avalabs/vm-module-types';
+
+export const hasNoteWarning = (
+ action: Action,
+): action is Action> =>
+ action.displayData.alert?.type === AlertType.WARNING;
diff --git a/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts b/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts
new file mode 100644
index 000000000..11025596f
--- /dev/null
+++ b/apps/next/src/pages/Approve/lib/hasOverlayWarning.ts
@@ -0,0 +1,7 @@
+import { AlertType, DisplayData } from '@avalabs/vm-module-types';
+import { Action, EnsureDefined } from '@core/types';
+
+export const hasOverlayWarning = (
+ action: Action,
+): action is Action> =>
+ action.displayData.alert?.type === AlertType.DANGER;
diff --git a/apps/next/src/pages/Approve/lib/index.ts b/apps/next/src/pages/Approve/lib/index.ts
new file mode 100644
index 000000000..6b5a46cc2
--- /dev/null
+++ b/apps/next/src/pages/Approve/lib/index.ts
@@ -0,0 +1,2 @@
+export * from './hasNoteWarning';
+export * from './hasOverlayWarning';
diff --git a/apps/next/src/popup/app.tsx b/apps/next/src/popup/app.tsx
index 82f87b9e1..003caf3fd 100644
--- a/apps/next/src/popup/app.tsx
+++ b/apps/next/src/popup/app.tsx
@@ -43,6 +43,7 @@ const pagesWithoutHeader = [
'/receive',
'/approve',
'/permissions',
+ '/network/switch',
getContactsPath(),
getSendPath(),
];
diff --git a/apps/next/src/routing/ApprovalRoutes.tsx b/apps/next/src/routing/ApprovalRoutes.tsx
index b626bea65..fd7b9fe5d 100644
--- a/apps/next/src/routing/ApprovalRoutes.tsx
+++ b/apps/next/src/routing/ApprovalRoutes.tsx
@@ -4,6 +4,7 @@ import { Route, Switch, SwitchProps } from 'react-router-dom';
import { GenericApprovalScreen } from '@/pages/Approve/GenericApprovalScreen';
import { ApproveDappConnection } from '@/pages/Approve/ApproveDappConnection';
+import { ExtensionActionApprovalScreen } from '@/pages/Approve/ExtensionActionApprovalScreen';
export const ApprovalRoutes = (props: SwitchProps) => (
(
}
>
+
+
+
+ isAvalancheNetwork(network) ? DEFAULT_FEE_PRESET_C_CHAIN : DEFAULT_FEE_PRESET;
diff --git a/packages/inpage/package.json b/packages/inpage/package.json
index 38fdc4433..573f7118c 100644
--- a/packages/inpage/package.json
+++ b/packages/inpage/package.json
@@ -15,8 +15,8 @@
"typecheck": "yarn tsc --skipLibCheck --noEmit"
},
"dependencies": {
- "@avalabs/evm-module": "1.9.10",
- "@avalabs/svm-module": "1.9.10",
+ "@avalabs/evm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
+ "@avalabs/svm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@core/common": "workspace:*",
"@core/messaging": "workspace:*",
"@core/types": "workspace:*",
diff --git a/packages/service-worker/package.json b/packages/service-worker/package.json
index cf9f5ff3c..95a529ad0 100644
--- a/packages/service-worker/package.json
+++ b/packages/service-worker/package.json
@@ -16,9 +16,9 @@
"typecheck": "yarn tsc --skipLibCheck --noEmit"
},
"dependencies": {
- "@avalabs/avalanche-module": "1.9.10",
+ "@avalabs/avalanche-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/avalanchejs": "5.1.0-alpha.2",
- "@avalabs/bitcoin-module": "1.9.10",
+ "@avalabs/bitcoin-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/bridge-unified": "4.0.3",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.60",
"@avalabs/core-chains-sdk": "3.1.0-alpha.60",
@@ -30,13 +30,13 @@
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.60",
"@avalabs/core-utils-sdk": "3.1.0-alpha.60",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.60",
- "@avalabs/evm-module": "1.9.10",
+ "@avalabs/evm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/glacier-sdk": "3.1.0-alpha.60",
- "@avalabs/hvm-module": "1.9.10",
+ "@avalabs/hvm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/hw-app-avalanche": "0.14.1",
- "@avalabs/svm-module": "1.9.10",
+ "@avalabs/svm-module": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@avalabs/types": "3.1.0-alpha.60",
- "@avalabs/vm-module-types": "1.9.10",
+ "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@blockaid/client": "0.10.0",
"@coinbase/cbpay-js": "1.6.0",
"@cubist-labs/cubesigner-sdk": "0.3.28",
diff --git a/packages/types/src/network-fee.ts b/packages/types/src/network-fee.ts
index e390401a0..c18c106bb 100644
--- a/packages/types/src/network-fee.ts
+++ b/packages/types/src/network-fee.ts
@@ -1,3 +1,5 @@
+import { NetworkFees } from '@avalabs/vm-module-types';
+
export type FeeRate = {
maxFeePerGas: bigint;
maxPriorityFeePerGas?: bigint;
@@ -12,7 +14,10 @@ export interface NetworkFee {
isFixedFee: boolean;
}
-export type TransactionPriority = 'low' | 'medium' | 'high';
+export type TransactionPriority = Extract<
+ keyof NetworkFees,
+ 'low' | 'medium' | 'high'
+>;
export type SerializedNetworkFee = Omit<
NetworkFee,
diff --git a/packages/types/src/network.ts b/packages/types/src/network.ts
index f7c99b7ed..7384c4337 100644
--- a/packages/types/src/network.ts
+++ b/packages/types/src/network.ts
@@ -75,7 +75,12 @@ export type AddEthereumChainDisplayData = {
export const PLACEHOLDER_RPC_HEADERS = { '': '' };
export type EvmNetwork = NetworkWithCaipId & { vmName: NetworkVMType.EVM };
+export type BtcNetwork = NetworkWithCaipId & { vmName: NetworkVMType.BITCOIN };
export const isEvmNetwork = (
network: NetworkWithCaipId,
): network is EvmNetwork => network.vmName === NetworkVMType.EVM;
+
+export const isBtcNetwork = (
+ network: NetworkWithCaipId,
+): network is BtcNetwork => network.vmName === NetworkVMType.BITCOIN;
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 562443e23..8e00fba99 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -23,7 +23,7 @@
"@avalabs/glacier-sdk": "3.1.0-alpha.60",
"@avalabs/hw-app-avalanche": "0.14.1",
"@avalabs/types": "3.1.0-alpha.60",
- "@avalabs/vm-module-types": "1.9.10",
+ "@avalabs/vm-module-types": "0.0.0-fix-nft-set-approval-for-all-20250829175304",
"@blockaid/client": "0.10.0",
"@cubist-labs/cubesigner-sdk": "0.3.28",
"@ethereumjs/common": "2.6.5",
diff --git a/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx b/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx
index c9d9d74b0..e11337388 100644
--- a/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx
+++ b/packages/ui/src/contexts/BalancesProvider/BalancesProvider.tsx
@@ -345,10 +345,14 @@ export function BalancesProvider({ children }: PropsWithChildren) {
return;
}
+ const accountBalances =
+ balances.tokens?.[tokenNetwork.chainId]?.[addressForChain];
+
const token =
- balances.tokens?.[tokenNetwork.chainId]?.[addressForChain]?.[
- addressOrSymbol
- ];
+ accountBalances?.[addressOrSymbol] ??
+ // Also try lower-cased.
+ // Native token symbols are not lower-cased by the balance services.
+ accountBalances?.[addressOrSymbol.toLowerCase()];
return token?.priceInCurrency;
},
diff --git a/packages/ui/src/hooks/useApproveAction.ts b/packages/ui/src/hooks/useApproveAction.ts
index fadc2079b..65ec8c857 100644
--- a/packages/ui/src/hooks/useApproveAction.ts
+++ b/packages/ui/src/hooks/useApproveAction.ts
@@ -8,11 +8,13 @@ import {
isBatchApprovalAction,
ContextContainer,
} from '@core/types';
+import { filter } from 'rxjs';
import { GetActionHandler, UpdateActionHandler } from '@core/service-worker';
import { isSpecificContextContainer } from '../utils/isSpecificContextContainer';
import { useCallback, useEffect, useState } from 'react';
import { getUpdatedSigningData } from '@core/common';
import { useWindowGetsClosedOrHidden } from './useWindowGetsClosedOrHidden';
+import { isActionsUpdate } from '../contexts/ApprovalsProvider/isActionsUpdate';
type ActionType = IsBatchApproval extends true
? MultiTxAction
@@ -44,7 +46,7 @@ export function useApproveAction(
actionId: string,
isBatchApproval: boolean = false,
): HookResult | MultiTxAction | undefined> {
- const { request } = useConnectionContext();
+ const { request, events } = useConnectionContext();
const isConfirmPopup = isSpecificContextContainer(ContextContainer.CONFIRM);
const { approval } = useApprovalsContext();
const [action, setAction] = useState>();
@@ -125,6 +127,22 @@ export function useApproveAction(
}
}, [actionId, request, approval, isConfirmPopup, isBatchApproval]);
+ useEffect(() => {
+ const actionsUpdates = events()
+ .pipe(filter(isActionsUpdate))
+ .subscribe(async (event) => {
+ setAction((prev) => {
+ const actionFromEvent = event.value[actionId];
+
+ return actionFromEvent ?? prev;
+ });
+ });
+
+ return () => {
+ actionsUpdates.unsubscribe();
+ };
+ });
+
useWindowGetsClosedOrHidden(cancelHandler);
return { action, updateAction, error, cancelHandler };
diff --git a/yarn.lock b/yarn.lock
index d406c044d..b330495f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,9 +49,9 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/avalanche-module@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/avalanche-module@npm:1.9.10"
+"@avalabs/avalanche-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/avalanche-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/avalanchejs": "npm:5.1.0-alpha.2"
"@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58"
@@ -61,12 +61,12 @@ __metadata:
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58"
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.58"
"@avalabs/types": "npm:3.1.0-alpha.58"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@metamask/rpc-errors": "npm:6.3.0"
big.js: "npm:6.2.1"
bn.js: "npm:5.2.1"
zod: "npm:3.23.8"
- checksum: 10c0/b63fe029070a1070422cb07ab5f3480614d98b43aaa07617cf8d3d9821a56b735a43c1f9d2ec33b21d0fdb42cf66772c127015788bf2a09ad03aa1897e22abd7
+ checksum: 10c0/dbb35d0c3633f96aff180ab4ac656efb144c3a8691e78f204e235ef4c8ef31484b87b10885ce5b9ef73a2f48f5cc3ecec55e3731626d7fbf48363c0c4794f620
languageName: node
linkType: hard
@@ -96,20 +96,20 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/bitcoin-module@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/bitcoin-module@npm:1.9.10"
+"@avalabs/bitcoin-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/bitcoin-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@metamask/rpc-errors": "npm:6.3.0"
big.js: "npm:6.2.1"
bitcoinjs-lib: "npm:5.2.0"
bn.js: "npm:5.2.1"
zod: "npm:3.23.8"
- checksum: 10c0/2d1574869435495c951cbdf8f908373b1e7ffd79727de97e8994b3fe84feace0a896bb49db507db96f5d6276f989fb29a70140ec89e5da52a3d702dc6bdb1880
+ checksum: 10c0/52eaa0db84693b97d0ea470c256fa12ffc7b395ee309668cc26c9f1d37460374f3f89d854857422c63fcd5747d40fa24d423a82ea969e8f99f33723d4709229b
languageName: node
linkType: hard
@@ -367,9 +367,9 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/evm-module@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/evm-module@npm:1.9.10"
+"@avalabs/evm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/evm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58"
@@ -378,7 +378,7 @@ __metadata:
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58"
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.58"
"@avalabs/types": "npm:3.1.0-alpha.58"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@blockaid/client": "npm:0.36.0"
"@metamask/rpc-errors": "npm:6.3.0"
"@openzeppelin/contracts": "npm:4.9.6"
@@ -388,7 +388,7 @@ __metadata:
zod: "npm:3.23.8"
peerDependencies:
ethers: 6.13.5
- checksum: 10c0/630f485da7791aeb56227aec5db5928dae5a4dc28d0181663f8885dc21624999de2f8ade9d077f1166e5a79c9469a64b0533b3d69383a8015a8053bb353f496c
+ checksum: 10c0/e21b993efc1bff7b5817a78577a781905f8faf10557b885347bb84eee84affa0b1d58299e69da77f726963d3ee9a071fc051233df63c60c4b1398ac962d9b303
languageName: node
linkType: hard
@@ -406,18 +406,18 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/hvm-module@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/hvm-module@npm:1.9.10"
+"@avalabs/hvm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/hvm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@metamask/rpc-errors": "npm:6.3.0"
"@noble/hashes": "npm:1.5.0"
"@scure/base": "npm:1.2.4"
hypersdk-client: "npm:0.4.16"
zod: "npm:3.23.8"
- checksum: 10c0/49f2114d90f516fcf51c4e309fbb93f5f80d818a33d2888765d5e2855a9d8d92a929c074aa59d8f07159bd580d2ad5a5746ead0666f6d668213fc78ea8f9859d
+ checksum: 10c0/89716ab6c99083ce96b0cbd0fb0df1b24ca0069d609f6f1969eec6e8133b26e2dc39478ae3d687c129d8acc914f2d4eca9c09ef06b58d6c6b8ad40005c6247f1
languageName: node
linkType: hard
@@ -456,15 +456,15 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/svm-module@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/svm-module@npm:1.9.10"
+"@avalabs/svm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/svm-module@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/core-chains-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-coingecko-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-utils-sdk": "npm:3.1.0-alpha.58"
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@blockaid/client": "npm:0.48.0"
"@metamask/rpc-errors": "npm:6.3.0"
"@scure/base": "npm:1.2.4"
@@ -475,7 +475,7 @@ __metadata:
"@wallet-standard/base": "npm:1.1.0"
"@wallet-standard/features": "npm:1.1.0"
zod: "npm:3.23.8"
- checksum: 10c0/b52fd88b179f24467f9aaaefc41f70e2c27fc4c2eda0777a7e48bd0b9d10a5d096fac6119cda48580ea7496454417785f7ffb94e42646ada4ffaee1b644f2ca8
+ checksum: 10c0/f56b2cc05cf43befede0696fc7dd9816664139d492b45e63ede0f6b67b053d241b7c0c3f88521930e5545e30f794454581c35a2c193b2867e1acc50150324eb0
languageName: node
linkType: hard
@@ -493,9 +493,9 @@ __metadata:
languageName: node
linkType: hard
-"@avalabs/vm-module-types@npm:1.9.10":
- version: 1.9.10
- resolution: "@avalabs/vm-module-types@npm:1.9.10"
+"@avalabs/vm-module-types@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304":
+ version: 0.0.0-fix-nft-set-approval-for-all-20250829175304
+ resolution: "@avalabs/vm-module-types@npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
dependencies:
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.58"
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.58"
@@ -505,7 +505,7 @@ __metadata:
zod: "npm:3.23.8"
peerDependencies:
ethers: 6.13.5
- checksum: 10c0/d09fe3ee7cc61a1c43b456d31c4490f6fbe5b6b94c7a54a9d339ae99afdbf4c1cb3170716fafc9a619809ce8dd0a77028098c8c27e568afe2cc291b88d6a068c
+ checksum: 10c0/de1b338fd4a1bb55d43a8829a439a55c9430d05c7a8d906799a5fc95624593977cc40f03718156cd99af29a44bb2d4dab8138e97ad00563047125bd32dcf4ee3
languageName: node
linkType: hard
@@ -3781,9 +3781,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@core-ext/legacy@workspace:apps/legacy"
dependencies:
- "@avalabs/avalanche-module": "npm:1.9.10"
+ "@avalabs/avalanche-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/avalanchejs": "npm:5.1.0-alpha.2"
- "@avalabs/bitcoin-module": "npm:1.9.10"
+ "@avalabs/bitcoin-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/bridge-unified": "npm:4.0.3"
"@avalabs/core-bridge-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-chains-sdk": "npm:3.1.0-alpha.60"
@@ -3796,13 +3796,13 @@ __metadata:
"@avalabs/core-token-prices-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-utils-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60"
- "@avalabs/evm-module": "npm:1.9.10"
+ "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.60"
- "@avalabs/hvm-module": "npm:1.9.10"
+ "@avalabs/hvm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/hw-app-avalanche": "npm:0.14.1"
- "@avalabs/svm-module": "npm:1.9.10"
+ "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/types": "npm:3.1.0-alpha.60"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@babel/plugin-proposal-decorators": "npm:7.24.1"
"@babel/preset-env": "npm:7.24.4"
"@babel/preset-react": "npm:7.24.1"
@@ -3977,7 +3977,7 @@ __metadata:
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60"
"@avalabs/k2-alpine": "npm:1.228.0"
"@avalabs/types": "npm:3.1.0-alpha.60"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@babel/plugin-proposal-decorators": "npm:7.24.1"
"@babel/preset-env": "npm:7.24.4"
"@babel/preset-react": "npm:7.24.1"
@@ -4136,8 +4136,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@core/inpage@workspace:packages/inpage"
dependencies:
- "@avalabs/evm-module": "npm:1.9.10"
- "@avalabs/svm-module": "npm:1.9.10"
+ "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
+ "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@core/common": "workspace:*"
"@core/messaging": "workspace:*"
"@core/types": "workspace:*"
@@ -4220,9 +4220,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "@core/service-worker@workspace:packages/service-worker"
dependencies:
- "@avalabs/avalanche-module": "npm:1.9.10"
+ "@avalabs/avalanche-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/avalanchejs": "npm:5.1.0-alpha.2"
- "@avalabs/bitcoin-module": "npm:1.9.10"
+ "@avalabs/bitcoin-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/bridge-unified": "npm:4.0.3"
"@avalabs/core-bridge-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-chains-sdk": "npm:3.1.0-alpha.60"
@@ -4234,13 +4234,13 @@ __metadata:
"@avalabs/core-token-prices-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-utils-sdk": "npm:3.1.0-alpha.60"
"@avalabs/core-wallets-sdk": "npm:3.1.0-alpha.60"
- "@avalabs/evm-module": "npm:1.9.10"
+ "@avalabs/evm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.60"
- "@avalabs/hvm-module": "npm:1.9.10"
+ "@avalabs/hvm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/hw-app-avalanche": "npm:0.14.1"
- "@avalabs/svm-module": "npm:1.9.10"
+ "@avalabs/svm-module": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@avalabs/types": "npm:3.1.0-alpha.60"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@babel/plugin-proposal-decorators": "npm:7.24.1"
"@babel/preset-env": "npm:7.24.4"
"@babel/preset-typescript": "npm:7.24.1"
@@ -4402,7 +4402,7 @@ __metadata:
"@avalabs/glacier-sdk": "npm:3.1.0-alpha.60"
"@avalabs/hw-app-avalanche": "npm:0.14.1"
"@avalabs/types": "npm:3.1.0-alpha.60"
- "@avalabs/vm-module-types": "npm:1.9.10"
+ "@avalabs/vm-module-types": "npm:0.0.0-fix-nft-set-approval-for-all-20250829175304"
"@blockaid/client": "npm:0.10.0"
"@cubist-labs/cubesigner-sdk": "npm:0.3.28"
"@eslint/compat": "npm:1.2.4"