Skip to content

Commit

Permalink
fix: [GPR-528] Add support for zero IMX balance swaps with Passport i…
Browse files Browse the repository at this point in the history
…n Swap widget (#1716)
  • Loading branch information
dreamoftrees authored May 1, 2024
1 parent 1de6ec3 commit 5cc264c
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 9 deletions.
44 changes: 43 additions & 1 deletion packages/checkout/sdk/src/transaction/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ import { ethers } from 'ethers';
import { CheckoutError, CheckoutErrorType } from '../errors';
import { sendTransaction } from './transaction';
import { ChainId, NetworkInfo } from '../types';
import { IMMUTABLE_ZKVEM_GAS_OVERRIDES } from '../env';

describe('transaction', () => {
beforeEach(() => {
jest.resetAllMocks();
});

it('should send the transaction and return success', async () => {
const transactionResponse = {
hash: '123',
from: '0x234',
confirmations: 5,
};
const mockSendTransaction = jest.fn().mockResolvedValue(transactionResponse);
const mockProvider = {
getNetwork: jest.fn().mockReturnValue({
chainId: ChainId.IMTBL_ZKEVM_TESTNET,
} as NetworkInfo),
getSigner: jest.fn().mockReturnValue({
sendTransaction: () => transactionResponse,
sendTransaction: mockSendTransaction,
}),
} as unknown as Web3Provider;

Expand All @@ -34,6 +40,7 @@ describe('transaction', () => {
await expect(sendTransaction(mockProvider, transaction)).resolves.toEqual({
transactionResponse,
});
expect(mockSendTransaction).toHaveBeenCalledWith(transaction);
});

it('should return errored status if transaction errors', async () => {
Expand Down Expand Up @@ -163,4 +170,39 @@ describe('transaction', () => {
}
},
);

it(
'should include txn gas limits for zkEVM chains if the gasPrice is not defined on the transaction',
async () => {
const transactionResponse = {
hash: '123',
from: '0x234',
confirmations: 5,
};
const mockSendTransaction = jest.fn().mockResolvedValue(transactionResponse);
const mockProvider = {
getNetwork: jest.fn().mockReturnValue({
chainId: ChainId.IMTBL_ZKEVM_TESTNET,
} as NetworkInfo),
getSigner: jest.fn().mockReturnValue({
sendTransaction: mockSendTransaction,
}),
} as unknown as Web3Provider;

const transaction = {
to: '0xAAA',
from: '0x234',
chainId: ChainId.ETHEREUM,
};

await expect(sendTransaction(mockProvider, transaction)).resolves.toEqual({
transactionResponse,
});
expect(mockSendTransaction).toHaveBeenCalledWith({
...transaction,
maxFeePerGas: IMMUTABLE_ZKVEM_GAS_OVERRIDES.maxFeePerGas,
maxPriorityFeePerGas: IMMUTABLE_ZKVEM_GAS_OVERRIDES.maxPriorityFeePerGas,
});
},
);
});
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const setTransactionGasLimits = async (

const { chainId } = await web3Provider.getNetwork();
if (!isZkEvmChainId(chainId)) return rawTx;
if (typeof rawTx.gasPrice !== 'undefined') return rawTx;

rawTx.maxFeePerGas = IMMUTABLE_ZKVEM_GAS_OVERRIDES.maxFeePerGas;
rawTx.maxPriorityFeePerGas = IMMUTABLE_ZKVEM_GAS_OVERRIDES.maxPriorityFeePerGas;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TransactionResponse } from '@imtbl/dex-sdk';
import { CheckoutErrorType } from '@imtbl/checkout-sdk';
import { useTranslation } from 'react-i18next';
import { getL2ChainId } from 'lib';
import { BigNumber } from 'ethers';
import { PrefilledSwapForm, SwapWidgetViews } from '../../../context/view-context/SwapViewContextTypes';
import {
ViewContext,
Expand All @@ -18,6 +19,7 @@ import { SwapFormData } from './swapFormTypes';
import { TransactionRejected } from '../../../components/TransactionRejected/TransactionRejected';
import { ConnectLoaderContext } from '../../../context/connect-loader-context/ConnectLoaderContext';
import { UserJourney, useAnalytics } from '../../../context/analytics-provider/SegmentAnalyticsProvider';
import { isPassportProvider } from '../../../lib/provider';

export interface SwapButtonProps {
loading: boolean
Expand Down Expand Up @@ -117,7 +119,10 @@ export function SwapButton({
}
const txn = await checkout.sendTransaction({
provider,
transaction: transaction.swap.transaction,
transaction: {
...transaction.swap.transaction,
gasPrice: (isPassportProvider(provider) ? BigNumber.from(0) : undefined),
},
});

viewDispatch({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { UnableToSwap } from './UnableToSwap';
import { ConnectLoaderContext } from '../../../context/connect-loader-context/ConnectLoaderContext';
import useDebounce from '../../../lib/hooks/useDebounce';
import { CancellablePromise } from '../../../lib/async/cancellablePromise';
import { isPassportProvider } from '../../../lib/provider';

enum SwapDirection {
FROM = 'FROM',
Expand Down Expand Up @@ -512,11 +513,14 @@ export function SwapForm({ data, theme }: SwapFromProps) {
}
}, [debouncedToAmount, toToken, fromToken]);

// during swaps, having enough IMX to cover the gas fee means
// during swaps, having enough IMX to cover the gas fee means (only relevant for non-Passport wallets)
// 1. swapping from any token to any token costs IMX - so do a check
// 2. If the swap from token is also IMX, include the additional amount into the calc
// as user will need enough imx for the swap amount and the gas
const insufficientFundsForGas = useMemo(() => {
if (!provider) return true;
if (isPassportProvider(provider)) return false;

const imxBalance = tokenBalances.find((b) => b.token.address?.toLowerCase() === NATIVE);
if (!imxBalance) return true;

Expand All @@ -527,7 +531,7 @@ export function SwapForm({ data, theme }: SwapFromProps) {
: BigNumber.from('0');

return gasAmount.add(additionalAmount).gt(imxBalance.balance);
}, [gasFeeValue, tokenBalances, fromToken, fromAmount]);
}, [gasFeeValue, tokenBalances, fromToken, fromAmount, provider]);

// -------------//
// FROM //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from 'react';
import { CheckoutErrorType, TokenInfo } from '@imtbl/checkout-sdk';
import { useTranslation } from 'react-i18next';
import { BigNumber } from 'ethers';
import { SimpleLayout } from '../../../components/SimpleLayout/SimpleLayout';
import { HeaderNavigation } from '../../../components/Header/HeaderNavigation';
import { sendSwapWidgetCloseEvent } from '../SwapWidgetEvents';
Expand Down Expand Up @@ -120,6 +121,8 @@ export function ApproveERC20Onboarding({ data }: ApproveERC20Props) {
return;
}

// eslint-disable-next-line no-console
console.error('Approve ERC20 failed', err);
viewDispatch({
payload: {
type: ViewActions.UPDATE_VIEW,
Expand All @@ -131,6 +134,11 @@ export function ApproveERC20Onboarding({ data }: ApproveERC20Props) {
});
};

const prepareTransaction = (transaction, isGasFree = false) => ({
...transaction,
gasPrice: (isGasFree ? BigNumber.from(0) : undefined),
});

/* --------------------- */
// Approve spending step //
/* --------------------- */
Expand All @@ -155,7 +163,7 @@ export function ApproveERC20Onboarding({ data }: ApproveERC20Props) {
try {
const txnResult = await checkout.sendTransaction({
provider,
transaction: data.approveTransaction,
transaction: prepareTransaction(data.approveTransaction, isPassport),
});

setApprovalTxnLoading(true);
Expand Down Expand Up @@ -258,7 +266,7 @@ export function ApproveERC20Onboarding({ data }: ApproveERC20Props) {
try {
const txn = await checkout.sendTransaction({
provider,
transaction: data.transaction,
transaction: prepareTransaction(data.transaction, isPassport),
});

setActionDisabled(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { NotEnoughImx } from '../../../components/NotEnoughImx/NotEnoughImx';
import { IMX_TOKEN_SYMBOL } from '../../../lib';
import { EventTargetContext } from '../../../context/event-target-context/EventTargetContext';
import { UserJourney, useAnalytics } from '../../../context/analytics-provider/SegmentAnalyticsProvider';
import { isPassportProvider } from '../../../lib/provider';

export interface SwapCoinsProps {
theme: WidgetTheme;
Expand Down Expand Up @@ -49,6 +50,7 @@ export function SwapCoins({
const {
connectLoaderState: {
checkout,
provider,
},
} = useContext(ConnectLoaderContext);

Expand All @@ -70,7 +72,7 @@ export function SwapCoins({
}, []);

useEffect(() => {
if (hasZeroBalance(tokenBalances, IMX_TOKEN_SYMBOL)) {
if (hasZeroBalance(tokenBalances, IMX_TOKEN_SYMBOL) && !isPassportProvider(provider)) {
setShowNotEnoughImxDrawer(true);
}
}, [tokenBalances]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ export const MainPage = () => {
const swapWidget = useMemo(() => widgetsFactory.create(WidgetType.SWAP), [widgetsFactory]);
const onRampWidget = useMemo(() => widgetsFactory.create(WidgetType.ONRAMP), [widgetsFactory]);

connectWidget.addListener(ConnectEventType.WALLETCONNECT_PROVIDER_UPDATED, (event) => {
connectWidget.addListener(ConnectEventType.WALLETCONNECT_PROVIDER_UPDATED, (event: any) => {
console.log('WalletConnnect provider ready', event);
});
connectWidget.addListener(ConnectEventType.CLOSE_WIDGET, () => { connectWidget.unmount() });
connectWidget.addListener(ConnectEventType.SUCCESS, (event) => {
connectWidget.addListener(ConnectEventType.SUCCESS, (event: any) => {
console.log('Connect success', event);
});
walletWidget.addListener(WalletEventType.CLOSE_WIDGET, () => { walletWidget.unmount() });
Expand Down

0 comments on commit 5cc264c

Please sign in to comment.