Deposit/Withdraw with abacus
This commit is contained in:
Generated
+6
@@ -15836,6 +15836,12 @@ packages:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: true
|
||||
|
||||
file:../../abacus/build/packages/js/dydxprotocol-abacus-0.2.28.tgz:
|
||||
resolution: {integrity: sha512-aOT+QROV5aL0QLNq7QINW6Bw3Vz8ds7LOoC4MQfPDsGGqgyundpAcPkLmgcbIk0Ft8BQsdOz+HbbAYL4n7Iqlg==, tarball: file:../../abacus/build/packages/js/dydxprotocol-abacus-0.2.28.tgz}
|
||||
name: '@dydxprotocol/abacus'
|
||||
version: 0.2.28
|
||||
dev: false
|
||||
|
||||
github.com/release-it/conventional-changelog/a4acfeac76369937a6566d698a5ae9180eceb413(release-it@15.11.0):
|
||||
resolution: {tarball: https://codeload.github.com/release-it/conventional-changelog/tar.gz/a4acfeac76369937a6566d698a5ae9180eceb413}
|
||||
id: github.com/release-it/conventional-changelog/a4acfeac76369937a6566d698a5ae9180eceb413
|
||||
|
||||
+5
-1
@@ -14,6 +14,8 @@ import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
|
||||
import { LocaleProvider } from './hooks/useLocaleSeparators';
|
||||
import { NotificationsProvider } from './hooks/useNotifications';
|
||||
import { SubaccountProvider } from './hooks/useSubaccount';
|
||||
import { SquidProvider } from '@/hooks/useSquidRouter';
|
||||
|
||||
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
|
||||
|
||||
import MarketsPage from '@/pages/markets/Markets';
|
||||
@@ -106,7 +108,9 @@ const App = () => (
|
||||
<SubaccountProvider>
|
||||
<NotificationsProvider>
|
||||
<DialogAreaProvider>
|
||||
<Content />
|
||||
<SquidProvider>
|
||||
<Content />
|
||||
</SquidProvider>
|
||||
</DialogAreaProvider>
|
||||
</NotificationsProvider>
|
||||
</SubaccountProvider>
|
||||
|
||||
@@ -19,6 +19,7 @@ type ElementProps = {
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
items: MenuConfig<string, string>;
|
||||
slotEmpty?: ReactNode;
|
||||
withSearch?: boolean;
|
||||
withReceiptItems?: DetailsItem[];
|
||||
};
|
||||
@@ -52,12 +53,10 @@ export const SearchSelectMenu = ({
|
||||
const Trigger = asChild ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
<Styled.MenuTrigger>
|
||||
<Styled.WithLabel label={label}>{children}</Styled.WithLabel>
|
||||
</Styled.MenuTrigger>
|
||||
<Styled.MenuTrigger>
|
||||
{label ? <Styled.WithLabel label={label}>{children}</Styled.WithLabel> : children}
|
||||
<Styled.TriggerIcon iconName={IconName.Triangle} open={open} />
|
||||
</>
|
||||
</Styled.MenuTrigger>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -86,7 +85,7 @@ export const SearchSelectMenu = ({
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.MenuTrigger = styled.div`
|
||||
height: 3.5rem;
|
||||
height: var(--form-input-height);
|
||||
|
||||
${layoutMixins.spacedRow}
|
||||
align-items: center;
|
||||
@@ -117,7 +116,7 @@ Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
|
||||
Styled.Popover = styled(Popover)`
|
||||
max-height: 30vh;
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--color-layer-6);
|
||||
border: var(--border-width) solid var(--color-layer-6);
|
||||
border-radius: 0.5rem;
|
||||
z-index: 2;
|
||||
box-shadow: none;
|
||||
@@ -136,6 +135,8 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
|
||||
--comboboxMenu-item-checked-textColor: var(--color-text-2);
|
||||
--comboboxMenu-item-highlighted-textColor: var(--color-text-2);
|
||||
|
||||
--stickyArea1-topHeight: var(--form-input-height);
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ export type AbacusDydxChainQueriesProtocol = Omit<
|
||||
Abacus.exchange.dydx.abacus.protocols.DYDXChainQueriesProtocol,
|
||||
'__doNotUseOrImplementIt'
|
||||
>;
|
||||
export type AbacusDYDXChainTransactionsProtocol = Omit<
|
||||
Abacus.exchange.dydx.abacus.protocols.DYDXChainTransactionsProtocol,
|
||||
'__doNotUseOrImplementIt'
|
||||
>;
|
||||
export type AbacusRestProtocol = Omit<
|
||||
Abacus.exchange.dydx.abacus.protocols.RestProtocol,
|
||||
'__doNotUseOrImplementIt'
|
||||
@@ -44,6 +48,10 @@ export const QueryType = Abacus.exchange.dydx.abacus.protocols.QueryType;
|
||||
const queryTypes = [...QueryType.values()] as const;
|
||||
export type QueryTypes = (typeof queryTypes)[number];
|
||||
|
||||
export const TransactionType = Abacus.exchange.dydx.abacus.protocols.TransactionType;
|
||||
const TransactionTypes = [...TransactionType.values()] as const;
|
||||
export type TransactionTypes = (typeof TransactionTypes)[number];
|
||||
|
||||
export type NetworkConfig = {
|
||||
chainId: string;
|
||||
indexerUrl: string;
|
||||
@@ -98,6 +106,8 @@ export type ClosePositionInputs = Abacus.exchange.dydx.abacus.output.input.Close
|
||||
export type TradeInputSummary = Abacus.exchange.dydx.abacus.output.input.TradeInputSummary;
|
||||
export type TransferInputs = Abacus.exchange.dydx.abacus.output.input.TransferInput;
|
||||
export type InputError = Abacus.exchange.dydx.abacus.output.input.ValidationError;
|
||||
export type TransferInputTokenResource = Abacus.exchange.dydx.abacus.output.input.TransferInputTokenResource;
|
||||
export type TransferInputChainResource = Abacus.exchange.dydx.abacus.output.input.TransferInputChainResource;
|
||||
export const ErrorType = Abacus.exchange.dydx.abacus.output.input.ErrorType;
|
||||
export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.SelectionOption;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum DialogTypes {
|
||||
Receive = 'Receive',
|
||||
Trade = 'Trade',
|
||||
Transfer = 'Transfer',
|
||||
Withdraw = 'Withdraw',
|
||||
}
|
||||
|
||||
export enum TradeBoxDialogTypes {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useApiState } from './useApiState';
|
||||
import { useBreakpoints } from './useBreakpoints';
|
||||
import { useCommandMenu } from './useCommandMenu';
|
||||
import { useCurrentMarketId } from './useCurrentMarketId';
|
||||
import { useDebounce } from './useDebounce';
|
||||
import { useInterval } from './useInterval';
|
||||
import { useDocumentTitle } from './useDocumentTitle';
|
||||
import { useDydxClient } from './useDydxClient';
|
||||
@@ -17,6 +18,7 @@ import { useOnClickOutside } from './useOnClickOutside';
|
||||
import { usePageTitlePriceUpdates } from './usePageTitlePriceUpdates';
|
||||
import { useShouldShowFooter } from './useShouldShowFooter';
|
||||
import { useSelectedNetwork } from './useSelectedNetwork';
|
||||
import { useSquidRouter } from './useSquidRouter';
|
||||
import { useStringGetter } from './useStringGetter';
|
||||
import { useSubaccount } from './useSubaccount';
|
||||
|
||||
@@ -25,6 +27,7 @@ export {
|
||||
useBreakpoints,
|
||||
useCommandMenu,
|
||||
useCurrentMarketId,
|
||||
useDebounce,
|
||||
useDocumentTitle,
|
||||
useDydxClient,
|
||||
useAccountBalance,
|
||||
@@ -41,5 +44,6 @@ export {
|
||||
useShouldShowFooter,
|
||||
useSelectedNetwork,
|
||||
useStringGetter,
|
||||
useSquidRouter,
|
||||
useSubaccount,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useBalance } from 'wagmi';
|
||||
import { StargateClient } from '@cosmjs/stargate';
|
||||
import { useQuery } from 'react-query';
|
||||
import { formatUnits } from 'ethers';
|
||||
import { formatUnits } from 'viem';
|
||||
|
||||
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
|
||||
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
|
||||
@@ -43,7 +43,7 @@ export const useAccountBalance = ({
|
||||
assetSymbol,
|
||||
bech32AddrPrefix,
|
||||
chainId,
|
||||
decimals,
|
||||
decimals = 0,
|
||||
rpc,
|
||||
isCosmosChain,
|
||||
}: UseAccountBalanceProps = {}) => {
|
||||
@@ -66,7 +66,7 @@ export const useAccountBalance = ({
|
||||
});
|
||||
|
||||
const cosmosQueryFn = useCallback(async () => {
|
||||
if (dydxAddress && bech32AddrPrefix && rpc) {
|
||||
if (dydxAddress && bech32AddrPrefix && rpc && addressOrDenom) {
|
||||
const address = convertBech32Address({
|
||||
address: dydxAddress,
|
||||
bech32Prefix: bech32AddrPrefix,
|
||||
@@ -76,14 +76,7 @@ export const useAccountBalance = ({
|
||||
const balanceAsCoin = await client.getBalance(address, addressOrDenom);
|
||||
await client.disconnect();
|
||||
|
||||
/**
|
||||
* Hardcode decimals because most cosmos coins default to 6 decimals.
|
||||
* Squids testnet has some coins as having 18 decimals instead of 6.
|
||||
* @url https://github1s.com/cosmos/cosmjs/blob/HEAD/packages/amino/src/coins.ts#L8-L16
|
||||
*
|
||||
* @todo: change '6' to 'decimals' when Squid testnet data is updated.
|
||||
* */
|
||||
return formatUnits(balanceAsCoin.amount, decimals);
|
||||
return formatUnits(BigInt(balanceAsCoin.amount), decimals);
|
||||
}
|
||||
}, [addressOrDenom, chainId, rpc]);
|
||||
|
||||
@@ -109,11 +102,7 @@ export const useAccountBalance = ({
|
||||
return {
|
||||
balance,
|
||||
nativeTokenBalance,
|
||||
isBalanceError: isCosmosChain
|
||||
? cosmosQuery.status === 'error' && !cosmosQuery.isFetching
|
||||
: evmQuery.status === 'error' && evmQuery.fetchStatus === 'idle',
|
||||
isBalanceLoading: isCosmosChain
|
||||
? cosmosQuery.status === 'loading' && cosmosQuery.isFetching
|
||||
: evmQuery.status === 'loading' && evmQuery.fetchStatus === 'fetching',
|
||||
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
|
||||
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ const useAccountsContext = () => {
|
||||
forgetEvmSignature(previousEvmAddress);
|
||||
}
|
||||
|
||||
if (evmAddress) abacusStateManager.setEvmAddress(evmAddress);
|
||||
|
||||
setPreviousEvmAddress(evmAddress);
|
||||
}, [evmAddress]);
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* https://usehooks-ts.com/react-hook/use-debounce
|
||||
* @param value
|
||||
* @param delay
|
||||
* @returns the debouncedValue
|
||||
*/
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { LocalStorageKey } from '@/constants/localStorage';
|
||||
import {
|
||||
DEFAULT_APP_ENVIRONMENT,
|
||||
DydxNetwork,
|
||||
isDydxV4Network,
|
||||
isValidDydxNetwork,
|
||||
} from '@/constants/networks';
|
||||
|
||||
@@ -16,8 +15,6 @@ import { initializeLocalization } from '@/state/app';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
|
||||
import squidRouter from '@/lib/squidRouter';
|
||||
|
||||
export const useInitializePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -31,9 +28,5 @@ export const useInitializePage = () => {
|
||||
useEffect(() => {
|
||||
dispatch(initializeLocalization());
|
||||
abacusStateManager.start({ network: localStorageNetwork });
|
||||
|
||||
if (isDydxV4Network(localStorageNetwork)) {
|
||||
squidRouter.switchNetwork(localStorageNetwork);
|
||||
}
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { ethers } from 'ethers';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Squid, type GetRoute, type RouteResponse, ChainData, ChainType } from '@0xsquid/sdk';
|
||||
import { USDC_DENOM } from '@dydxprotocol/v4-client';
|
||||
|
||||
import { TransferInputs } from '@/constants/abacus';
|
||||
import { DydxV4Network, isDydxV4Network } from '@/constants/networks';
|
||||
import { useAccounts } from '@/hooks/useAccounts';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
|
||||
const SQUID_BASE_URL: Record<DydxV4Network, string | undefined> = {
|
||||
[DydxV4Network.V4Testnet2]: 'https://squid-api-git-feat-cosmos-maintestnet-0xsquid.vercel.app',
|
||||
[DydxV4Network.V4Staging]: undefined,
|
||||
[DydxV4Network.V4Local]: undefined,
|
||||
[DydxV4Network.V4Mainnet]: 'https://api.0xsquid.com',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Deposit route defaults for IBC Testnet
|
||||
* */
|
||||
export const SQUID_DEPOSIT_ROUTE_DEFAULTS = {
|
||||
toChain: 'dydx-testnet-2',
|
||||
toToken: USDC_DENOM,
|
||||
enableForecall: false, // instant execution service, defaults to true
|
||||
};
|
||||
|
||||
export const SQUID_WITHDRAW_ROUTE_DEFAULTS = {
|
||||
fromChain: 'dydx-testnet-2',
|
||||
fromToken: USDC_DENOM,
|
||||
enableForecall: false, // instant execution service, defaults to true
|
||||
};
|
||||
|
||||
const useSquidContext = () => {
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
const [_, setInitialized] = useState(false);
|
||||
|
||||
const initializeClient = async () => {
|
||||
setInitialized(false);
|
||||
if (!squid) return;
|
||||
await squid.init();
|
||||
setInitialized(true);
|
||||
};
|
||||
|
||||
const squid = useMemo(
|
||||
() =>
|
||||
isDydxV4Network(selectedNetwork)
|
||||
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
|
||||
: undefined,
|
||||
[selectedNetwork]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (squid) {
|
||||
initializeClient();
|
||||
}
|
||||
}, [squid]);
|
||||
|
||||
return squid;
|
||||
};
|
||||
|
||||
type SquidContextType = ReturnType<typeof useSquidContext>;
|
||||
const SquidContext = createContext<SquidContextType>(undefined);
|
||||
SquidContext.displayName = '0xSquid';
|
||||
|
||||
export const SquidProvider = ({ ...props }) => (
|
||||
<SquidContext.Provider value={useSquidContext()} {...props} />
|
||||
);
|
||||
|
||||
export const useSquid = () => useContext(SquidContext);
|
||||
|
||||
export const useSquidRouter = () => {
|
||||
const squid = useSquid();
|
||||
const { signerWagmi } = useAccounts();
|
||||
|
||||
const submitDeposit = useCallback(
|
||||
async (requestPayload: TransferInputs['requestPayload']) => {
|
||||
if (!signerWagmi) {
|
||||
throw new Error('Missing signer');
|
||||
}
|
||||
if (
|
||||
!requestPayload?.targetAddress ||
|
||||
!requestPayload.data ||
|
||||
!requestPayload.value ||
|
||||
!requestPayload.gasLimit ||
|
||||
!requestPayload.gasPrice ||
|
||||
!requestPayload.routeType
|
||||
) {
|
||||
throw new Error('Missing request payload');
|
||||
}
|
||||
let tx = {
|
||||
to: requestPayload.targetAddress as `0x${string}`,
|
||||
data: requestPayload.data as `0x${string}`,
|
||||
gasLimit: ethers.toBigInt(requestPayload.gasLimit),
|
||||
value:
|
||||
requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined,
|
||||
};
|
||||
return await signerWagmi.sendTransaction(tx);
|
||||
},
|
||||
[signerWagmi]
|
||||
);
|
||||
|
||||
const getStatus = useCallback(
|
||||
async (transactionId: string) => {
|
||||
const status = await squid?.getStatus({ transactionId });
|
||||
return status;
|
||||
},
|
||||
[squid]
|
||||
);
|
||||
|
||||
return {
|
||||
getStatus,
|
||||
submitDeposit,
|
||||
|
||||
axelarscanURL: squid?.axelarscanURL,
|
||||
};
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux';
|
||||
import type { Nullable } from '@dydxprotocol/abacus';
|
||||
import Long from 'long';
|
||||
import type { IndexedTx } from '@cosmjs/stargate';
|
||||
import { type Coin } from '@cosmjs/proto-signing';
|
||||
import { EncodeObject, type Coin } from '@cosmjs/proto-signing';
|
||||
import { Method } from '@cosmjs/tendermint-rpc';
|
||||
|
||||
import {
|
||||
@@ -301,6 +301,32 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
sendSquidWithdraw: async ({
|
||||
subaccount,
|
||||
payload,
|
||||
}:{
|
||||
subaccount: SubaccountClient;
|
||||
payload: string;
|
||||
}) => {
|
||||
const decode = (str: string):string => Buffer.from(str, 'base64').toString('binary');
|
||||
const decoded = decode(payload);
|
||||
|
||||
const json = JSON.parse(decoded);
|
||||
|
||||
const ibcMsg: EncodeObject = {
|
||||
typeUrl: json.msgTypeUrl,
|
||||
value: json.msg,
|
||||
};
|
||||
const encodeObjects: Promise<EncodeObject[]> = Promise.resolve([ibcMsg]);
|
||||
|
||||
await compositeClient?.send(
|
||||
subaccount.wallet,
|
||||
() => encodeObjects,
|
||||
false
|
||||
)
|
||||
},
|
||||
|
||||
}),
|
||||
[compositeClient]
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
|
||||
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
|
||||
import { TradeDialog } from '@/views/dialogs/TradeDialog';
|
||||
import { TransferDialog } from '@/views/dialogs/TransferDialog';
|
||||
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
|
||||
|
||||
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
|
||||
import { FillDetailsDialog } from '@/views/dialogs/DetailsDialog/FillDetailsDialog';
|
||||
@@ -53,5 +54,6 @@ export const DialogManager = () => {
|
||||
[DialogTypes.Receive]: <ReceiveDialog {...modalProps} />,
|
||||
[DialogTypes.Trade]: <TradeDialog {...modalProps} />,
|
||||
[DialogTypes.Transfer]: <TransferDialog {...modalProps} />,
|
||||
[DialogTypes.Withdraw]: <WithdrawDialog {...modalProps} />,
|
||||
}[type];
|
||||
};
|
||||
|
||||
@@ -147,6 +147,10 @@ class AbacusStateManager {
|
||||
this.stateManager.accountAddress = walletAddress;
|
||||
};
|
||||
|
||||
setEvmAddress = (evmAddress: string) => {
|
||||
this.stateManager.sourceAddress = evmAddress;
|
||||
};
|
||||
|
||||
setSubaccountNumber = (subaccountNumber: number) =>
|
||||
(this.stateManager.subaccountNumber = subaccountNumber);
|
||||
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { RouteResponse, Squid } from '@0xsquid/sdk';
|
||||
import type { GetRoute, ChainData, RouteData, TokenData } from '@0xsquid/sdk';
|
||||
|
||||
import { DEFAULT_APP_ENVIRONMENT, DydxV4Network } from '@/constants/networks';
|
||||
|
||||
import { log } from './telemetry';
|
||||
|
||||
/**
|
||||
* @todo dYdX chain will eventually be added to squidSDK as an option for toChain and
|
||||
* USDC from Noble chain will also be a possible toToken in the near future. We will
|
||||
* need to eventually change these constants.
|
||||
* */
|
||||
const SQUID_TESTNET_ROUTE_DEFAULTS = {
|
||||
toChain: 'osmo-test-4', // Osmosis Testnet
|
||||
toToken: 'uausdc', // aUSDC
|
||||
enableForecall: true, // instant execution service, defaults to true
|
||||
quoteOnly: false, // optional, defaults to false
|
||||
};
|
||||
|
||||
// This config is for dev only! Used for testing General Message Passing (route provided by 0xSquid)
|
||||
const SQUID_TESTNET_AVAX_TO_AXELAR_WITH_GMP = {
|
||||
toChain: 'axelar-testnet-lisbon-3',
|
||||
toToken: 'uosmo',
|
||||
enableForecall: false,
|
||||
quoteOnly: false,
|
||||
};
|
||||
|
||||
const SQUID_MAINNET_ROUTE_DEFAULTS = {
|
||||
toChain: 'osmosis-1', // Osmosis Testnet
|
||||
toToken: 'uusdc', // aUSDC
|
||||
enableForecall: true, // instant execution service, defaults to true
|
||||
quoteOnly: false, // optional, defaults to false
|
||||
};
|
||||
|
||||
class SquidFactory {
|
||||
private client: Squid;
|
||||
private isInitialized: boolean;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.client = new Squid({ baseUrl });
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
try {
|
||||
await this.client.init();
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
log('SquidFactory/init', error);
|
||||
}
|
||||
}
|
||||
|
||||
getClient = () => this.client;
|
||||
}
|
||||
|
||||
const squidClientTestnet = new SquidFactory('https://testnet.api.0xsquid.com'); // new SquidFactory('https://squid-2gwsw7ij1-0xsquid.vercel.app');
|
||||
const squidClientMainnet = new SquidFactory('https://api.0xsquid.com');
|
||||
|
||||
const SQUID_CLIENT_BY_NETWORK: Record<DydxV4Network, SquidFactory> = {
|
||||
[DydxV4Network.V4Local]: squidClientTestnet,
|
||||
[DydxV4Network.V4Mainnet]: squidClientMainnet,
|
||||
[DydxV4Network.V4Staging]: squidClientTestnet,
|
||||
[DydxV4Network.V4Testnet2]: squidClientTestnet,
|
||||
};
|
||||
|
||||
class SquidRouter {
|
||||
private client: Squid;
|
||||
private network: DydxV4Network;
|
||||
|
||||
constructor() {
|
||||
this.network = DEFAULT_APP_ENVIRONMENT;
|
||||
this.client = SQUID_CLIENT_BY_NETWORK[this.network].getClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description the default destination chain/token and other hardcoded options
|
||||
*/
|
||||
get SQUID_ROUTE_DEFAULTS() {
|
||||
return {
|
||||
[DydxV4Network.V4Local]: SQUID_TESTNET_ROUTE_DEFAULTS,
|
||||
[DydxV4Network.V4Staging]: SQUID_TESTNET_ROUTE_DEFAULTS,
|
||||
[DydxV4Network.V4Testnet2]: SQUID_TESTNET_ROUTE_DEFAULTS,
|
||||
[DydxV4Network.V4Mainnet]: SQUID_MAINNET_ROUTE_DEFAULTS,
|
||||
}[this.network];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description updates the client to the correct 0xSquid environment based on which dYdX v4 network is selected.
|
||||
* @param network dYdX v4 network that is currently selected
|
||||
*/
|
||||
switchNetwork = (network: DydxV4Network) => {
|
||||
this.client = SQUID_CLIENT_BY_NETWORK[network].getClient();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description initializes the Squid client if it was not previously initialized (API request)
|
||||
*/
|
||||
initializeClient = () => SQUID_CLIENT_BY_NETWORK[this.network].init();
|
||||
|
||||
/**
|
||||
* @returns list of chains supported by Axelar
|
||||
*/
|
||||
getChains = (): ChainData[] => this.client.chains;
|
||||
|
||||
/**
|
||||
* @returns list of tokens supported by Axelar
|
||||
*/
|
||||
getTokens = (): TokenData[] => this.client.tokens;
|
||||
|
||||
/**
|
||||
* @param getRouteParams
|
||||
* @returns 0xSquid RouteData
|
||||
*/
|
||||
getRoute = async (getRouteParams: GetRoute): Promise<RouteResponse> => {
|
||||
const resp = await this.client.getRoute(getRouteParams);
|
||||
return resp;
|
||||
};
|
||||
|
||||
/**
|
||||
* @params {
|
||||
* @route the 0xSquid RouteData
|
||||
* @signer the EVM/Cosmos Signer used to sign the Axelar bridge
|
||||
* }
|
||||
* @returns transaction information after the route has been executed
|
||||
*/
|
||||
executeRoute = async ({ route, signer }: { route: RouteData; signer: any }) => {
|
||||
const tx = await this.client.executeRoute({
|
||||
signer,
|
||||
route,
|
||||
});
|
||||
|
||||
return await tx.wait();
|
||||
};
|
||||
}
|
||||
|
||||
const squidRouter = new SquidRouter();
|
||||
|
||||
export default squidRouter;
|
||||
+19
-12
@@ -38,6 +38,10 @@ export const WAGMI_SUPPORTED_CHAINS: Chain[] = [
|
||||
optimismGoerli,
|
||||
];
|
||||
|
||||
export const WAGMI_SUPPORTED_CHAIN_MAP = Object.fromEntries(
|
||||
WAGMI_SUPPORTED_CHAINS.map((supportedChain) => [supportedChain.id, supportedChain])
|
||||
);
|
||||
|
||||
const { chains, publicClient, webSocketPublicClient } = configureChains(WAGMI_SUPPORTED_CHAINS, [
|
||||
alchemyProvider({ apiKey: import.meta.env.VITE_ALCHEMY_API_KEY }),
|
||||
jsonRpcProvider({
|
||||
@@ -162,7 +166,13 @@ const createWalletConnect2ConnectorWithId = (walletconnect2Id: string) =>
|
||||
});
|
||||
|
||||
// Custom connector from wallet selection
|
||||
import { type WalletConnection, WalletConnectionType, type WalletType, walletConnectionTypes, wallets } from '@/constants/wallets';
|
||||
import {
|
||||
type WalletConnection,
|
||||
WalletConnectionType,
|
||||
type WalletType,
|
||||
walletConnectionTypes,
|
||||
wallets,
|
||||
} from '@/constants/wallets';
|
||||
|
||||
export const resolveWagmiConnector = ({
|
||||
walletType,
|
||||
@@ -174,15 +184,12 @@ export const resolveWagmiConnector = ({
|
||||
const walletConfig = wallets[walletType];
|
||||
const walletConnectionConfig = walletConnectionTypes[walletConnection.type];
|
||||
|
||||
return (
|
||||
walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider ?
|
||||
createInjectedConnectorWithProvider(walletConnection.provider)
|
||||
: walletConnection.type === WalletConnectionType.WalletConnect1 && walletConfig.walletconnect1Name ?
|
||||
createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
|
||||
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id ?
|
||||
createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
|
||||
: connectors.find(
|
||||
({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId
|
||||
)
|
||||
);
|
||||
return walletConnection.type === WalletConnectionType.InjectedEip1193 && walletConnection.provider
|
||||
? createInjectedConnectorWithProvider(walletConnection.provider)
|
||||
: walletConnection.type === WalletConnectionType.WalletConnect1 &&
|
||||
walletConfig.walletconnect1Name
|
||||
? createWalletConnect1ConnectorWithName(walletConfig.walletconnect1Name)
|
||||
: walletConnection.type === WalletConnectionType.WalletConnect2 && walletConfig.walletconnect2Id
|
||||
? createWalletConnect2ConnectorWithId(walletConfig.walletconnect2Id)
|
||||
: connectors.find(({ id }: { id: string }) => id === walletConnectionConfig.wagmiConnectorId);
|
||||
};
|
||||
|
||||
@@ -2,13 +2,11 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { AbacusApiStatus } from '@/constants/abacus';
|
||||
import { DialogTypes } from '@/constants/dialogs';
|
||||
import { isDydxV4Network } from '@/constants/networks';
|
||||
|
||||
import { setApiState, setSelectedNetwork } from '@/state/app';
|
||||
import { resetPerpetualsState } from '@/state/perpetuals';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import squidRouter from '@/lib/squidRouter';
|
||||
|
||||
import { openDialog } from './dialogs';
|
||||
import { getActiveDialog } from './dialogsSelectors';
|
||||
@@ -22,10 +20,6 @@ export default (store: any) => (next: any) => async (action: PayloadAction<any>)
|
||||
case setSelectedNetwork.type: {
|
||||
store.dispatch(resetPerpetualsState());
|
||||
abacusStateManager.switchNetwork(payload);
|
||||
|
||||
if (isDydxV4Network(payload)) {
|
||||
squidRouter.switchNetwork(payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case setApiState.type: {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const popoverMixins = {
|
||||
|
||||
${layoutMixins.spacedRow}
|
||||
grid-template-columns: 1fr auto;
|
||||
padding-right: 1rem;
|
||||
gap: 0;
|
||||
border: 1px solid var(--color-layer-6);
|
||||
`,
|
||||
|
||||
|
||||
@@ -72,16 +72,24 @@ export const AccountInfoConnectedState = () => {
|
||||
{!showHeader ? null : (
|
||||
<Styled.Header>
|
||||
<span>{stringGetter({ key: STRING_KEYS.ACCOUNT })}</span>
|
||||
|
||||
<Styled.Button
|
||||
state={{ isDisabled: !dydxAccounts }}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
|
||||
shape={ButtonShape.Pill}
|
||||
size={ButtonSize.XSmall}
|
||||
slotLeft={<Icon iconName={IconName.Transfer} />}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.MANAGE_FUNDS })}
|
||||
</Styled.Button>
|
||||
<Styled.TransferButtons>
|
||||
<Styled.Button
|
||||
state={{ isDisabled: !dydxAccounts }}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Withdraw }))}
|
||||
shape={ButtonShape.Pill}
|
||||
size={ButtonSize.XSmall}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
|
||||
</Styled.Button>
|
||||
<Styled.Button
|
||||
state={{ isDisabled: !dydxAccounts }}
|
||||
onClick={() => dispatch(openDialog({ type: DialogTypes.Deposit }))}
|
||||
shape={ButtonShape.Pill}
|
||||
size={ButtonSize.XSmall}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT })}
|
||||
</Styled.Button>
|
||||
</Styled.TransferButtons>
|
||||
</Styled.Header>
|
||||
)}
|
||||
<Styled.Stack>
|
||||
@@ -273,6 +281,11 @@ Styled.Header = styled.header`
|
||||
padding: 0 1.25rem;
|
||||
`;
|
||||
|
||||
Styled.TransferButtons = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: 1rem;
|
||||
`
|
||||
|
||||
Styled.ConnectedAccountInfoContainer = styled.div<{ $showHeader?: boolean }>`
|
||||
${layoutMixins.column}
|
||||
${layoutMixins.withOuterAndInnerBorders}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useBreakpoints, useStringGetter } from '@/hooks';
|
||||
|
||||
import { Dialog, DialogPlacement } from '@/components/Dialog';
|
||||
|
||||
import { DepositForm } from '@/views/forms/DepositForm';
|
||||
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
|
||||
import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
|
||||
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
@@ -32,12 +32,12 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
|
||||
>
|
||||
<Styled.Content>
|
||||
{showTestDeposit ? <TestnetDepositForm onDeposit={closeDialog} /> : <DepositForm />}
|
||||
{/* TODO TRCL-1693 - uncomment when deposit form is ready
|
||||
{/* TODO TRCL-1693 - uncomment when deposit form is ready */}
|
||||
{import.meta.env.MODE !== 'production' && (
|
||||
<Styled.TextToggle onClick={() => setShowTestDeposit(!showTestDeposit)}>
|
||||
{showTestDeposit ? 'Show deposit form (Under Construction)' : 'Show test faucet'}
|
||||
</Styled.TextToggle>
|
||||
)} */}
|
||||
)}
|
||||
</Styled.Content>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { GreenCheckCircle } from '@/components/GreenCheckCircle';
|
||||
import { Icon } from '@/components/Icon';
|
||||
import { Ring } from '@/components/Ring';
|
||||
|
||||
import { TestnetDepositForm } from '@/views/forms/TestnetDepositForm';
|
||||
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
|
||||
|
||||
import { track } from '@/lib/analytics';
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { useBreakpoints, useStringGetter } from '@/hooks';
|
||||
|
||||
import { Dialog, DialogPlacement } from '@/components/Dialog';
|
||||
|
||||
import { WithdrawForm } from '@/views/forms/AccountManagementForms/WithdrawForm';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
type ElementProps = {
|
||||
setIsOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const WithdrawDialog = ({ setIsOpen }: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { isTablet } = useBreakpoints();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen
|
||||
setIsOpen={setIsOpen}
|
||||
title={stringGetter({ key: STRING_KEYS.WITHDRAW })}
|
||||
placement={isTablet ? DialogPlacement.FullScreen : DialogPlacement.Default}
|
||||
>
|
||||
<Styled.Content>
|
||||
<WithdrawForm />
|
||||
</Styled.Content>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.TextToggle = styled.div`
|
||||
${layoutMixins.stickyFooter}
|
||||
color: var(--color-accent);
|
||||
cursor: pointer;
|
||||
|
||||
margin-top: auto;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.Content = styled.div`
|
||||
${layoutMixins.stickyArea0}
|
||||
--stickyArea0-bottomHeight: 2rem;
|
||||
--stickyArea0-bottomGap: 1rem;
|
||||
--stickyArea0-totalInsetBottom: 0.5rem;
|
||||
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 1rem;
|
||||
`;
|
||||
+28
-14
@@ -1,29 +1,43 @@
|
||||
import type { ChainData } from '@0xsquid/sdk';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { TransferInputChainResource, TransferType } from '@/constants/abacus';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
|
||||
import { WithLabel } from '@/components/WithLabel';
|
||||
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
import { popoverMixins } from '@/styles/popoverMixins';
|
||||
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
type ElementProps = {
|
||||
chains?: ChainData[];
|
||||
currentChain?: ChainData;
|
||||
setCurrentChain: (chain: ChainData) => void;
|
||||
label?: string;
|
||||
selectedChain?: TransferInputChainResource;
|
||||
onSelectChain: (chain: TransferInputChainResource) => void;
|
||||
};
|
||||
|
||||
export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }: ElementProps) => {
|
||||
export const ChainSelectMenu = ({
|
||||
label,
|
||||
selectedChain,
|
||||
onSelectChain,
|
||||
}: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { type, depositOptions, withdrawalOptions, resources } =
|
||||
useSelector(getTransferInputs, shallowEqual) || {};
|
||||
const chains =
|
||||
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
|
||||
|
||||
const chainItems = Object.values(chains).map((chain: ChainData) => ({
|
||||
value: chain.chainId.toString(),
|
||||
label: chain.chainName,
|
||||
onSelect: () => setCurrentChain(chain),
|
||||
slotBefore: <Styled.Img src={chain.chainIconURI} alt="" />,
|
||||
const chainItems = Object.values(chains).map((chain) => ({
|
||||
value: chain.type,
|
||||
label: chain.stringKey,
|
||||
onSelect: () => {
|
||||
const selectedChain = resources?.chainResources?.get(chain.type);
|
||||
selectedChain && onSelectChain(selectedChain);
|
||||
},
|
||||
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -35,12 +49,12 @@ export const ChainSelectMenu = ({ chains = [], currentChain, setCurrentChain }:
|
||||
items: chainItems,
|
||||
},
|
||||
]}
|
||||
label="Source"
|
||||
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
|
||||
>
|
||||
<Styled.ChainRow>
|
||||
{currentChain ? (
|
||||
{selectedChain ? (
|
||||
<>
|
||||
<Styled.Img src={currentChain?.chainIconURI} alt="" /> {currentChain?.chainName}
|
||||
<Styled.Img src={selectedChain?.iconUrl} alt="" /> {selectedChain?.chainName}
|
||||
</>
|
||||
) : (
|
||||
stringGetter({ key: STRING_KEYS.SELECT_CHAIN })
|
||||
@@ -67,4 +81,4 @@ Styled.ChainRow = styled.div`
|
||||
gap: 0.5rem;
|
||||
color: var(--color-text-2);
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
`;
|
||||
@@ -0,0 +1,305 @@
|
||||
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { type NumberFormatValues } from 'react-number-format';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
TransferInputField,
|
||||
TransferInputChainResource,
|
||||
TransferInputTokenResource,
|
||||
TransferType,
|
||||
} from '@/constants/abacus';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { StringGetterFunction, STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
|
||||
import { useDebounce, useStringGetter, useSquidRouter } from '@/hooks';
|
||||
import { useAccountBalance } from '@/hooks/useAccountBalance';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { AlertMessage } from '@/components/AlertMessage';
|
||||
import { Button } from '@/components/Button';
|
||||
import { DiffOutput } from '@/components/DiffOutput';
|
||||
import { FormInput } from '@/components/FormInput';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { InputType } from '@/components/Input';
|
||||
import { Link } from '@/components/Link';
|
||||
import { LoadingSpace } from '@/components/Loading/LoadingSpinner';
|
||||
import { OutputType } from '@/components/Output';
|
||||
import { Tag } from '@/components/Tag';
|
||||
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
import { ChainSelectMenu } from './ChainSelectMenu';
|
||||
import { TokenSelectMenu } from './TokenSelectMenu';
|
||||
|
||||
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
|
||||
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
type DepositFormProps = {
|
||||
onDeposit?: () => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { submitDeposit, axelarscanURL } = useSquidRouter();
|
||||
|
||||
const { requestPayload, token, chain, resources } = useSelector(getTransferInputs) || {};
|
||||
|
||||
// User inputs
|
||||
const sourceChain = useMemo(
|
||||
() => (chain ? resources?.chainResources?.get(chain) : undefined),
|
||||
[chain, resources]
|
||||
);
|
||||
|
||||
const sourceToken = useMemo(
|
||||
() => (token ? resources?.tokenResources?.get(token) : undefined),
|
||||
[token, resources]
|
||||
);
|
||||
|
||||
const [fromAmount, setFromAmount] = useState('');
|
||||
const [slippage, setSlippage] = useState(0.01); // 1% slippage
|
||||
const debouncedAmount = useDebounce<string>(fromAmount, 500);
|
||||
|
||||
// Async Data
|
||||
const [transactionHash, setTransactionHash] = useState<string>();
|
||||
|
||||
const { balance, queryStatus, isQueryFetching } = useAccountBalance({
|
||||
addressOrDenom: sourceToken?.address || undefined,
|
||||
assetSymbol: sourceToken?.symbol || undefined,
|
||||
chainId: sourceChain?.chainId || undefined,
|
||||
decimals: sourceToken?.decimals || undefined,
|
||||
rpc: sourceChain?.rpc || undefined,
|
||||
isCosmosChain: false,
|
||||
});
|
||||
|
||||
// BN
|
||||
const debouncedAmountBN = MustBigNumber(debouncedAmount);
|
||||
const balanceBN = MustBigNumber(balance);
|
||||
|
||||
useEffect(() => {
|
||||
const hasInvalidInput =
|
||||
debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0) || debouncedAmountBN.gte(balanceBN);
|
||||
|
||||
abacusStateManager.setTransferValue({
|
||||
value: hasInvalidInput ? 0 : debouncedAmount,
|
||||
field: TransferInputField.size,
|
||||
});
|
||||
|
||||
setError(null);
|
||||
}, [debouncedAmountBN.toNumber()]);
|
||||
|
||||
useEffect(() => {
|
||||
abacusStateManager.setTransferValue({
|
||||
value: TransferType.deposit.rawValue,
|
||||
field: TransferInputField.type,
|
||||
});
|
||||
|
||||
return () => {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) onError?.();
|
||||
}, [error]);
|
||||
|
||||
const onSelectChain = useCallback((chain: TransferInputChainResource) => {
|
||||
if (chain) {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
abacusStateManager.setTransferValue({
|
||||
field: TransferInputField.chain,
|
||||
value: chain.chainId,
|
||||
});
|
||||
setFromAmount('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSelectToken = useCallback((token: TransferInputTokenResource) => {
|
||||
if (token) {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
abacusStateManager.setTransferValue({
|
||||
field: TransferInputField.token,
|
||||
value: token.address,
|
||||
});
|
||||
setFromAmount('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onChangeAmount = useCallback(
|
||||
({ value }: NumberFormatValues) => {
|
||||
setFromAmount(value);
|
||||
},
|
||||
[setFromAmount]
|
||||
);
|
||||
|
||||
const onSetSlippage = useCallback(
|
||||
(newSlippage: number) => {
|
||||
setSlippage(newSlippage);
|
||||
// TODO: to be implemented via abacus
|
||||
},
|
||||
[setSlippage, debouncedAmount]
|
||||
);
|
||||
|
||||
const onClickMax = useCallback(() => {
|
||||
if (balance) {
|
||||
setFromAmount(balanceBN.toString());
|
||||
}
|
||||
}, [balance, setFromAmount]);
|
||||
|
||||
const amountInputReceipt = [
|
||||
{
|
||||
key: 'amount',
|
||||
label: (
|
||||
<span>
|
||||
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
|
||||
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
|
||||
</span>
|
||||
),
|
||||
value: (
|
||||
<DiffOutput
|
||||
type={OutputType.Asset}
|
||||
value={balance}
|
||||
newValue={balanceBN.minus(debouncedAmountBN).toString()}
|
||||
sign={NumberSign.Negative}
|
||||
hasInvalidNewValue={balanceBN.minus(debouncedAmountBN).isNegative()}
|
||||
withDiff={
|
||||
Boolean(fromAmount && balance) &&
|
||||
!debouncedAmountBN.isNaN() &&
|
||||
!debouncedAmountBN.isZero()
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (error) {
|
||||
return error?.message
|
||||
? stringGetter({
|
||||
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
|
||||
params: { ERROR_MESSAGE: error.message },
|
||||
})
|
||||
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
|
||||
}
|
||||
|
||||
if (fromAmount) {
|
||||
if (!sourceChain) {
|
||||
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
|
||||
} else if (!sourceToken) {
|
||||
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
|
||||
}
|
||||
}
|
||||
|
||||
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
|
||||
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [error, balance, sourceChain, fromAmount, sourceToken])
|
||||
|
||||
const isDisabled =
|
||||
Boolean(errorMessage) ||
|
||||
!sourceToken ||
|
||||
!sourceChain ||
|
||||
debouncedAmountBN.isNaN() ||
|
||||
debouncedAmountBN.isZero();
|
||||
|
||||
if (!resources) {
|
||||
return <LoadingSpace id="DepositForm" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Form
|
||||
onSubmit={async (e: FormEvent) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
const txHash = await submitDeposit(requestPayload);
|
||||
onDeposit?.();
|
||||
setTransactionHash(txHash);
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChainSelectMenu selectedChain={sourceChain || undefined} onSelectChain={onSelectChain} />
|
||||
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
|
||||
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
|
||||
<FormInput
|
||||
type={InputType.Number}
|
||||
onChange={onChangeAmount}
|
||||
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
|
||||
value={fromAmount}
|
||||
slotRight={
|
||||
<Button size={ButtonSize.XSmall} onClick={onClickMax}>
|
||||
{stringGetter({ key: STRING_KEYS.MAX })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Styled.WithDetailsReceipt>
|
||||
{errorMessage ? (
|
||||
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
|
||||
) : (
|
||||
transactionHash && (
|
||||
<AlertMessage type={AlertType.Success}>
|
||||
<Styled.TransactionInfo>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })}
|
||||
</Styled.TransactionInfo>
|
||||
<Styled.TransactionInfo>
|
||||
<Styled.Link href={`${axelarscanURL}/gmp/${transactionHash}`}>
|
||||
{stringGetter({ key: STRING_KEYS.VIEW_TRANSACTION })}
|
||||
<Icon iconName={IconName.LinkOut} />
|
||||
</Styled.Link>
|
||||
</Styled.TransactionInfo>
|
||||
</AlertMessage>
|
||||
)
|
||||
)}
|
||||
<DepositButtonAndReceipt
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
chainId={sourceChain?.chainId || undefined}
|
||||
setSlippage={onSetSlippage}
|
||||
slippage={slippage}
|
||||
sourceToken={sourceToken || undefined}
|
||||
/>
|
||||
</Styled.Form>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Form = styled.form`
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 1.5rem;
|
||||
|
||||
${layoutMixins.stickyArea1}
|
||||
min-height: calc(100% - var(--stickyArea0-bottomHeight));
|
||||
`;
|
||||
|
||||
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
`;
|
||||
|
||||
Styled.Link = styled(Link)`
|
||||
color: var(--color-accent);
|
||||
|
||||
&:visited {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.TransactionInfo = styled.span`
|
||||
${layoutMixins.row}
|
||||
`;
|
||||
+36
-48
@@ -1,4 +1,4 @@
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react';
|
||||
import { type Dispatch, useMemo, type SetStateAction, useState, type ReactNode } from 'react';
|
||||
import styled, { type AnyStyledComponent, css } from 'styled-components';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import type { RouteData, TokenData } from '@0xsquid/sdk';
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ButtonType,
|
||||
} from '@/constants/buttons';
|
||||
|
||||
import { TransferInputTokenResource } from '@/constants/abacus';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
|
||||
@@ -30,11 +31,11 @@ import { ToggleButton } from '@/components/ToggleButton';
|
||||
import { WithReceipt } from '@/components/WithReceipt';
|
||||
|
||||
import { getSubaccountBuyingPower, getSubaccountEquity } from '@/state/accountSelectors';
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
|
||||
import squidRouter from '@/lib/squidRouter';
|
||||
|
||||
import { SlippageEditor } from './SlippageEditor';
|
||||
import { SlippageEditor } from '../SlippageEditor';
|
||||
|
||||
type ElementProps = {
|
||||
isDisabled?: boolean;
|
||||
@@ -43,8 +44,9 @@ type ElementProps = {
|
||||
chainId?: string | number;
|
||||
setError?: Dispatch<SetStateAction<Error | undefined>>;
|
||||
slippage: number;
|
||||
slotError?: ReactNode;
|
||||
setSlippage: (slippage: number) => void;
|
||||
sourceToken?: TokenData;
|
||||
sourceToken?: TransferInputTokenResource;
|
||||
squidRoute?: RouteData;
|
||||
};
|
||||
|
||||
@@ -54,10 +56,9 @@ export const DepositButtonAndReceipt = ({
|
||||
slippage,
|
||||
setSlippage,
|
||||
sourceToken,
|
||||
squidRoute,
|
||||
|
||||
isDisabled,
|
||||
isLoading,
|
||||
slotError,
|
||||
}: ElementProps) => {
|
||||
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
|
||||
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
|
||||
@@ -78,35 +79,23 @@ export const DepositButtonAndReceipt = ({
|
||||
const { current: buyingPower, postOrder: newBuyingPower } =
|
||||
useSelector(getSubaccountBuyingPower, shallowEqual) || {};
|
||||
|
||||
const { gasCosts, feeCosts } = squidRoute?.estimate || {};
|
||||
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
|
||||
|
||||
const totalGasCosts =
|
||||
gasCosts?.reduce(
|
||||
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
|
||||
BIG_NUMBERS.ZERO
|
||||
) || BIG_NUMBERS.ZERO;
|
||||
const feeSubitems: DetailsItem[] = [];
|
||||
|
||||
const totalFeeCosts =
|
||||
feeCosts?.reduce(
|
||||
(acc: BigNumber, { amountUSD }: { amountUSD: string }) => acc.plus(MustBigNumber(amountUSD)),
|
||||
BIG_NUMBERS.ZERO
|
||||
) || BIG_NUMBERS.ZERO;
|
||||
|
||||
const totalFees = totalGasCosts.plus(totalFeeCosts);
|
||||
|
||||
const feeSubitems: DetailsItem[] = feeCosts
|
||||
? feeCosts.map(({ amountUSD, name }: { amountUSD: string; name: string }) => ({
|
||||
key: name,
|
||||
label: <span>{name}</span>,
|
||||
value: <Output type={OutputType.Fiat} value={amountUSD} />,
|
||||
}))
|
||||
: [];
|
||||
|
||||
if (gasCosts) {
|
||||
if (typeof summary?.gasFee === 'number') {
|
||||
feeSubitems.push({
|
||||
key: 'gas-fees',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
|
||||
value: <Output type={OutputType.Fiat} value={totalGasCosts} />,
|
||||
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof summary?.bridgeFee === 'number') {
|
||||
feeSubitems.push({
|
||||
key: 'bridge-fees',
|
||||
label: <span>Bridge Fee</span>,
|
||||
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,9 +117,9 @@ export const DepositButtonAndReceipt = ({
|
||||
<DiffOutput
|
||||
type={OutputType.Fiat}
|
||||
value={equity}
|
||||
newValue={newEquity}
|
||||
newValue={newEquity} // using toAmountUSD as a proxy for equity until Abacus supports accounts with no funds.
|
||||
sign={NumberSign.Positive}
|
||||
withDiff={Boolean(newEquity) && equity !== newEquity}
|
||||
withDiff={equity !== newEquity}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -154,27 +143,20 @@ export const DepositButtonAndReceipt = ({
|
||||
{
|
||||
key: 'exchange-rate',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
|
||||
value: sourceToken && squidRoute && (
|
||||
value: typeof summary?.exchangeRate === 'number' && (
|
||||
<Styled.ExchangeRate>
|
||||
<Output
|
||||
type={OutputType.Asset}
|
||||
value={1}
|
||||
fractionDigits={0}
|
||||
tag={<Tag>{sourceToken?.symbol}</Tag>}
|
||||
/>
|
||||
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag={sourceToken?.symbol} />
|
||||
=
|
||||
<Output
|
||||
type={OutputType.Asset}
|
||||
value={squidRoute?.estimate?.exchangeRate}
|
||||
tag={<Tag>{squidRouter.SQUID_ROUTE_DEFAULTS.toToken.toUpperCase()}</Tag>}
|
||||
/>
|
||||
<Output type={OutputType.Asset} value={summary?.exchangeRate} tag="USDC" />
|
||||
</Styled.ExchangeRate>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'total-fees',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
|
||||
value: squidRoute && <Output type={OutputType.Fiat} value={totalFees} />,
|
||||
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
|
||||
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
|
||||
),
|
||||
subitems: feeSubitems,
|
||||
},
|
||||
{
|
||||
@@ -191,6 +173,7 @@ export const DepositButtonAndReceipt = ({
|
||||
];
|
||||
|
||||
const isFormValid = !isDisabled && !isEditingSlippage;
|
||||
const isLoading = isFormValid && !requestPayload;
|
||||
|
||||
return (
|
||||
<Styled.WithReceipt
|
||||
@@ -212,17 +195,22 @@ export const DepositButtonAndReceipt = ({
|
||||
</Styled.DetailButtons>
|
||||
</Styled.CollapsibleDetails>
|
||||
}
|
||||
slotError={slotError}
|
||||
>
|
||||
{!isMatchingNetwork ? (
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
onClick={switchNetwork}
|
||||
state={{ isLoading: isSwitchingNetwork || isLoading }}
|
||||
state={{ isLoading: isSwitchingNetwork }}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.SWITCH_NETWORK })}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type={ButtonType.Submit} state={{ isDisabled: !isFormValid, isLoading }}>
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
type={ButtonType.Submit}
|
||||
state={{ isDisabled: !isFormValid, isLoading }}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT_FUNDS })}
|
||||
</Button>
|
||||
)}
|
||||
@@ -234,7 +222,7 @@ const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.ExchangeRate = styled.span`
|
||||
${layoutMixins.row}
|
||||
gap: 1ch;
|
||||
gap: 0.5ch;
|
||||
`;
|
||||
|
||||
Styled.WithReceipt = styled(WithReceipt)`
|
||||
+4
-5
@@ -12,6 +12,7 @@ import { Icon, IconName } from '@/components/Icon';
|
||||
import { Input, InputType } from '@/components/Input';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
import { ToggleGroup } from '@/components/ToggleGroup';
|
||||
import { FormInput } from '@/components/FormInput';
|
||||
|
||||
enum EditorState {
|
||||
Viewing = 'Viewing',
|
||||
@@ -115,7 +116,7 @@ export const SlippageEditor = ({ slippage, setIsEditing, setSlippage }: Slippage
|
||||
</Styled.SlippageInput>
|
||||
),
|
||||
[EditorState.Editing]: (
|
||||
<Styled.Input
|
||||
<Styled.FormInput
|
||||
type={InputType.Percent}
|
||||
value={slippageInputValue}
|
||||
tabIndex={0}
|
||||
@@ -151,10 +152,8 @@ Styled.SlippageInput = styled.div`
|
||||
border-radius: 0.5em;
|
||||
`;
|
||||
|
||||
Styled.Input = styled(Input)`
|
||||
// TODO(@aforaleka): replace with input container
|
||||
--input-height: 1.5rem;
|
||||
--input-backgroundColor: var(--color-layer-5);
|
||||
Styled.FormInput = styled(FormInput)`
|
||||
--form-input-height: 1.5rem;
|
||||
|
||||
input {
|
||||
text-align: end;
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { ChainData, TokenData } from '@0xsquid/sdk';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { TransferInputTokenResource, TransferType } from '@/constants/abacus';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
|
||||
import { Tag } from '@/components/Tag';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
type ElementProps = {
|
||||
selectedToken?: TransferInputTokenResource;
|
||||
onSelectToken: (token: TransferInputTokenResource) => void;
|
||||
};
|
||||
|
||||
export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { type, depositOptions, withdrawalOptions, resources } =
|
||||
useSelector(getTransferInputs, shallowEqual) || {};
|
||||
const tokens =
|
||||
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.assets?.toArray() || [];
|
||||
|
||||
const tokenItems = Object.values(tokens).map((token) => ({
|
||||
value: token.type,
|
||||
label: token.stringKey,
|
||||
onSelect: () => {
|
||||
const selectedToken = resources?.tokenResources?.get(token.type);
|
||||
selectedToken && onSelectToken(selectedToken);
|
||||
},
|
||||
slotBefore: <Styled.Img src={token.iconUrl} alt="" />,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SearchSelectMenu
|
||||
items={[
|
||||
{
|
||||
group: 'assets',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
|
||||
items: tokenItems,
|
||||
},
|
||||
]}
|
||||
label={stringGetter({ key: STRING_KEYS.ASSET })}
|
||||
withReceiptItems={[
|
||||
{
|
||||
key: 'swap',
|
||||
label: stringGetter({ key: STRING_KEYS.SWAP }),
|
||||
value: selectedToken && (
|
||||
<>
|
||||
<Tag>{type === TransferType.deposit ? selectedToken?.symbol : 'USDC'}</Tag>
|
||||
<Styled.Icon iconName={IconName.Arrow} />
|
||||
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
|
||||
</>
|
||||
),
|
||||
tooltip: 'swap',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Styled.AssetRow>
|
||||
{selectedToken ? (
|
||||
<>
|
||||
<Styled.Img src={selectedToken?.iconUrl} alt="" /> {selectedToken?.name}{' '}
|
||||
<Tag>{selectedToken?.symbol}</Tag>
|
||||
</>
|
||||
) : (
|
||||
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
|
||||
)}
|
||||
</Styled.AssetRow>
|
||||
</SearchSelectMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.AssetRow = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: 0.5rem;
|
||||
color: var(--color-text-2);
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
|
||||
Styled.Img = styled.img`
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
Styled.Icon = styled(Icon)`
|
||||
height: 0.5rem;
|
||||
`;
|
||||
@@ -0,0 +1,309 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ChangeEvent, FormEvent } from 'react';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import type { NumberFormatValues } from 'react-number-format';
|
||||
import type { TokenData, ChainData } from '@0xsquid/sdk';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import { parseUnits } from 'viem';
|
||||
|
||||
import { TransferInputField, TransferInputChainResource, TransferInputTokenResource, TransferType } from '@/constants/abacus';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { ButtonAction, ButtonSize, ButtonType } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
|
||||
import { useAccounts, useDebounce, useStringGetter, useSquidRouter } from '@/hooks';
|
||||
import { SQUID_WITHDRAW_ROUTE_DEFAULTS } from '@/hooks/useSquidRouter';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { AlertMessage } from '@/components/AlertMessage';
|
||||
import { Button } from '@/components/Button';
|
||||
import { DiffOutput } from '@/components/DiffOutput';
|
||||
import { FormInput } from '@/components/FormInput';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { InputType } from '@/components/Input';
|
||||
import { Link } from '@/components/Link';
|
||||
import { OutputType } from '@/components/Output';
|
||||
import { Tag } from '@/components/Tag';
|
||||
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
|
||||
|
||||
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu';
|
||||
|
||||
import { getSubaccount } from '@/state/accountSelectors';
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
import { TokenSelectMenu } from './TokenSelectMenu';
|
||||
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
|
||||
|
||||
export const WithdrawForm = () => {
|
||||
const stringGetter = useStringGetter();
|
||||
const { dydxAddress } = useAccounts();
|
||||
const { freeCollateral } = useSelector(getSubaccount, shallowEqual) || {};
|
||||
|
||||
const {
|
||||
error,
|
||||
setError,
|
||||
isLoading,
|
||||
|
||||
axelarscanURL,
|
||||
} = useSquidRouter({ allChains: true });
|
||||
|
||||
// User input
|
||||
const [withdrawAmount, setWithdrawAmount] = useState('');
|
||||
const [slippage, setSlippage] = useState(0.001); // 0.1% slippage
|
||||
const debouncedAmount = useDebounce<string>(withdrawAmount, 500);
|
||||
|
||||
const { requestPayload, token, chain, address: toAddress, resources } = useSelector(getTransferInputs) || {};
|
||||
|
||||
const toChain = useMemo(
|
||||
() => (chain ? resources?.chainResources?.get(chain) : undefined),
|
||||
[chain, resources]
|
||||
);
|
||||
const toToken = useMemo(
|
||||
() => (token ? resources?.tokenResources?.get(token) : undefined),
|
||||
[token, resources]
|
||||
);
|
||||
|
||||
// Async Data
|
||||
const [transactionHash, setTransactionHash] = useState<string>();
|
||||
|
||||
const debouncedAmountBN = MustBigNumber(debouncedAmount);
|
||||
const withdrawAmountBN = MustBigNumber(withdrawAmount);
|
||||
const freeCollateralBN = MustBigNumber(freeCollateral?.current);
|
||||
|
||||
useEffect(() => {
|
||||
abacusStateManager.setTransferValue({
|
||||
value: TransferType.withdrawal.rawValue,
|
||||
field: TransferInputField.type,
|
||||
});
|
||||
|
||||
return () => {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const hasInvalidInput = debouncedAmountBN.isNaN() || debouncedAmountBN.lte(0);
|
||||
|
||||
abacusStateManager.setTransferValue({
|
||||
value: hasInvalidInput ? 0 : debouncedAmount,
|
||||
field: TransferInputField.usdcSize,
|
||||
});
|
||||
|
||||
setError(null);
|
||||
}, [debouncedAmountBN.toNumber()]);
|
||||
|
||||
/**
|
||||
* @todo Withdrawing involves two steps.
|
||||
* 1. MsgCreateTransfer for withdrawing from the subaccount.
|
||||
* 2. MsgTransfer to IBC transfer the funds to the destination chain. 0xSquid will provide the IBC path instructions.
|
||||
*/
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
// const withdraw = await dydxClient.withdrawFromSubaccount(debouncedAmountBN.toNumber(), 0);
|
||||
// const txHash = await executeRoute();
|
||||
// @ts-ignore execute route returns Promise<string> instead of Promise<TransactionReponse>
|
||||
// setTransactionHash(txHash);
|
||||
},
|
||||
[setTransactionHash]
|
||||
);
|
||||
|
||||
const onChangeAddress = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
abacusStateManager.setTransferValue({
|
||||
field: TransferInputField.address,
|
||||
value: e.target.value,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onChangeAmount = useCallback(
|
||||
({ value }: NumberFormatValues) => {
|
||||
setWithdrawAmount(value);
|
||||
},
|
||||
[setWithdrawAmount]
|
||||
);
|
||||
|
||||
const onSetSlippage = useCallback(
|
||||
(newSlippage: number) => {
|
||||
setSlippage(newSlippage);
|
||||
|
||||
// if (MustBigNumber(newSlippage).gt(0) && debouncedAmountBN.gt(0)) {
|
||||
// fetchRoute({ newAmount: debouncedAmount, newSlippage });
|
||||
// }
|
||||
},
|
||||
[setSlippage, debouncedAmount]
|
||||
);
|
||||
|
||||
const onClickMax = useCallback(() => {
|
||||
setWithdrawAmount(freeCollateralBN.toString());
|
||||
}, [freeCollateral?.current, setWithdrawAmount]);
|
||||
|
||||
|
||||
const onSelectChain = useCallback((chain: TransferInputChainResource) => {
|
||||
if (chain) {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
abacusStateManager.setTransferValue({
|
||||
field: TransferInputField.chain,
|
||||
value: chain.chainId,
|
||||
});
|
||||
setWithdrawAmount('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onSelectToken = useCallback(
|
||||
(token: TransferInputTokenResource) => {
|
||||
if (token) {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
abacusStateManager.setTransferValue({
|
||||
field: TransferInputField.token,
|
||||
value: token.address,
|
||||
});
|
||||
setWithdrawAmount('');
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const amountInputReceipt = [
|
||||
{
|
||||
key: 'freeCollateral',
|
||||
label: (
|
||||
<span>
|
||||
{stringGetter({ key: STRING_KEYS.FREE_COLLATERAL })} <Tag>USDC</Tag>
|
||||
</span>
|
||||
),
|
||||
value: (
|
||||
<Styled.DiffOutput
|
||||
type={OutputType.Fiat}
|
||||
value={freeCollateral?.current}
|
||||
newValue={freeCollateral?.postOrder}
|
||||
sign={NumberSign.Negative}
|
||||
hasInvalidNewValue={withdrawAmountBN.minus(freeCollateralBN).isNegative()}
|
||||
withDiff={
|
||||
Boolean(withdrawAmount) && !debouncedAmountBN.isNaN() && !debouncedAmountBN.isZero()
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
let errorMessage = !toAddress ? 'Please enter a destination address' : undefined;
|
||||
|
||||
const isDisabled = !!errorMessage ||
|
||||
!toToken ||
|
||||
!toChain ||
|
||||
debouncedAmountBN.isNaN() ||
|
||||
debouncedAmountBN.isZero();
|
||||
|
||||
return (
|
||||
<Styled.Form onSubmit={onSubmit}>
|
||||
<Styled.DestinationRow>
|
||||
<FormInput
|
||||
type={InputType.Text}
|
||||
placeholder={stringGetter({ key: STRING_KEYS.ADDRESS })}
|
||||
onChange={onChangeAddress}
|
||||
value={toAddress || ''}
|
||||
label={stringGetter({ key: STRING_KEYS.ADDRESS })}
|
||||
/>
|
||||
<ChainSelectMenu
|
||||
label={stringGetter({ key: STRING_KEYS.NETWORK })}
|
||||
selectedChain={toChain || undefined}
|
||||
onSelectChain={onSelectChain}
|
||||
/>
|
||||
</Styled.DestinationRow>
|
||||
<TokenSelectMenu
|
||||
selectedToken={toToken || undefined}
|
||||
onSelectToken={onSelectToken}
|
||||
/>
|
||||
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
|
||||
<FormInput
|
||||
type={InputType.Number}
|
||||
onChange={onChangeAmount}
|
||||
value={withdrawAmount}
|
||||
label={stringGetter({ key: STRING_KEYS.AMOUNT })}
|
||||
slotRight={
|
||||
<Button
|
||||
type={ButtonType.Button}
|
||||
action={ButtonAction.Secondary}
|
||||
size={ButtonSize.XSmall}
|
||||
onClick={onClickMax}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.MAX })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Styled.WithDetailsReceipt>
|
||||
{errorMessage ? (
|
||||
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
|
||||
) : (
|
||||
transactionHash && (
|
||||
<AlertMessage type={AlertType.Success}>
|
||||
<Styled.TransactionInfo>
|
||||
{stringGetter({ key: STRING_KEYS.DEPOSIT_IN_PROGRESS })}
|
||||
</Styled.TransactionInfo>
|
||||
<Styled.TransactionInfo>
|
||||
<Styled.Link href={`${axelarscanURL}/gmp/${transactionHash}`}>
|
||||
{stringGetter({ key: STRING_KEYS.VIEW_TRANSACTION })}
|
||||
<Icon iconName={IconName.LinkOut} />
|
||||
</Styled.Link>
|
||||
</Styled.TransactionInfo>
|
||||
</AlertMessage>
|
||||
)
|
||||
)}
|
||||
<WithdrawButtonAndReceipt
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading}
|
||||
setError={undefined}
|
||||
setSlippage={onSetSlippage}
|
||||
slippage={slippage}
|
||||
withdrawChain={toChain || undefined}
|
||||
withdrawToken={toToken || undefined}
|
||||
/>
|
||||
</Styled.Form>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.DiffOutput = styled(DiffOutput)`
|
||||
--diffOutput-valueWithDiff-fontSize: 1em;
|
||||
`;
|
||||
|
||||
Styled.Form = styled.form`
|
||||
min-height: calc(100% - var(--stickyArea0-bottomHeight));
|
||||
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 1.25rem;
|
||||
|
||||
${layoutMixins.stickyArea1}
|
||||
`;
|
||||
|
||||
Styled.DestinationRow = styled.div`
|
||||
${layoutMixins.spacedRow}
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
`;
|
||||
|
||||
Styled.Link = styled(Link)`
|
||||
color: var(--color-accent);
|
||||
|
||||
&:visited {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
`;
|
||||
|
||||
Styled.TransactionInfo = styled.span`
|
||||
${layoutMixins.row}
|
||||
`;
|
||||
@@ -0,0 +1,249 @@
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react';
|
||||
import { shallowEqual, useSelector } from 'react-redux';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import { ChainType } from '@0xsquid/sdk';
|
||||
import type { ChainData, CosmosChain, RouteData, TokenData } from '@0xsquid/sdk';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { parseUnits } from 'viem';
|
||||
|
||||
import { TransferInputChainResource, TransferInputTokenResource } from '@/constants/abacus';
|
||||
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
import { useAccountBalance } from '@/hooks/useAccountBalance';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { Button } from '@/components/Button';
|
||||
|
||||
import { Details, DetailsItem } from '@/components/Details';
|
||||
import { DiffOutput } from '@/components/DiffOutput';
|
||||
import { Icon, IconName } from '@/components/Icon';
|
||||
import { Output, OutputType } from '@/components/Output';
|
||||
import { Tag } from '@/components/Tag';
|
||||
import { ToggleButton } from '@/components/ToggleButton';
|
||||
import { WithReceipt } from '@/components/WithReceipt';
|
||||
|
||||
import { BIG_NUMBERS, MustBigNumber } from '@/lib/numbers';
|
||||
|
||||
import { SlippageEditor } from '../SlippageEditor';
|
||||
|
||||
import { getSubaccount } from '@/state/accountSelectors';
|
||||
import { getTransferInputs } from '@/state/inputsSelectors';
|
||||
|
||||
type ElementProps = {
|
||||
setError?: Dispatch<SetStateAction<Error | undefined>>;
|
||||
setSlippage: (slippage: number) => void;
|
||||
|
||||
slippage: number;
|
||||
withdrawChain?: TransferInputChainResource;
|
||||
withdrawToken?: TransferInputTokenResource;
|
||||
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const WithdrawButtonAndReceipt = ({
|
||||
setError,
|
||||
setSlippage,
|
||||
|
||||
slippage,
|
||||
withdrawChain,
|
||||
withdrawToken,
|
||||
|
||||
isDisabled,
|
||||
isLoading,
|
||||
}: ElementProps) => {
|
||||
const [showFeeBreakdown, setShowFeeBreakdown] = useState(false);
|
||||
const [isEditingSlippage, setIsEditingSlipapge] = useState(false);
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const { balance, queryStatus } = useAccountBalance({
|
||||
addressOrDenom: withdrawToken?.address || undefined,
|
||||
assetSymbol: withdrawToken?.symbol || undefined,
|
||||
chainId: withdrawChain?.chainId || undefined,
|
||||
decimals: withdrawToken?.decimals || undefined,
|
||||
rpc: withdrawChain?.rpc || undefined,
|
||||
isCosmosChain: false,
|
||||
});
|
||||
|
||||
const balanceBN = MustBigNumber(balance);
|
||||
// const newBalance =
|
||||
// // toAmountMin && withdrawToken && parseUnits(toAmountMin, withdrawToken.decimals).toString();
|
||||
|
||||
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
|
||||
const { summary, requestPayload } = useSelector(getTransferInputs) || {};
|
||||
|
||||
const feeSubitems: DetailsItem[] = [];
|
||||
|
||||
if (typeof summary?.gasFee === 'number') {
|
||||
feeSubitems.push({
|
||||
key: 'gas-fees',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.GAS_FEE })}</span>,
|
||||
value: <Output type={OutputType.Fiat} value={summary?.gasFee} />,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof summary?.bridgeFee === 'number') {
|
||||
feeSubitems.push({
|
||||
key: 'bridge-fees',
|
||||
label: <span>Bridge Fee</span>,
|
||||
value: <Output type={OutputType.Fiat} value={summary?.bridgeFee} />,
|
||||
});
|
||||
}
|
||||
|
||||
const hasSubitems = feeSubitems.length > 0;
|
||||
|
||||
const showSubitemsToggle = showFeeBreakdown
|
||||
? stringGetter({ key: STRING_KEYS.HIDE_ALL_DETAILS })
|
||||
: stringGetter({ key: STRING_KEYS.SHOW_ALL_DETAILS });
|
||||
|
||||
const submitButtonReceipt = [
|
||||
{
|
||||
key: 'total-fees',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.TOTAL_FEES })}</span>,
|
||||
value: typeof summary?.bridgeFee === 'number' && typeof summary?.gasFee === 'number' && (
|
||||
<Output type={OutputType.Fiat} value={summary?.bridgeFee + summary?.gasFee} />
|
||||
),
|
||||
subitems: feeSubitems,
|
||||
},
|
||||
{
|
||||
key: 'leverage',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.LEVERAGE })}</span>,
|
||||
value: (
|
||||
<Styled.DiffOutput
|
||||
type={OutputType.Multiple}
|
||||
value={leverage?.current}
|
||||
newValue={leverage?.postOrder}
|
||||
sign={NumberSign.Negative}
|
||||
withDiff={Boolean(leverage?.current && leverage.current !== leverage?.postOrder)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'exchange-rate',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
|
||||
value: withdrawToken && (
|
||||
<Styled.ExchangeRate>
|
||||
<Output type={OutputType.Asset} value={1} fractionDigits={0} tag="USDC" />
|
||||
=
|
||||
<Output
|
||||
type={OutputType.Asset}
|
||||
value={summary?.exchangeRate}
|
||||
tag={withdrawToken?.symbol}
|
||||
/>
|
||||
</Styled.ExchangeRate>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'slippage',
|
||||
label: <span>{stringGetter({ key: STRING_KEYS.SLIPPAGE })}</span>,
|
||||
value: (
|
||||
<SlippageEditor
|
||||
slippage={slippage}
|
||||
setIsEditing={setIsEditingSlipapge}
|
||||
setSlippage={setSlippage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// key: 'wallet',
|
||||
// label: (
|
||||
// <span>
|
||||
// {stringGetter({ key: STRING_KEYS.WALLET })}{' '}
|
||||
// {withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>}
|
||||
// </span>
|
||||
// ),
|
||||
// value: (
|
||||
// <Styled.DiffOutput
|
||||
// type={OutputType.Asset}
|
||||
// value={balanceBN?.toString()}
|
||||
// newValue={newBalance}
|
||||
// sign={NumberSign.Negative}
|
||||
// withDiff={Boolean(balance !== newBalance)}
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
];
|
||||
|
||||
return (
|
||||
<Styled.WithReceipt
|
||||
slotReceipt={
|
||||
<Styled.CollapsibleDetails>
|
||||
<Styled.Details showSubitems={showFeeBreakdown} items={submitButtonReceipt} />
|
||||
{hasSubitems && (
|
||||
<Styled.DetailButtons>
|
||||
<Styled.ToggleButton
|
||||
shape={ButtonShape.Pill}
|
||||
size={ButtonSize.XSmall}
|
||||
state={{ isDisabled: !hasSubitems }}
|
||||
isPressed={showFeeBreakdown}
|
||||
onPressedChanged={() => setShowFeeBreakdown(!showFeeBreakdown)}
|
||||
slotLeft={<Icon iconName={IconName.Caret} />}
|
||||
>
|
||||
{showSubitemsToggle}
|
||||
</Styled.ToggleButton>
|
||||
</Styled.DetailButtons>
|
||||
)}
|
||||
</Styled.CollapsibleDetails>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
action={ButtonAction.Primary}
|
||||
type={ButtonType.Submit}
|
||||
state={{
|
||||
isDisabled: isEditingSlippage || isDisabled || queryStatus === 'error',
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
{stringGetter({ key: STRING_KEYS.WITHDRAW })}
|
||||
</Button>
|
||||
</Styled.WithReceipt>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.DiffOutput = styled(DiffOutput)`
|
||||
--diffOutput-valueWithDiff-fontSize: 1em;
|
||||
`;
|
||||
|
||||
Styled.ExchangeRate = styled.span`
|
||||
${layoutMixins.row}
|
||||
gap: 0.5ch;
|
||||
`;
|
||||
|
||||
Styled.WithReceipt = styled(WithReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
`;
|
||||
|
||||
Styled.CollapsibleDetails = styled.div`
|
||||
${layoutMixins.column}
|
||||
padding: 0.375rem 1rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
Styled.Details = styled(Details)`
|
||||
font-size: 0.8125em;
|
||||
`;
|
||||
|
||||
Styled.DetailButtons = styled.div`
|
||||
${layoutMixins.spacedRow}
|
||||
`;
|
||||
|
||||
Styled.ToggleButton = styled(ToggleButton)`
|
||||
--button-toggle-off-backgroundColor: transparent;
|
||||
--button-toggle-on-backgroundColor: transparent;
|
||||
--button-toggle-on-textColor: var(--color-text-0);
|
||||
|
||||
svg {
|
||||
width: 0.875em;
|
||||
height: 0.875em;
|
||||
}
|
||||
|
||||
&[data-state='on'] {
|
||||
svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,428 +0,0 @@
|
||||
import { type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
import type { ChainData, CosmosChain, RouteData, TokenData } from '@0xsquid/sdk';
|
||||
import { type NumberFormatValues } from 'react-number-format';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { parseUnits } from 'ethers';
|
||||
|
||||
import { TransferInputField } from '@/constants/abacus';
|
||||
import { AlertType } from '@/constants/alerts';
|
||||
import { ButtonSize } from '@/constants/buttons';
|
||||
import { StringGetterFunction, STRING_KEYS } from '@/constants/localization';
|
||||
import { NumberSign } from '@/constants/numbers';
|
||||
import { CLIENT_NETWORK_CONFIGS, type DydxV4Network } from '@/constants/networks';
|
||||
|
||||
import { useAccounts, useDydxClient, useStringGetter } from '@/hooks';
|
||||
import { useAccountBalance } from '@/hooks/useAccountBalance';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { AlertMessage } from '@/components/AlertMessage';
|
||||
import { Button } from '@/components/Button';
|
||||
import { DiffOutput } from '@/components/DiffOutput';
|
||||
import { FormInput } from '@/components/FormInput';
|
||||
import { InputType } from '@/components/Input';
|
||||
import { Link } from '@/components/Link';
|
||||
import { OutputType } from '@/components/Output';
|
||||
import { Tag } from '@/components/Tag';
|
||||
import { WithLabel } from '@/components/WithLabel';
|
||||
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
|
||||
|
||||
import abacusStateManager from '@/lib/abacus';
|
||||
import { convertBech32Address } from '@/lib/addressUtils';
|
||||
import { MustBigNumber } from '@/lib/numbers';
|
||||
import squidRouter from '@/lib/squidRouter';
|
||||
|
||||
import { useSquidData } from './DepositForm/useSquidData';
|
||||
import { ChainSelectMenu } from './DepositForm/ChainSelectMenu';
|
||||
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
|
||||
import { TokenSelectMenu } from './DepositForm/TokenSelectMenu';
|
||||
import { log } from '@/lib/telemetry';
|
||||
|
||||
import { getSelectedNetwork } from '@/state/appSelectors';
|
||||
|
||||
type DepositFormProps = {
|
||||
onDeposit?: () => void;
|
||||
onError?: () => void;
|
||||
};
|
||||
|
||||
// debounced function should be outside our functional component
|
||||
const debouncedGetRoute = debounce(
|
||||
async ({
|
||||
amount,
|
||||
currentChain,
|
||||
cosmosAddress,
|
||||
fromAmount,
|
||||
setAbacusTransferInput,
|
||||
setError,
|
||||
setIsLoading,
|
||||
setSquidRoute,
|
||||
slippage,
|
||||
sourceToken,
|
||||
}) => {
|
||||
if (!currentChain || !sourceToken || !fromAmount || !cosmosAddress) return;
|
||||
setIsLoading(true);
|
||||
setSquidRoute(undefined);
|
||||
setError(undefined);
|
||||
|
||||
const params = {
|
||||
fromChain: currentChain.chainId,
|
||||
fromToken: sourceToken.address,
|
||||
fromAmount: parseUnits(amount, sourceToken.decimals).toString(),
|
||||
toAddress: convertBech32Address({ address: cosmosAddress, bech32Prefix: 'osmo' }), // the recipient of the trade
|
||||
slippage: slippage * 100,
|
||||
...squidRouter.SQUID_ROUTE_DEFAULTS,
|
||||
};
|
||||
|
||||
try {
|
||||
const { route } = await squidRouter.getRoute(params);
|
||||
setSquidRoute(route);
|
||||
setAbacusTransferInput(route.estimate.toAmountUSD);
|
||||
} catch (error) {
|
||||
log('DepositForm/getRoute', error);
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ trailing: true }
|
||||
);
|
||||
|
||||
const getDepositFormErrorMessage = ({
|
||||
error,
|
||||
balance,
|
||||
currentChain,
|
||||
fromAmount,
|
||||
sourceToken,
|
||||
stringGetter,
|
||||
}: {
|
||||
error?: any;
|
||||
balance?: string;
|
||||
currentChain?: ChainData;
|
||||
fromAmount: string;
|
||||
sourceToken?: TokenData;
|
||||
stringGetter: StringGetterFunction;
|
||||
}): string | undefined => {
|
||||
if (error) {
|
||||
return error?.message
|
||||
? stringGetter({
|
||||
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
|
||||
params: { ERROR_MESSAGE: error.message },
|
||||
})
|
||||
: stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG });
|
||||
}
|
||||
|
||||
if (fromAmount) {
|
||||
if (!currentChain) {
|
||||
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_CHAIN });
|
||||
} else if (!sourceToken) {
|
||||
return stringGetter({ key: STRING_KEYS.MUST_SPECIFY_ASSET });
|
||||
}
|
||||
}
|
||||
|
||||
if (MustBigNumber(fromAmount).gt(MustBigNumber(balance))) {
|
||||
return stringGetter({ key: STRING_KEYS.DEPOSIT_MORE_THAN_BALANCE });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const { dydxAddress: cosmosAddress, signerWagmi, signerGraz } = useAccounts();
|
||||
|
||||
// const { networkConfig } = useDydxClient();
|
||||
const selectedNetwork = useSelector(getSelectedNetwork);
|
||||
const evmChainId = Number(CLIENT_NETWORK_CONFIGS[selectedNetwork as DydxV4Network].ethereumChainId);
|
||||
|
||||
const { chains, tokens } = useSquidData(signerGraz ? 'cosmos' : 'evm');
|
||||
|
||||
// User inputs
|
||||
const defaultChain: ChainData | undefined = useMemo(
|
||||
() => chains?.find(({ chainId }) => chainId === evmChainId) ?? undefined,
|
||||
[chains]
|
||||
);
|
||||
const [currentChain, setCurrentChain] = useState<ChainData | undefined>(defaultChain);
|
||||
|
||||
const defaultToken: TokenData | undefined = useMemo(
|
||||
() => tokens?.filter(({ chainId }) => chainId === currentChain?.chainId)?.[0] ?? undefined,
|
||||
[currentChain]
|
||||
);
|
||||
const [sourceToken, setSourceToken] = useState<TokenData | undefined>(defaultToken);
|
||||
const [fromAmount, setFromAmount] = useState('');
|
||||
const [slippage, setSlippage] = useState(0.001); // 0.1% slippage
|
||||
|
||||
// Async Data
|
||||
const [squidRoute, setSquidRoute] = useState<RouteData>();
|
||||
const [transactionHash, setTransactionHash] = useState<string | undefined>();
|
||||
|
||||
// Form states
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentChain(defaultChain);
|
||||
|
||||
return () => {
|
||||
abacusStateManager.clearTransferInputValues();
|
||||
};
|
||||
}, [defaultChain]);
|
||||
|
||||
useEffect(() => {
|
||||
setSourceToken(defaultToken);
|
||||
setSquidRoute(undefined);
|
||||
setFromAmount('');
|
||||
}, [currentChain]);
|
||||
|
||||
// @ts-ignore ibcDenom exists but is not typed properly on 0xSquid SDK
|
||||
const addressOrDenom = sourceToken?.ibcDenom || sourceToken?.address;
|
||||
const bech32AddrPrefix = (currentChain as CosmosChain)?.bech32Config?.bech32PrefixAccAddr;
|
||||
|
||||
const { balance, isBalanceError, isBalanceLoading } = useAccountBalance({
|
||||
addressOrDenom,
|
||||
assetSymbol: sourceToken?.symbol,
|
||||
bech32AddrPrefix,
|
||||
chainId: currentChain?.chainId,
|
||||
decimals: sourceToken?.decimals,
|
||||
rpc: currentChain?.rpc,
|
||||
isCosmosChain: currentChain?.chainType === 'cosmos',
|
||||
});
|
||||
|
||||
// BN
|
||||
const fromAmountBN = MustBigNumber(fromAmount);
|
||||
const balanceBN = MustBigNumber(balance);
|
||||
|
||||
const requestDeposit = async () => {
|
||||
if (!squidRoute) {
|
||||
log('DepositForm/requestDeposit', new Error('No squid route'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
setTransactionHash(undefined);
|
||||
|
||||
const txReceipt = await squidRouter.executeRoute({
|
||||
route: squidRoute,
|
||||
signer: signerGraz || signerWagmi,
|
||||
});
|
||||
console.log('DepositForm/executeRoute', { txReceipt });
|
||||
setFromAmount('');
|
||||
setTransactionHash(txReceipt?.transactionHash);
|
||||
} catch (error) {
|
||||
log('DepositForm/executeRoute', error);
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setAbacusTransferInput = (value: string | number) => {
|
||||
abacusStateManager.setTransferValue({ value, field: TransferInputField.usdcSize });
|
||||
};
|
||||
|
||||
const getRoute = useCallback(
|
||||
({ newAmount, newSlippage }: { newAmount: string; newSlippage?: number }) => {
|
||||
debouncedGetRoute.cancel();
|
||||
|
||||
if (MustBigNumber(newAmount).gt(0) && MustBigNumber(newAmount).lte(balanceBN)) {
|
||||
debouncedGetRoute({
|
||||
amount: newAmount,
|
||||
currentChain,
|
||||
cosmosAddress,
|
||||
fromAmount,
|
||||
setAbacusTransferInput,
|
||||
setError,
|
||||
setIsLoading,
|
||||
setSquidRoute,
|
||||
slippage: newSlippage || slippage,
|
||||
sourceToken,
|
||||
});
|
||||
} else {
|
||||
setSquidRoute(undefined);
|
||||
setAbacusTransferInput(0);
|
||||
}
|
||||
},
|
||||
[currentChain, sourceToken, fromAmount, slippage]
|
||||
);
|
||||
|
||||
const onChangeAmount = ({ value }: NumberFormatValues) => {
|
||||
setFromAmount(value);
|
||||
getRoute({ newAmount: value });
|
||||
};
|
||||
|
||||
const onSetSlippage = (newSlippage: number) => {
|
||||
setSlippage(newSlippage);
|
||||
|
||||
if (MustBigNumber(newSlippage).gt(0) && fromAmountBN.gt(0)) {
|
||||
getRoute({ newAmount: fromAmount, newSlippage });
|
||||
}
|
||||
};
|
||||
|
||||
const onClickMax = () => {
|
||||
if (balance) {
|
||||
setFromAmount(balanceBN.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const amountInputReceipt = [
|
||||
{
|
||||
key: 'amount',
|
||||
label: (
|
||||
<span>
|
||||
{stringGetter({ key: STRING_KEYS.AVAILABLE })}{' '}
|
||||
{sourceToken && <Tag>{sourceToken.symbol}</Tag>}
|
||||
</span>
|
||||
),
|
||||
value: (
|
||||
<DiffOutput
|
||||
type={OutputType.Asset}
|
||||
value={balance}
|
||||
newValue={balanceBN.minus(fromAmountBN).toString()}
|
||||
sign={NumberSign.Negative}
|
||||
hasInvalidNewValue={balanceBN.minus(fromAmountBN).isNegative()}
|
||||
withDiff={Boolean(fromAmount && balance) && !fromAmountBN.isNaN()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const errorMessage = getDepositFormErrorMessage({
|
||||
error,
|
||||
balance,
|
||||
currentChain,
|
||||
fromAmount,
|
||||
sourceToken,
|
||||
stringGetter,
|
||||
});
|
||||
|
||||
const isDisabled =
|
||||
Boolean(errorMessage) ||
|
||||
!sourceToken ||
|
||||
!currentChain ||
|
||||
fromAmountBN.isNaN() ||
|
||||
fromAmountBN.isZero() ||
|
||||
!squidRoute;
|
||||
|
||||
const alertMessage = {
|
||||
type: errorMessage ? AlertType.Error : AlertType.Info,
|
||||
message:
|
||||
errorMessage ||
|
||||
(transactionHash && (
|
||||
<span>
|
||||
{stringGetter({ key: STRING_KEYS.VIEW_DETAILS })}
|
||||
{' on '}
|
||||
<Styled.Link href={`https://testnet.axelarscan.io/transfer/${transactionHash}`}>
|
||||
Axelar
|
||||
</Styled.Link>
|
||||
.
|
||||
</span>
|
||||
)),
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.Form
|
||||
onSubmit={(e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
requestDeposit();
|
||||
}}
|
||||
>
|
||||
<ChainSelectMenu
|
||||
chains={chains}
|
||||
currentChain={currentChain}
|
||||
setCurrentChain={setCurrentChain}
|
||||
/>
|
||||
<TokenSelectMenu
|
||||
tokens={tokens}
|
||||
chain={currentChain}
|
||||
sourceToken={sourceToken}
|
||||
setSourceToken={(token: TokenData) => {
|
||||
setSourceToken(token);
|
||||
|
||||
if (fromAmount) {
|
||||
debouncedGetRoute({
|
||||
amount: fromAmount,
|
||||
currentChain,
|
||||
cosmosAddress,
|
||||
fromAmount,
|
||||
setError,
|
||||
setIsLoading,
|
||||
setSquidRoute,
|
||||
slippage,
|
||||
sourceToken: token,
|
||||
});
|
||||
} else {
|
||||
setSquidRoute(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<WithLabel label={stringGetter({ key: STRING_KEYS.AMOUNT })}>
|
||||
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
|
||||
<Styled.FormInput
|
||||
type={InputType.Number}
|
||||
onChange={onChangeAmount}
|
||||
value={fromAmount}
|
||||
slotRight={
|
||||
<Button size={ButtonSize.XSmall} onClick={onClickMax}>
|
||||
{stringGetter({ key: STRING_KEYS.MAX })}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Styled.WithDetailsReceipt>
|
||||
</WithLabel>
|
||||
{alertMessage.message && (
|
||||
<Styled.AlertMessage type={alertMessage.type}>{alertMessage.message}</Styled.AlertMessage>
|
||||
)}
|
||||
<Styled.Footer>
|
||||
<DepositButtonAndReceipt
|
||||
isDisabled={isDisabled}
|
||||
isLoading={isLoading || isBalanceLoading}
|
||||
chainId={currentChain?.chainId}
|
||||
setError={setError}
|
||||
setSlippage={onSetSlippage}
|
||||
slippage={slippage}
|
||||
sourceToken={sourceToken}
|
||||
squidRoute={squidRoute}
|
||||
/>
|
||||
</Styled.Footer>
|
||||
</Styled.Form>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.Form = styled.form`
|
||||
${layoutMixins.flexColumn}
|
||||
gap: 1.5rem;
|
||||
|
||||
${layoutMixins.stickyArea1}
|
||||
min-height: calc(100% - var(--stickyArea0-bottomHeight));
|
||||
`;
|
||||
|
||||
Styled.WithDetailsReceipt = styled(WithDetailsReceipt)`
|
||||
--withReceipt-backgroundColor: var(--color-layer-2);
|
||||
`;
|
||||
|
||||
Styled.FormInput = styled(FormInput)`
|
||||
--formInput-backgroundColor: var(--color-layer-4);
|
||||
--formInput-input-height: 3.375rem;
|
||||
border: 1px solid var(--color-layer-6);
|
||||
`;
|
||||
|
||||
Styled.Link = styled(Link)`
|
||||
color: var(--color-accent);
|
||||
`;
|
||||
|
||||
Styled.AlertMessage = styled(AlertMessage)`
|
||||
cursor: normal;
|
||||
user-select: none;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
Styled.Footer = styled.footer`
|
||||
${layoutMixins.stickyFooter}
|
||||
margin-top: auto;
|
||||
`;
|
||||
@@ -1,89 +0,0 @@
|
||||
import type { ChainData, TokenData } from '@0xsquid/sdk';
|
||||
import styled, { type AnyStyledComponent } from 'styled-components';
|
||||
|
||||
import { STRING_KEYS } from '@/constants/localization';
|
||||
import { useStringGetter } from '@/hooks';
|
||||
|
||||
import { SearchSelectMenu } from '@/components/SearchSelectMenu';
|
||||
import { Tag } from '@/components/Tag';
|
||||
import { WithLabel } from '@/components/WithLabel';
|
||||
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
type ElementProps = {
|
||||
chain?: ChainData;
|
||||
tokens?: TokenData[];
|
||||
sourceToken?: TokenData;
|
||||
setSourceToken: (token: TokenData) => void;
|
||||
};
|
||||
|
||||
export const TokenSelectMenu = ({
|
||||
tokens = [],
|
||||
chain,
|
||||
sourceToken,
|
||||
setSourceToken,
|
||||
}: ElementProps) => {
|
||||
const stringGetter = useStringGetter();
|
||||
|
||||
const tokenItems = Object.values(tokens)
|
||||
.filter((token: TokenData) => token.chainId === chain?.chainId)
|
||||
.map((token: TokenData) => ({
|
||||
value: token.address,
|
||||
label: token.name,
|
||||
onSelect: () => setSourceToken(token),
|
||||
slotBefore: <Styled.Img src={token.logoURI} alt="" />,
|
||||
}));
|
||||
|
||||
return (
|
||||
<SearchSelectMenu
|
||||
disabled={!chain}
|
||||
items={[
|
||||
{
|
||||
group: 'assets',
|
||||
groupLabel: stringGetter({ key: STRING_KEYS.ASSET }),
|
||||
items: tokenItems,
|
||||
},
|
||||
]}
|
||||
label={stringGetter({ key: STRING_KEYS.ASSET })}
|
||||
withReceiptItems={[
|
||||
{
|
||||
key: 'swap',
|
||||
label: stringGetter({ key: STRING_KEYS.SWAP }),
|
||||
value: sourceToken && (
|
||||
<>
|
||||
<Tag>{sourceToken?.symbol}</Tag>→<Tag>USDC</Tag>
|
||||
</>
|
||||
),
|
||||
tooltip: 'swap',
|
||||
withTooltipIcon: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Styled.AssetRow>
|
||||
{sourceToken ? (
|
||||
<>
|
||||
<Styled.Img src={sourceToken?.logoURI} alt="" /> {sourceToken?.name}{' '}
|
||||
<Tag>{sourceToken?.symbol}</Tag>
|
||||
</>
|
||||
) : (
|
||||
stringGetter({ key: STRING_KEYS.SELECT_ASSET })
|
||||
)}
|
||||
</Styled.AssetRow>
|
||||
</SearchSelectMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const Styled: Record<string, AnyStyledComponent> = {};
|
||||
|
||||
Styled.AssetRow = styled.div`
|
||||
${layoutMixins.row}
|
||||
gap: 0.5rem;
|
||||
color: var(--color-text-2);
|
||||
font: var(--font-base-book);
|
||||
`;
|
||||
|
||||
Styled.Img = styled.img`
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ChainData, TokenData } from '@0xsquid/sdk';
|
||||
|
||||
import squidRouter from '@/lib/squidRouter';
|
||||
import { WAGMI_SUPPORTED_CHAINS } from '@/lib/wagmi';
|
||||
|
||||
/**
|
||||
* @description hook to return chains and tokens supported by squid
|
||||
* @note Limit to EVM chains | Cosmos chains can only be used as destination on 0xSquid as of 02/21/2023
|
||||
*/
|
||||
export const useSquidData = (selectedChainType: 'cosmos' | 'evm') => {
|
||||
const [, setHasInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await squidRouter.initializeClient();
|
||||
setHasInitialized(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const chainData: ChainData[] = squidRouter.getChains();
|
||||
const tokenData: TokenData[] = squidRouter.getTokens();
|
||||
const wagmiChainIds = Object.fromEntries(
|
||||
WAGMI_SUPPORTED_CHAINS.map((supportedChain) => [supportedChain.id, supportedChain])
|
||||
);
|
||||
|
||||
return {
|
||||
chains: chainData.filter(({ chainId, chainType }) =>
|
||||
selectedChainType === 'evm' ? wagmiChainIds[chainId] : chainType === 'cosmos'
|
||||
),
|
||||
tokens: tokenData,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user