diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 977a65502..8af888826 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -55,7 +55,7 @@
"@rabby-wallet/eth-walletconnect-keyring": "2.1.3",
"@rabby-wallet/object-multiplex": "workspace:^",
"@rabby-wallet/persist-store": "workspace:^",
- "@rabby-wallet/rabby-api": "0.7.12",
+ "@rabby-wallet/rabby-api": "0.7.14",
"@rabby-wallet/rabby-security-engine": "^1.1.17",
"@rabby-wallet/rabby-swap": "0.0.36",
"@rabby-wallet/service-address": "workspace:^",
diff --git a/apps/mobile/src/assets/icons/sign/tx/bg.svg b/apps/mobile/src/assets/icons/sign/tx/bg.svg
new file mode 100644
index 000000000..342f7daed
--- /dev/null
+++ b/apps/mobile/src/assets/icons/sign/tx/bg.svg
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/apps/mobile/src/assets/icons/sign/tx/gas-dark.svg b/apps/mobile/src/assets/icons/sign/tx/gas-dark.svg
new file mode 100644
index 000000000..fd37eff34
--- /dev/null
+++ b/apps/mobile/src/assets/icons/sign/tx/gas-dark.svg
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/apps/mobile/src/assets/icons/sign/tx/gas-light.svg b/apps/mobile/src/assets/icons/sign/tx/gas-light.svg
new file mode 100644
index 000000000..7aedae140
--- /dev/null
+++ b/apps/mobile/src/assets/icons/sign/tx/gas-light.svg
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/apps/mobile/src/assets/icons/sign/tx/pay-for-gas.png b/apps/mobile/src/assets/icons/sign/tx/pay-for-gas.png
new file mode 100644
index 000000000..b54f91a76
Binary files /dev/null and b/apps/mobile/src/assets/icons/sign/tx/pay-for-gas.png differ
diff --git a/apps/mobile/src/assets/icons/sign/tx/rabby.svg b/apps/mobile/src/assets/icons/sign/tx/rabby.svg
new file mode 100644
index 000000000..21bfa4e1e
--- /dev/null
+++ b/apps/mobile/src/assets/icons/sign/tx/rabby.svg
@@ -0,0 +1,36 @@
+
\ No newline at end of file
diff --git a/apps/mobile/src/assets/locales/en/messages.json b/apps/mobile/src/assets/locales/en/messages.json
index 89ba1f70d..d3f045eac 100644
--- a/apps/mobile/src/assets/locales/en/messages.json
+++ b/apps/mobile/src/assets/locales/en/messages.json
@@ -34,7 +34,7 @@
"gasLimitNotEnough": "Gas limit is less than 21000. Transaction can't be submitted",
"gasLimitLessThanExpect": "Gas limit is low. There is 1% chance that the transaction may fail.",
"gasLimitLessThanGasUsed": "Gas limit is too low. There is 95% chance that the transaction may fail.",
- "nativeTokenNotEngouthForGas": "You do not have enough gas in your wallet",
+ "nativeTokenNotEngouthForGas": "Gas balance is not enough for transaction",
"nonceLowerThanExpect": "Nonce is too low, the minimum should be {{0}}",
"canOnlyUseImportedAddress": "You can only use imported addresses to sign",
"multiSigChainNotMatch": "Multi-signature addresses are not on this chain and cannot initiate transactions",
@@ -298,6 +298,12 @@
"ledgerConnected": "Ledger is connected",
"importedByLedger": "Imported by Ledger",
"signAndSubmitButton": "Sign and Create",
+ "gasless": {
+ "unavailable": "Gas balance is not enough for this transaction",
+ "notEnough": "Gas balance is not enough",
+ "GetFreeGasToSign": "Get Free Gas to sign",
+ "rabbyPayGas": "Rabby'll pay for the gas needed – just sign on"
+ },
"walletConnect": {
"connectedButCantSign": "Connected but unable to sign.",
"switchToCorrectAddress": "Please switch to the correct address in mobile wallet",
diff --git a/apps/mobile/src/components/Approval/components/FooterBar/ActionsContainer.tsx b/apps/mobile/src/components/Approval/components/FooterBar/ActionsContainer.tsx
index 5c19ba2cb..5a427c1c7 100644
--- a/apps/mobile/src/components/Approval/components/FooterBar/ActionsContainer.tsx
+++ b/apps/mobile/src/components/Approval/components/FooterBar/ActionsContainer.tsx
@@ -50,6 +50,7 @@ export interface Props {
children?: React.ReactNode;
chain?: Chain;
submitText?: string;
+ gasLess?: boolean;
}
export const ActionsContainer: React.FC<
diff --git a/apps/mobile/src/components/Approval/components/FooterBar/FooterBar.tsx b/apps/mobile/src/components/Approval/components/FooterBar/FooterBar.tsx
index a2de31a39..abd869789 100644
--- a/apps/mobile/src/components/Approval/components/FooterBar/FooterBar.tsx
+++ b/apps/mobile/src/components/Approval/components/FooterBar/FooterBar.tsx
@@ -19,6 +19,7 @@ import { useApprovalSecurityEngine } from '../../hooks/useApprovalSecurityEngine
import SecurityLevelTagNoText from '../SecurityEngine/SecurityLevelTagNoText';
import { AccountInfo } from './AccountInfo';
import { ActionGroup, Props as ActionGroupProps } from './ActionGroup';
+import { GasLessNotEnough, GasLessToSign } from './GasLessComponents';
interface Props extends Omit {
chain?: Chain;
@@ -31,6 +32,10 @@ interface Props extends Omit {
isTestnet?: boolean;
engineResults?: Result[];
onIgnoreAllRules(): void;
+ useGasLess?: boolean;
+ showGasLess?: boolean;
+ enableGasLess?: () => void;
+ canUseGasLess?: boolean;
}
const getStyles = (colors: AppColorsVariants) =>
@@ -159,7 +164,12 @@ export const FooterBar: React.FC = ({
engineResults = [],
hasUnProcessSecurityResult,
hasShadow = false,
+ showGasLess = false,
+ useGasLess = false,
+ canUseGasLess = false,
onIgnoreAllRules,
+ enableGasLess,
+
...props
}) => {
const [account, setAccount] = React.useState();
@@ -317,7 +327,13 @@ export const FooterBar: React.FC = ({
account={account}
isTestnet={props.isTestnet}
/>
-
+
{securityLevel && hasUnProcessSecurityResult && (
= ({
)}
+
+ {showGasLess &&
+ (!securityLevel || !hasUnProcessSecurityResult) &&
+ (canUseGasLess ? (
+ {
+ enableGasLess?.();
+ }}
+ />
+ ) : (
+
+ ))}
);
diff --git a/apps/mobile/src/components/Approval/components/FooterBar/GasLessComponents.tsx b/apps/mobile/src/components/Approval/components/FooterBar/GasLessComponents.tsx
new file mode 100644
index 000000000..c96c56667
--- /dev/null
+++ b/apps/mobile/src/components/Approval/components/FooterBar/GasLessComponents.tsx
@@ -0,0 +1,398 @@
+import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
+import { default as RcIconGasLight } from '@/assets/icons/sign/tx/gas-light.svg';
+import { default as RcIconGasDark } from '@/assets/icons/sign/tx/gas-dark.svg';
+
+import { useTranslation } from 'react-i18next';
+import { default as RcIconLogo } from '@/assets/icons/sign/tx/rabby.svg';
+
+import { createGetStyles } from '@/utils/styles';
+import { useThemeColors } from '@/hooks/theme';
+
+import {
+ View,
+ Text,
+ ImageBackground,
+ TouchableOpacity,
+ TextStyle,
+ ViewStyle,
+ DimensionValue,
+ StyleSheet,
+} from 'react-native';
+import { makeThemeIcon } from '@/hooks/makeThemeIcon';
+import LinearGradient from 'react-native-linear-gradient';
+import { StyleProp } from 'react-native';
+import Animated, {
+ Easing,
+ interpolate,
+ useAnimatedStyle,
+ useSharedValue,
+ withDelay,
+ withRepeat,
+ withSequence,
+ withTiming,
+} from 'react-native-reanimated';
+import { renderText } from '@/utils/renderNode';
+import { colord } from 'colord';
+
+const RcIconGas = makeThemeIcon(RcIconGasLight, RcIconGasDark);
+
+export function GasLessNotEnough() {
+ const { t } = useTranslation();
+ const colors = useThemeColors();
+ const styles = useMemo(() => getStyles(colors), [colors]);
+ return (
+
+
+
+
+ {t('page.signFooterBar.gasless.unavailable')}
+
+
+ );
+}
+
+function FreeGasReady() {
+ // const { t } = useTranslation();
+ // const colors = useThemeColors();
+ // const styles = useMemo(() => getStyles(colors), [colors]);
+
+ return (
+
+
+
+ );
+}
+
+export function GasLessToSign({
+ handleFreeGas,
+ gasLessEnable,
+}: {
+ handleFreeGas: () => void;
+ gasLessEnable: boolean;
+}) {
+ const { t } = useTranslation();
+ const colors = useThemeColors();
+ const styles = useMemo(() => getStyles(colors), [colors]);
+
+ const hiddenAnimated = useSharedValue(0);
+
+ const toSignStyle = useAnimatedStyle(() => ({
+ display: hiddenAnimated.value !== 1 ? 'flex' : 'none',
+ }));
+
+ const confirmedStyled = useAnimatedStyle(() => ({
+ display: hiddenAnimated.value === 1 ? 'flex' : 'none',
+ }));
+
+ const [animated, setAnimated] = useState(false);
+
+ const startAnimation = React.useCallback(() => {
+ setAnimated(true);
+ handleFreeGas();
+ hiddenAnimated.value = withDelay(
+ 900,
+ withTiming(1, {
+ duration: 0,
+ }),
+ );
+ }, [hiddenAnimated, handleFreeGas]);
+
+ if (gasLessEnable && !animated) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+
+
+ {t('page.signFooterBar.gasless.notEnough')}
+
+
+
+
+ {t('page.signFooterBar.gasless.GetFreeGasToSign')}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export const GasLessAnimatedWrapper = (
+ props: PropsWithChildren<{
+ gasLess?: boolean;
+ title: string;
+ titleStyle: StyleProp;
+ buttonStyle: StyleProp;
+ showOrigin: boolean;
+ type?: 'submit' | 'process';
+ }>,
+) => {
+ const colors = useThemeColors();
+
+ const blueBgXValue = useSharedValue(-200);
+
+ const logoXValue = useSharedValue(-10);
+
+ const logoYValue = useSharedValue(0);
+
+ const hiddenAnimated = useSharedValue(0);
+
+ const overlayStyle = useAnimatedStyle(
+ () => ({
+ position: 'absolute',
+ opacity: 0.5,
+ width: '100%',
+ height: '100%',
+ top: 0,
+ backgroundColor: colors['neutral-bg-1'],
+ left: (interpolate(logoXValue.value, [-10, 100], [0, 105]) +
+ '%') as DimensionValue, //(overlayValue.value + '%') as DimensionValue,
+ }),
+ [colors],
+ );
+
+ const logoStyle = useAnimatedStyle(() => ({
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: 16,
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: (logoXValue.value + '%') as DimensionValue,
+ transform: [{ translateY: logoYValue.value }],
+ }));
+
+ const blueBgStyle = useAnimatedStyle(() => ({
+ width: '200%',
+ height: '100%',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: colors['blue-default'],
+ left: (blueBgXValue.value + '%') as DimensionValue,
+ }));
+
+ const processBgColor = useMemo(
+ () => colord(colors['blue-default']).alpha(0.5).toRgbString(),
+ [colors],
+ );
+
+ const bgStyle = useAnimatedStyle(
+ () =>
+ logoXValue.value > -10
+ ? {
+ backgroundColor: processBgColor,
+ }
+ : {},
+ [processBgColor],
+ );
+
+ const start = React.useCallback(() => {
+ blueBgXValue.value = withTiming(-100, {
+ duration: 900,
+ easing: Easing.linear,
+ });
+
+ logoXValue.value = withTiming(100, {
+ duration: 900,
+ easing: Easing.linear,
+ });
+
+ const config = {
+ duration: 75,
+ easing: Easing.linear,
+ };
+
+ logoYValue.value = withRepeat(
+ withSequence(
+ withTiming(-16, config),
+ withTiming(0, config),
+ withTiming(16, config),
+ withTiming(0, config),
+ ),
+ 3,
+ true,
+ );
+
+ hiddenAnimated.value = withDelay(
+ 910,
+ withTiming(1, {
+ duration: 0,
+ }),
+ );
+ }, [blueBgXValue, hiddenAnimated, logoXValue, logoYValue]);
+
+ const showOriginButtonStyle = useAnimatedStyle(() => ({
+ display: hiddenAnimated.value === 1 ? 'flex' : 'none',
+ }));
+
+ const showAnimatedButtonStyle = useAnimatedStyle(() => ({
+ display: hiddenAnimated.value === 1 ? 'none' : 'flex',
+ }));
+
+ useEffect(() => {
+ if (props.gasLess) {
+ start();
+ }
+ }, [start, props.gasLess]);
+
+ if (props.showOrigin) {
+ return <>{props.children}>;
+ }
+
+ return (
+ <>
+
+ {props.children}
+
+
+
+
+
+
+
+
+ {renderText(props.title, {
+ style: StyleSheet.flatten([
+ props.titleStyle,
+ props.gasLess ? { color: colors['neutral-title-2'] } : {},
+ ]),
+ })}
+
+
+
+ >
+ );
+};
+
+const getStyles = createGetStyles(colors => ({
+ securityLevelTip: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 15,
+ backgroundColor: colors['neutral-card-2'],
+ color: colors['neutral-card-2'],
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ borderRadius: 4,
+ position: 'relative',
+ },
+ tipTriangle: {
+ position: 'absolute',
+ top: -13,
+ left: 110,
+ width: 0,
+ height: 0,
+ backgroundColor: 'transparent',
+ borderStyle: 'solid',
+ borderLeftWidth: 5,
+ borderRightWidth: 5,
+ borderTopWidth: 5,
+ borderBottomWidth: 8,
+ borderLeftColor: 'transparent',
+ borderRightColor: 'transparent',
+ borderTopColor: 'transparent',
+ borderBottomColor: colors['neutral-card-2'],
+ },
+ text: {
+ flex: 1,
+ color: colors['neutral-title-1'],
+ fontSize: 12,
+ fontWeight: '500',
+ },
+ imageBackground: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ backgroundColor: 'pink',
+ },
+ image: {
+ resizeMode: 'contain',
+ width: 100,
+ },
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ gasToSign: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ backgroundColor: colors['neutral-card-2'],
+ color: colors['neutral-card-2'],
+ },
+ gasText: {
+ color: colors['neutral-title-1'],
+ },
+ linearGradient: {
+ marginHorizontal: 'auto',
+ paddingHorizontal: 10,
+ paddingVertical: 7,
+ borderRadius: 6,
+ },
+ linearGradientText: {
+ fontSize: 11,
+ color: colors['neutral-title-2'],
+ cursor: 'pointer',
+ },
+}));
diff --git a/apps/mobile/src/components/Approval/components/FooterBar/ProcessActions.tsx b/apps/mobile/src/components/Approval/components/FooterBar/ProcessActions.tsx
index 8eb12be1c..23317bc81 100644
--- a/apps/mobile/src/components/Approval/components/FooterBar/ProcessActions.tsx
+++ b/apps/mobile/src/components/Approval/components/FooterBar/ProcessActions.tsx
@@ -6,6 +6,7 @@ import { StyleSheet, View } from 'react-native';
import { Button } from '@/components';
import { AppColorsVariants } from '@/constant/theme';
import { useThemeColors } from '@/hooks/theme';
+import { GasLessAnimatedWrapper } from './GasLessComponents';
const getStyles = (colors: AppColorsVariants) =>
StyleSheet.create({
@@ -32,6 +33,7 @@ export const ProcessActions: React.FC = ({
disabledProcess,
tooltipContent,
submitText,
+ gasLess,
}) => {
const { t } = useTranslation();
const colors = useThemeColors();
@@ -43,15 +45,30 @@ export const ProcessActions: React.FC = ({
// @ts-expect-error
content={tooltipContent}>
-
+ buttonStyle={styles.button}
+ gasLess={gasLess}
+ showOrigin={!gasLess && !disabledProcess}
+ type="process">
+
+
diff --git a/apps/mobile/src/components/Approval/components/FooterBar/SubmitActions.tsx b/apps/mobile/src/components/Approval/components/FooterBar/SubmitActions.tsx
index f40f01602..a8119f9fc 100644
--- a/apps/mobile/src/components/Approval/components/FooterBar/SubmitActions.tsx
+++ b/apps/mobile/src/components/Approval/components/FooterBar/SubmitActions.tsx
@@ -6,6 +6,7 @@ import { Button } from '@/components/Button';
import { Tip } from '@/components/Tip';
import { AppColorsVariants } from '@/constant/theme';
import { useThemeColors } from '@/hooks/theme';
+import { GasLessAnimatedWrapper } from './GasLessComponents';
const getStyles = (colors: AppColorsVariants) =>
StyleSheet.create({
@@ -35,6 +36,7 @@ export const SubmitActions: React.FC = ({
onCancel,
tooltipContent,
enableTooltip,
+ gasLess,
}) => {
const { t } = useTranslation();
const [isSign, setIsSign] = React.useState(false);
@@ -51,15 +53,22 @@ export const SubmitActions: React.FC = ({
// @ts-expect-error
-
+ titleStyle={styles.buttonText}
+ buttonStyle={styles.button}
+ gasLess={gasLess}
+ showOrigin={!gasLess && !disabledProcess}>
+
+
)}
diff --git a/apps/mobile/src/components/Approval/components/SignTx/SignTx.tsx b/apps/mobile/src/components/Approval/components/SignTx/SignTx.tsx
index 0ecc8b0b6..0ef163b90 100644
--- a/apps/mobile/src/components/Approval/components/SignTx/SignTx.tsx
+++ b/apps/mobile/src/components/Approval/components/SignTx/SignTx.tsx
@@ -66,7 +66,6 @@ import {
getRecommendNonce,
getRecommendGas,
getNativeTokenBalance,
- getGasLimitBaseAccountBalance,
explainGas,
} from './calc';
import { TxTypeComponent } from './TxTypeComponent';
@@ -232,6 +231,18 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
priority_price: null,
},
]);
+
+ const [currentAccountType, setCurrentAccountType] = useState<
+ undefined | string
+ >();
+ const [gasLessLoading, setGasLessLoading] = useState(false);
+ const [canUseGasLess, setCanUseGasLess] = useState(false);
+ const [useGasLess, setUseGasLess] = useState(false);
+
+ // const [isGnosisAccount, setIsGnosisAccount] = useState(false);
+ // const [isCoboArugsAccount, setIsCoboArugsAccount] = useState(false);
+ const isGnosisAccount = false;
+ const isCoboArugsAccount = false;
const scrollRef = useRef(null);
// const scrollRefSize = useSize(scrollRef);
// const scrollInfo = useScroll(scrollRef);
@@ -344,6 +355,7 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
nativeTokenPrice: txDetail?.native_token.price || 0,
tx,
gasLimit,
+ isReady,
});
const checkErrors = useCheckGasAndNonce({
@@ -360,6 +372,38 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
recommendGasLimitRatio,
});
+ const isGasNotEnough = useMemo(() => {
+ return checkErrors.some(e => e.code === 3001);
+ }, [checkErrors]);
+
+ const isSupportedAddr = useMemo(() => {
+ const isNotWalletConnect =
+ currentAccountType !== KEYRING_TYPE.WalletConnectKeyring;
+ const isNotWatchAddress =
+ currentAccountType !== KEYRING_TYPE.WatchAddressKeyring;
+
+ return isNotWatchAddress && isNotWalletConnect;
+ }, [currentAccountType]);
+
+ const [noCustomRPC, setNoCustomRPC] = useState(true);
+
+ // useEffect(() => {
+ // const hasCustomRPC = async () => {
+ // if (chain?.enum) {
+ // const b = await wallet.hasCustomRPC(chain?.enum);
+ // setNoCustomRPC(!b);
+ // }
+ // };
+ // hasCustomRPC();
+ // }, [chain?.enum]);
+
+ const showGasLess = useMemo(() => {
+ return isGasNotEnough;
+ // return (
+ // chainSupportGasLess && isGasNotEnough && isSupportedAddr && noCustomRPC
+ // );
+ }, [isGasNotEnough]);
+
const explainTx = async (address: string) => {
let recommendNonce = '0x0';
recommendNonce = await getRecommendNonce({
@@ -433,21 +477,6 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
}
if (tx.gas && origin === INTERNAL_REQUEST_ORIGIN) {
setGasLimit(intToHex(Number(tx.gas))); // use origin gas as gasLimit when tx is an internal tx with gasLimit(i.e. for SendMax native token)
- reCalcGasLimitBaseAccountBalance({
- nonce: (updateNonce ? recommendNonce : tx.nonce) || '0x1',
- tx: {
- ...tx,
- nonce: (updateNonce ? recommendNonce : tx.nonce) || '0x1', // set a mock nonce for explain if dapp not set it
- data: tx.data,
- value: tx.value || '0x0',
- gas: tx.gas || '', // set gas limit if dapp not set
- },
- gasPrice: selectedGas?.price || 0,
- customRecommendGasLimit: gas.toNumber(),
- customGasLimit: Number(tx.gas),
- customRecommendGasLimitRatio: 1,
- block,
- });
} else if (!gasLimit) {
// use server response gas limit
const ratio =
@@ -457,21 +486,6 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
? gas.times(ratio).toFixed(0)
: gas.toFixed(0);
setGasLimit(intToHex(Number(recommendGasLimit)));
- reCalcGasLimitBaseAccountBalance({
- nonce: (updateNonce ? recommendNonce : tx.nonce) || '0x1',
- tx: {
- ...tx,
- nonce: (updateNonce ? recommendNonce : tx.nonce) || '0x1', // set a mock nonce for explain if dapp not set it
- data: tx.data,
- value: tx.value || '0x0',
- gas: tx.gas || '', // set gas limit if dapp not set
- },
- gasPrice: selectedGas?.price || 0,
- customRecommendGasLimit: gas.toNumber(),
- customGasLimit: Number(recommendGasLimit),
- customRecommendGasLimitRatio: needRatio ? ratio : 1,
- block,
- });
}
setTxDetail(res);
@@ -650,6 +664,7 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
pushType: pushInfo.type,
lowGasDeadline: pushInfo.lowGasDeadline,
reqId,
+ isGasLess: useGasLess,
});
return;
@@ -722,18 +737,6 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
setGasLimit(intToHex(gas.gasLimit));
if (Number(gasLimit) !== gas.gasLimit) {
setManuallyChangeGasLimit(true);
- } else {
- reCalcGasLimitBaseAccountBalance({
- gasPrice: gas.price,
- tx: {
- ...tx,
- gasPrice: intToHex(Math.round(gas.price)),
- gas: intToHex(gas.gasLimit),
- nonce: afterNonce,
- },
- nonce: afterNonce,
- block: blockInfo,
- });
}
setRealNonce(afterNonce);
@@ -785,6 +788,44 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
}
};
+ const checkGasLessStatus = async () => {
+ const sendUsdValue =
+ txDetail?.balance_change.send_token_list?.reduce((sum, item) => {
+ return new BigNumber(item.raw_amount || 0)
+ .div(10 ** item.decimals)
+ .times(item.price || 0)
+ .plus(sum);
+ }, new BigNumber(0)) || new BigNumber(0);
+ const receiveUsdValue =
+ txDetail?.balance_change?.receive_token_list.reduce((sum, item) => {
+ return new BigNumber(item.raw_amount || 0)
+ .div(10 ** item.decimals)
+ .times(item.price || 0)
+ .plus(sum);
+ }, new BigNumber(0)) || new BigNumber(0);
+ try {
+ setGasLessLoading(true);
+ const res = await openapi.gasLessTxCheck({
+ tx: {
+ ...tx,
+ nonce: realNonce,
+ gasPrice: tx.gasPrice || tx.maxFeePerGas,
+ gas: gasLimit,
+ },
+ usdValue: Math.max(sendUsdValue.toNumber(), receiveUsdValue.toNumber()),
+ preExecSuccess: txDetail?.pre_exec.success || false,
+ gasUsed: txDetail?.gas?.gas_used || 0,
+ });
+
+ setCanUseGasLess(res.is_gasless);
+ setGasLessLoading(false);
+ } catch (error) {
+ console.error('gasLessTxCheck error', error);
+
+ setGasLessLoading(false);
+ }
+ };
+
const handleIgnoreAllRules = () => {
apiApprovalSecurityEngine.processAllRules(
engineResults.map(result => result.id),
@@ -820,6 +861,9 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
apiApprovalSecurityEngine.resetCurrentTx();
try {
const currentAccount = (await preferenceService.getCurrentAccount())!;
+
+ setCurrentAccountType(currentAccount.type);
+
const is1559 =
support1559 &&
SUPPORT_1559_KEYRING_TYPE.includes(currentAccount.type as any);
@@ -919,51 +963,6 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
}
};
- const reCalcGasLimitBaseAccountBalance = async ({
- gasPrice,
- nonce,
- tx,
- customRecommendGasLimit,
- customGasLimit,
- customRecommendGasLimitRatio,
- block,
- }: {
- tx: Tx;
- nonce: number | string | BigNumber;
- gasPrice: number | string | BigNumber;
- customRecommendGasLimit?: number;
- customGasLimit?: number;
- customRecommendGasLimitRatio?: number;
- block: BlockInfo | null;
- }) => {
- const calcGasLimit = customGasLimit || gasLimit;
- const calcGasLimitRatio =
- customRecommendGasLimitRatio || recommendGasLimitRatio;
- const calcRecommendGasLimit = customRecommendGasLimit || recommendGasLimit;
- if (!calcGasLimit) return;
- const currentAccount = (await preferenceService.getCurrentAccount())!;
- const { pendings } = await transactionHistoryService.getList(
- currentAccount.address,
- );
- let res = getGasLimitBaseAccountBalance({
- gasPrice,
- nonce,
- pendingList: pendings.filter(item => item.chainId === chainId),
- nativeTokenBalance,
- tx,
- recommendGasLimit: calcRecommendGasLimit,
- recommendGasLimitRatio: calcGasLimitRatio,
- });
-
- if (block && res > Number(block.gasLimit)) {
- res = Math.floor(Number(block.gasLimit) * 0.95); // use 95% of block gasLimit when gasLimit greater than block gasLimit
- }
- if (!new BigNumber(res).eq(calcGasLimit)) {
- setGasLimit(`0x${new BigNumber(res).toNumber().toString(16)}`);
- setManuallyChangeGasLimit(false);
- }
- };
-
const executeSecurityEngine = async () => {
const ctx = formatSecurityEngineCtx({
actionData: actionData,
@@ -1009,6 +1008,41 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
// }
// }, [isReady]);
+ useEffect(() => {
+ if (
+ isReady &&
+ !gasExplainResponse.isExplainingGas &&
+ !isGnosisAccount &&
+ !isCoboArugsAccount
+ ) {
+ let sendNativeTokenAmount = new BigNumber(tx.value); // current transaction native token transfer count
+ sendNativeTokenAmount = isNaN(sendNativeTokenAmount.toNumber())
+ ? new BigNumber(0)
+ : sendNativeTokenAmount;
+ const gasNotEnough = gasExplainResponse.maxGasCostAmount
+ .plus(sendNativeTokenAmount.div(1e18))
+ .isGreaterThan(new BigNumber(nativeTokenBalance).div(1e18));
+ if (gasNotEnough && isSupportedAddr && noCustomRPC) {
+ checkGasLessStatus();
+ } else {
+ setGasLessLoading(false);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ isReady,
+ nativeTokenBalance,
+ gasLimit,
+ tx,
+ realNonce,
+ txDetail,
+ isSupportedAddr,
+ noCustomRPC,
+ gasExplainResponse,
+ isGnosisAccount,
+ isCoboArugsAccount,
+ ]);
+
useEffect(() => {
if (!inited) return;
explain();
@@ -1035,7 +1069,6 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
const colors = useThemeColors();
const styles = React.useMemo(() => getStyles(colors), [colors]);
-
return (
@@ -1127,6 +1160,10 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
{txDetail && (
setUseGasLess(true)}
hasShadow={footerShowShadow}
origin={origin}
originLogo={params.session.icon}
@@ -1139,11 +1176,16 @@ export const SignTx = ({ params, origin }: SignTxProps) => {
onSubmit={() => handleAllow()}
onIgnoreAllRules={handleIgnoreAllRules}
enableTooltip={
- !canProcess ||
- !!checkErrors.find(item => item.level === 'forbidden')
+ // 3001 use gasless tip
+ checkErrors && checkErrors?.[0]?.code === 3001
+ ? false
+ : !canProcess ||
+ !!checkErrors.find(item => item.level === 'forbidden')
}
tooltipContent={
- checkErrors.find(item => item.level === 'forbidden')
+ checkErrors && checkErrors?.[0]?.code === 3001
+ ? undefined
+ : checkErrors.find(item => item.level === 'forbidden')
? checkErrors.find(item => item.level === 'forbidden')!.msg
: cantProcessReason
}
diff --git a/apps/mobile/src/components/Approval/components/SignTx/calc.ts b/apps/mobile/src/components/Approval/components/SignTx/calc.ts
index 574dd4eae..e04bf0e33 100644
--- a/apps/mobile/src/components/Approval/components/SignTx/calc.ts
+++ b/apps/mobile/src/components/Approval/components/SignTx/calc.ts
@@ -156,29 +156,45 @@ export const useExplainGas = ({
nativeTokenPrice,
tx,
gasLimit,
-}: Parameters[0]) => {
+ isReady,
+}: {
+ gasUsed: number | string;
+ gasPrice: number | string;
+ chainId: number;
+ nativeTokenPrice: number;
+ tx: Tx;
+ gasLimit: string | undefined;
+ isReady: boolean;
+}) => {
const [result, setResult] = useState({
gasCostUsd: new BigNumber(0),
gasCostAmount: new BigNumber(0),
maxGasCostAmount: new BigNumber(0),
});
+ const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
- explainGas({
- gasUsed,
- gasPrice,
- chainId,
- nativeTokenPrice,
- tx,
- gasLimit,
- }).then(data => {
- setResult(data);
- });
- }, [gasUsed, gasPrice, chainId, nativeTokenPrice, tx, gasLimit]);
+ if (isReady) {
+ explainGas({
+ gasUsed,
+ gasPrice,
+ chainId,
+ nativeTokenPrice,
+ tx,
+ gasLimit,
+ }).then(data => {
+ setResult(data);
+ setIsLoading(false);
+ });
+ }
+ }, [gasUsed, gasPrice, chainId, nativeTokenPrice, tx, gasLimit, isReady]);
- return {
- ...result,
- };
+ return useMemo(() => {
+ return {
+ ...result,
+ isExplainingGas: isLoading,
+ };
+ }, [result, isLoading]);
};
export const checkGasAndNonce = ({
@@ -306,6 +322,7 @@ export const useCheckGasAndNonce = ({
isGnosisAccount,
nativeTokenBalance,
}),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
[
recommendGasLimit,
recommendNonce,
@@ -320,84 +337,3 @@ export const useCheckGasAndNonce = ({
],
);
};
-
-export const getGasLimitBaseAccountBalance = ({
- gasPrice,
- nativeTokenBalance,
- nonce,
- pendingList,
- tx,
- recommendGasLimit,
- recommendGasLimitRatio,
-}: {
- tx: Tx;
- nonce: number | string | BigNumber;
- gasPrice: number | string | BigNumber;
- pendingList: TransactionGroup[];
- nativeTokenBalance: string;
- recommendGasLimit: string | number;
- recommendGasLimitRatio: number;
-}) => {
- let sendNativeTokenAmount = new BigNumber(tx.value); // current transaction native token transfer count
- sendNativeTokenAmount = isNaN(sendNativeTokenAmount.toNumber())
- ? new BigNumber(0)
- : sendNativeTokenAmount;
- const pendingsSumNativeTokenCost = pendingList
- .filter(item => new BigNumber(item.nonce).lt(nonce))
- .reduce((sum, item) => {
- return sum.plus(
- item.txs
- .map(txItem => ({
- value: isNaN(Number(txItem.rawTx.value))
- ? 0
- : Number(txItem.rawTx.value),
- gasPrice: txItem.rawTx.gasPrice || txItem.rawTx.maxFeePerGas,
- gasUsed:
- txItem.gasUsed || txItem.rawTx.gasLimit || txItem.rawTx.gas || 0,
- }))
- .reduce((sum, txItem) => {
- return sum.plus(
- new BigNumber(txItem.value).plus(
- new BigNumber(txItem.gasUsed).times(txItem.gasUsed),
- ),
- );
- }, new BigNumber(0)),
- );
- }, new BigNumber(0)); // sum native token cost in pending tx list which nonce less than current tx
- const avaliableGasToken = new BigNumber(nativeTokenBalance).minus(
- sendNativeTokenAmount.plus(pendingsSumNativeTokenCost),
- ); // avaliableGasToken = current native token balance - sendNativeTokenAmount - pendingsSumNativeTokenCost
- if (avaliableGasToken.lte(0)) {
- // avaliableGasToken less than 0 use 1.5x gasUsed as gasLimit
- return Math.floor(
- new BigNumber(recommendGasLimit)
- .times(Math.min(recommendGasLimitRatio, 1.5))
- .toNumber(),
- );
- }
- if (
- avaliableGasToken.gt(
- new BigNumber(gasPrice).times(
- Number(recommendGasLimit) * recommendGasLimitRatio,
- ),
- )
- ) {
- // if avaliableGasToken is enough to pay gas fee of recommendGasLimit * recommendGasLimitRatio, use recommendGasLimit * recommendGasLimitRatio as gasLimit
- return Math.ceil(Number(recommendGasLimit) * recommendGasLimitRatio);
- }
- const adaptGasLimit = avaliableGasToken.div(gasPrice); // adapt gasLimit by account balance
- if (
- adaptGasLimit.lt(
- new BigNumber(recommendGasLimit).times(
- Math.min(recommendGasLimitRatio, 1.5),
- ),
- )
- ) {
- return Math.floor(
- new BigNumber(recommendGasLimit)
- .times(Math.min(recommendGasLimitRatio, 1.5))
- .toNumber(),
- );
- }
- return Math.floor(adaptGasLimit.toNumber());
-};
diff --git a/apps/mobile/src/core/controllers/provider.ts b/apps/mobile/src/core/controllers/provider.ts
index 9f15cdd59..1867c2b5e 100644
--- a/apps/mobile/src/core/controllers/provider.ts
+++ b/apps/mobile/src/core/controllers/provider.ts
@@ -119,6 +119,7 @@ interface ApprovalRes extends Tx {
pushType?: TxPushType;
lowGasDeadline?: number;
reqId?: string;
+ isGasLess?: boolean;
}
interface Web3WalletPermission {
@@ -418,6 +419,7 @@ class ProviderController extends BaseController {
const pushType = approvalRes.pushType || 'default';
const lowGasDeadline = approvalRes.lowGasDeadline;
const preReqId = approvalRes.reqId;
+ const isGasLess = approvalRes.isGasLess || false;
let signedTransactionSuccess = false;
delete txParams.isSend;
@@ -434,6 +436,7 @@ class ProviderController extends BaseController {
delete approvalRes.lowGasDeadline;
delete approvalRes.reqId;
delete txParams.isCoboSafe;
+ delete approvalRes.isGasLess;
let is1559 = is1559Tx(approvalRes);
if (
@@ -734,6 +737,7 @@ class ProviderController extends BaseController {
low_gas_deadline: lowGasDeadline,
req_id: preReqId || '',
origin,
+ is_gasless: isGasLess,
});
hash = res.req.tx_id || undefined;
diff --git a/apps/mobile/src/screens/Swap/components/QuoteItem.tsx b/apps/mobile/src/screens/Swap/components/QuoteItem.tsx
index 6df47d915..c6b5a2435 100644
--- a/apps/mobile/src/screens/Swap/components/QuoteItem.tsx
+++ b/apps/mobile/src/screens/Swap/components/QuoteItem.tsx
@@ -151,12 +151,20 @@ export const DexQuoteItem = (
.times(receiveToken.price)
.minus(sortIncludeGasFee ? bestQuoteGasUsd : 0);
- const percent = receivedUsdBn
+ let percent = receivedUsdBn
.minus(bestQuoteUsdBn)
.div(bestQuoteUsdBn)
.abs()
.times(100);
+ if (!receiveToken.price) {
+ percent = receivedTokeAmountBn
+ .minus(bestQuoteAmountBn)
+ .div(bestQuoteAmountBn)
+ .abs()
+ .times(100);
+ }
+
receivedUsd = formatUsdValue(
receivedTokeAmountBn.times(receiveToken.price || 0).toString(10),
);
diff --git a/apps/mobile/src/screens/Swap/components/Quotes.tsx b/apps/mobile/src/screens/Swap/components/Quotes.tsx
index a64ead221..b517677bc 100644
--- a/apps/mobile/src/screens/Swap/components/Quotes.tsx
+++ b/apps/mobile/src/screens/Swap/components/Quotes.tsx
@@ -60,9 +60,16 @@ export const Quotes = ({
const viewCount = useMemo(() => {
if (swapViewList) {
+ const DexList = Object.keys(DEX);
+ const CEXList = Object.keys(CEX);
+
+ const availableList = [...DexList, ...CEXList];
+
return (
exchangeCount -
- Object.values(swapViewList).filter(e => e === false).length
+ Object.entries(swapViewList).filter(
+ ([name, e]) => e === false && availableList.includes(name),
+ ).length
);
}
return exchangeCount;
@@ -70,7 +77,10 @@ export const Quotes = ({
const tradeCount = useMemo(() => {
if (swapTradeList) {
- return Object.values(swapTradeList).filter(e => e === true).length;
+ const TradeDexList = Object.keys(DEX);
+ return Object.entries(swapTradeList).filter(
+ ([name, enable]) => enable === true && TradeDexList.includes(name),
+ ).length;
}
return 0;
}, [swapTradeList]);
@@ -83,6 +93,7 @@ export const Quotes = ({
() => [
...(list?.sort((a, b) => {
const getNumber = (quote: typeof a) => {
+ const price = other.receiveToken.price ? other.receiveToken.price : 1;
if (quote.isDex) {
if (inSufficient) {
return new BigNumber(quote.data?.toTokenAmount || 0)
@@ -91,10 +102,10 @@ export const Quotes = ({
(quote.data?.toTokenDecimals ||
other.receiveToken.decimals),
)
- .times(other.receiveToken.price);
+ .times(price);
}
if (!quote.preExecResult) {
- return new BigNumber(-Number.MAX_SAFE_INTEGER);
+ return new BigNumber(Number.MIN_SAFE_INTEGER);
}
if (sortIncludeGasFee) {
@@ -102,21 +113,19 @@ export const Quotes = ({
quote?.preExecResult.swapPreExecTx.balance_change
.receive_token_list?.[0]?.amount || 0,
)
- .times(other.receiveToken.price)
+ .times(price)
.minus(quote?.preExecResult?.gasUsdValue || 0);
}
return new BigNumber(
quote?.preExecResult.swapPreExecTx.balance_change
.receive_token_list?.[0]?.amount || 0,
- ).times(other.receiveToken.price);
+ ).times(price);
}
- return new BigNumber(
- quote?.data?.receive_token
- ? quote?.data?.receive_token?.amount
- : -Number.MAX_SAFE_INTEGER,
- ).times(other.receiveToken.price);
+ return quote?.data?.receive_token
+ ? new BigNumber(quote?.data?.receive_token?.amount).times(price)
+ : new BigNumber(Number.MIN_SAFE_INTEGER);
};
return getNumber(b).minus(getNumber(a)).toNumber();
}) || []),
diff --git a/yarn.lock b/yarn.lock
index fff7b9a7a..e758d6b3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5613,9 +5613,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@rabby-wallet/rabby-api@npm:0.7.12":
- version: 0.7.12
- resolution: "@rabby-wallet/rabby-api@npm:0.7.12"
+"@rabby-wallet/rabby-api@npm:0.7.14":
+ version: 0.7.14
+ resolution: "@rabby-wallet/rabby-api@npm:0.7.14"
dependencies:
"@rabby-wallet/rabby-sign": ^0.3.3
axios: ^0.27.2
@@ -5626,7 +5626,7 @@ __metadata:
peerDependencies:
"@debank/common": ^0.3.9
"@rabby-wallet/rabby-sign": ">= 0.3"
- checksum: 2b327aad3dcf2392d7a02a702df3262036ba6d8bde825e77005996f6fefecc4c08535041b57401ad69d68e4b9d4644cc26bf07a09c72e55603d6dc7a325a29e1
+ checksum: 6b2cce84e07d8110e10fda2e6cad899f68c5b1f20e7d9856b0812df0aa84d225ad458200b2e9c16599ddc617935d32eb0f62d6d572e3ae932f37ebd67647e713
languageName: node
linkType: hard
@@ -26745,7 +26745,7 @@ __metadata:
"@rabby-wallet/eth-walletconnect-keyring": 2.1.3
"@rabby-wallet/object-multiplex": "workspace:^"
"@rabby-wallet/persist-store": "workspace:^"
- "@rabby-wallet/rabby-api": 0.7.12
+ "@rabby-wallet/rabby-api": 0.7.14
"@rabby-wallet/rabby-security-engine": ^1.1.17
"@rabby-wallet/rabby-swap": 0.0.36
"@rabby-wallet/service-address": "workspace:^"