diff --git a/package.json b/package.json
index b9d68d4..355ff83 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/v4-abacus": "^0.6.4",
"@dydxprotocol/v4-client-js": "^0.36.1",
- "@dydxprotocol/v4-localization": "^0.1.28",
+ "@dydxprotocol/v4-localization": "^0.1.30",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b7a6906..3e01552 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
dependencies:
'@0xsquid/sdk':
specifier: ^1.10.0
@@ -29,8 +33,8 @@ dependencies:
specifier: ^0.36.1
version: 0.36.1
'@dydxprotocol/v4-localization':
- specifier: ^0.1.28
- version: 0.1.28
+ specifier: ^0.1.30
+ version: 0.1.30
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -1006,8 +1010,8 @@ packages:
- utf-8-validate
dev: false
- /@dydxprotocol/v4-localization@0.1.28:
- resolution: {integrity: sha512-g6LwjX7EIRm9HISfSX0H6yhktBCZby5opP/yI2aIUvn2OYyB25WcjLEuEvzW5Bh9XXiLijgLf3Tk91wPKWuFbA==}
+ /@dydxprotocol/v4-localization@0.1.30:
+ resolution: {integrity: sha512-xTlQuQmPz7wfGkjd07giYMAxRrolDbJE3DryJSIMB6R1MBq1wLzAcwp/iR/nELRwilRX9c44Tap0gGt+oAajiQ==}
dev: false
/@dydxprotocol/v4-proto@0.2.1:
@@ -14095,7 +14099,3 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
diff --git a/src/App.tsx b/src/App.tsx
index b6acfea..35c6e86 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -14,6 +14,7 @@ import { DialogAreaProvider, useDialogArea } from '@/hooks/useDialogArea';
import { LocaleProvider } from '@/hooks/useLocaleSeparators';
import { NotificationsProvider } from '@/hooks/useNotifications';
import { LocalNotificationsProvider } from '@/hooks/useLocalNotifications';
+import { RestrictionProvider } from '@/hooks/useRestrictions';
import { SubaccountProvider } from '@/hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquid';
@@ -66,6 +67,7 @@ const Content = () => {
} />
} />
+
} />
} />
{isTablet && (
@@ -79,6 +81,7 @@ const Content = () => {
}>
} />
+
} />
@@ -109,6 +112,7 @@ const providers = [
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(LocaleProvider),
+ wrapProvider(RestrictionProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
diff --git a/src/components/Input.tsx b/src/components/Input.tsx
index 5184433..107e559 100644
--- a/src/components/Input.tsx
+++ b/src/components/Input.tsx
@@ -207,6 +207,16 @@ const InputStyle = css`
color: var(--color-text-0);
opacity: 1;
}
+
+ // Input autofill Styles
+ &:-webkit-autofill,
+ &:-webkit-autofill:focus {
+ transition: background-color 600000s 0s, color 600000s 0s;
+ }
+
+ &[data-autocompleted] {
+ background-color: var(--input-backgroundColor) !important;
+ }
`;
Styled.NumericFormat = styled(NumericFormat)`
diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts
index c75ab88..592cb33 100644
--- a/src/constants/abacus.ts
+++ b/src/constants/abacus.ts
@@ -78,8 +78,13 @@ export const Changes = Abacus.exchange.dydx.abacus.state.changes.Changes;
export type PerpetualStateChanges = Abacus.exchange.dydx.abacus.state.changes.StateChanges;
export const AsyncAbacusStateManager =
Abacus.exchange.dydx.abacus.state.manager.AsyncAbacusStateManager;
+
+// ------ Parsing Errors ------ //
export type ParsingError = Abacus.exchange.dydx.abacus.responses.ParsingError;
export type ParsingErrors = kollections.List;
+export const ParsingErrorType = Abacus.exchange.dydx.abacus.responses.ParsingErrorType;
+const parsingErrorTypes = [...ParsingErrorType.values()] as const;
+export type ParsingErrorTypes = (typeof parsingErrorTypes)[number];
// ------ Perpetuals/Markets ------ //
export type PerpetualState = Abacus.exchange.dydx.abacus.output.PerpetualState;
@@ -199,6 +204,12 @@ export type RiskLevels = (typeof riskLevels)[number];
// ------ Notifications ------ //
export type AbacusNotification = Abacus.exchange.dydx.abacus.output.Notification;
+// ------ Restrictions ------ //
+export type UsageRestriction = Abacus.exchange.dydx.abacus.output.UsageRestriction;
+export const RestrictionType = Abacus.exchange.dydx.abacus.output.Restriction;
+const restrictionTypes = [...RestrictionType.values()] as const;
+export type RestrictionTypes = (typeof restrictionTypes)[number];
+
// ------ Enum Conversions ------ //
type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2
? A
diff --git a/src/constants/dialogs.ts b/src/constants/dialogs.ts
index c14c0fb..ac5d5b5 100644
--- a/src/constants/dialogs.ts
+++ b/src/constants/dialogs.ts
@@ -10,7 +10,10 @@ export enum DialogTypes {
MobileSignIn = 'MobileSignIn',
Onboarding = 'Onboarding',
OrderDetails = 'OrderDetails',
+ RateLimit = 'RateLimit',
Receive = 'Receive',
+ RestrictedGeo = 'RestrictedGeo',
+ RestrictedWallet = 'RestrictedWallet',
Trade = 'Trade',
Transfer = 'Transfer',
Withdraw = 'Withdraw',
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index d6c228d..6908601 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -16,6 +16,7 @@ import { useLocalStorage } from './useLocalStorage';
import { useNow } from './useNow';
import { useOnClickOutside } from './useOnClickOutside';
import { usePageTitlePriceUpdates } from './usePageTitlePriceUpdates';
+import { useRestrictions } from './useRestrictions';
import { useShouldShowFooter } from './useShouldShowFooter';
import { useSelectedNetwork } from './useSelectedNetwork';
import { useStringGetter } from './useStringGetter';
@@ -40,6 +41,7 @@ export {
useNow,
useOnClickOutside,
usePageTitlePriceUpdates,
+ useRestrictions,
useShouldShowFooter,
useSelectedNetwork,
useStringGetter,
diff --git a/src/hooks/useAccounts.tsx b/src/hooks/useAccounts.tsx
index 5a6f494..34a6b03 100644
--- a/src/hooks/useAccounts.tsx
+++ b/src/hooks/useAccounts.tsx
@@ -5,18 +5,20 @@ import { AES, enc } from 'crypto-js';
import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-client-js';
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
+import { DialogTypes } from '@/constants/dialogs';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import { setOnboardingState, setOnboardingGuard } from '@/state/account';
+import { forceOpenDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry';
-import { useLocalStorage } from './useLocalStorage';
-
-import { useWalletConnection } from './useWalletConnection';
import { useDydxClient } from './useDydxClient';
+import { useLocalStorage } from './useLocalStorage';
+import { useRestrictions } from './useRestrictions';
+import { useWalletConnection } from './useWalletConnection';
const AccountsContext = createContext | undefined>(undefined);
@@ -57,7 +59,9 @@ const useAccountsContext = () => {
forgetEvmSignature(previousEvmAddress);
}
- if (evmAddress) abacusStateManager.setTransfersSourceAddress(evmAddress);
+ if (evmAddress) {
+ abacusStateManager.setTransfersSourceAddress(evmAddress);
+ }
setPreviousEvmAddress(evmAddress);
}, [evmAddress]);
@@ -217,7 +221,6 @@ const useAccountsContext = () => {
}, [evmAddress, evmDerivedAddresses, signerWagmi, connectedDydxAddress, signerGraz]);
// abacus
- // TODO: useAbacus({ dydxAddress })
useEffect(() => {
if (dydxAddress) abacusStateManager.setAccount(localDydxWallet);
else abacusStateManager.attemptDisconnectAccount();
@@ -258,6 +261,21 @@ const useAccountsContext = () => {
);
}, [dydxSubaccounts]);
+ // Restrictions
+ const { isBadActor, sanctionedAddresses } = useRestrictions();
+
+ useEffect(() => {
+ if (
+ dydxAddress &&
+ (isBadActor ||
+ sanctionedAddresses.has(dydxAddress) ||
+ (evmAddress && sanctionedAddresses.has(evmAddress)))
+ ) {
+ dispatch(forceOpenDialog({ type: DialogTypes.RestrictedWallet }));
+ disconnect();
+ }
+ }, [isBadActor, evmAddress, dydxAddress, sanctionedAddresses]);
+
// Disconnect wallet / accounts
const disconnectLocalDydxWallet = () => {
setLocalDydxWallet(undefined);
diff --git a/src/hooks/useDydxClient.tsx b/src/hooks/useDydxClient.tsx
index 40176b8..b8202ff 100644
--- a/src/hooks/useDydxClient.tsx
+++ b/src/hooks/useDydxClient.tsx
@@ -17,8 +17,11 @@ import type { NetworkConfig, ConnectNetworkEvent } from '@/constants/abacus';
import { type Candle, RESOLUTION_MAP } from '@/constants/candles';
import { getSelectedNetwork } from '@/state/appSelectors';
+
import { log } from '@/lib/telemetry';
+import { useRestrictions } from './useRestrictions';
+
type DydxContextType = ReturnType;
const DydxContext = createContext({} as DydxContextType);
DydxContext.displayName = 'dYdXClient';
@@ -178,7 +181,27 @@ const useDydxClientContext = () => {
[requestCandles]
);
- // ------ Subacount Methods ------ //
+ const { updateSanctionedAddresses } = useRestrictions();
+
+ const screenAddresses = useCallback(
+ async ({ addresses }: { addresses: string[] }) => {
+ if (compositeClient) {
+ const promises = addresses.map((address) =>
+ compositeClient.indexerClient.utility.screen(address)
+ );
+
+ const results = await Promise.all(promises);
+
+ const screenedAddresses = Object.fromEntries(
+ addresses.map((address, index) => [address, results[index]?.restricted])
+ );
+
+ updateSanctionedAddresses(screenedAddresses);
+ return screenedAddresses;
+ }
+ },
+ [compositeClient]
+ );
return {
// Client initialization
@@ -192,5 +215,6 @@ const useDydxClientContext = () => {
// Public Methods
getCandlesForDatafeed,
+ screenAddresses,
};
};
diff --git a/src/hooks/useInitializePage.ts b/src/hooks/useInitializePage.ts
index 05568d8..9a61b97 100644
--- a/src/hooks/useInitializePage.ts
+++ b/src/hooks/useInitializePage.ts
@@ -10,6 +10,7 @@ import { useLocalStorage } from '@/hooks';
import { initializeLocalization } from '@/state/app';
import abacusStateManager from '@/lib/abacus';
+import { validateAgainstAvailableEnvironments } from '@/lib/network';
export const useInitializePage = () => {
const dispatch = useDispatch();
@@ -18,6 +19,7 @@ export const useInitializePage = () => {
const [localStorageNetwork] = useLocalStorage({
key: LocalStorageKey.SelectedNetwork,
defaultValue: DEFAULT_APP_ENVIRONMENT,
+ validateFn: validateAgainstAvailableEnvironments,
});
useEffect(() => {
diff --git a/src/hooks/useRestrictions.tsx b/src/hooks/useRestrictions.tsx
new file mode 100644
index 0000000..184a523
--- /dev/null
+++ b/src/hooks/useRestrictions.tsx
@@ -0,0 +1,53 @@
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { shallowEqual, useDispatch, useSelector } from 'react-redux';
+
+import { RestrictionType } from '@/constants/abacus';
+import { DialogTypes } from '@/constants/dialogs';
+
+import { getRestrictionType } from '@/state/accountSelectors';
+import { forceOpenDialog } from '@/state/dialogs';
+import { isTruthy } from '@/lib/isTruthy';
+
+const useRestrictionContext = () => {
+ const dispatch = useDispatch();
+ const restrictionType = useSelector(getRestrictionType, shallowEqual);
+ const [sanctionedAddresses, setSanctionedAddresses] = useState([]);
+
+ useEffect(() => {
+ if (restrictionType === RestrictionType.GEO_RESTRICTED) {
+ dispatch(
+ forceOpenDialog({ type: DialogTypes.RestrictedGeo, dialogProps: { preventClose: true } })
+ );
+ }
+ }, [restrictionType, dispatch]);
+
+ const updateSanctionedAddresses = useCallback(
+ (screenedAddresses: { [address: string]: boolean }) => {
+ const toAdd = Object.entries(screenedAddresses)
+ .filter(([, isSanctioned]) => isSanctioned)
+ .map(([address]) => address);
+
+ if (toAdd.length) {
+ setSanctionedAddresses([...sanctionedAddresses, ...toAdd]);
+ }
+ },
+ [sanctionedAddresses]
+ );
+
+ return {
+ isBadActor: restrictionType === RestrictionType.USER_RESTRICTED,
+ isGeoRestricted: restrictionType === RestrictionType.GEO_RESTRICTED,
+ updateSanctionedAddresses,
+ sanctionedAddresses: useMemo(() => new Set(sanctionedAddresses), [sanctionedAddresses]),
+ };
+};
+
+type RestrictionContextType = ReturnType;
+const RestrictionContext = createContext({} as RestrictionContextType);
+RestrictionContext.displayName = 'Restriction';
+
+export const RestrictionProvider = ({ ...props }) => (
+
+);
+
+export const useRestrictions = () => useContext(RestrictionContext);
diff --git a/src/hooks/useSelectedNetwork.ts b/src/hooks/useSelectedNetwork.ts
index ca4cbb0..9b7a898 100644
--- a/src/hooks/useSelectedNetwork.ts
+++ b/src/hooks/useSelectedNetwork.ts
@@ -2,13 +2,15 @@ import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { LocalStorageKey } from '@/constants/localStorage';
-import { DEFAULT_APP_ENVIRONMENT, DydxNetwork } from '@/constants/networks';
+import { DEFAULT_APP_ENVIRONMENT, DydxNetwork, ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
import { useAccounts, useLocalStorage } from '@/hooks';
import { setSelectedNetwork } from '@/state/app';
import { getSelectedNetwork } from '@/state/appSelectors';
+import { validateAgainstAvailableEnvironments } from '@/lib/network';
+
export const useSelectedNetwork = (): {
switchNetwork: (network: DydxNetwork) => void;
selectedNetwork: DydxNetwork;
@@ -20,6 +22,7 @@ export const useSelectedNetwork = (): {
const [, setLocalStorageNetwork] = useLocalStorage({
key: LocalStorageKey.SelectedNetwork,
defaultValue: DEFAULT_APP_ENVIRONMENT,
+ validateFn: validateAgainstAvailableEnvironments,
});
const switchNetwork = useCallback(
diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx
index 5ab0a73..042a599 100644
--- a/src/hooks/useSubaccount.tsx
+++ b/src/hooks/useSubaccount.tsx
@@ -36,7 +36,6 @@ import { log } from '@/lib/telemetry';
import { useAccounts } from './useAccounts';
import { useDydxClient } from './useDydxClient';
-
type SubaccountContextType = ReturnType;
const SubaccountContext = createContext({} as SubaccountContextType);
SubaccountContext.displayName = 'Subaccount';
@@ -275,6 +274,7 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
await getFaucetFunds({ dydxAddress, subaccountNumber });
} catch (error) {
log('useSubaccount/getFaucetFunds', error);
+ throw error;
}
}, [dydxAddress, getFaucetFunds, subaccountNumber]);
diff --git a/src/layout/DialogManager.tsx b/src/layout/DialogManager.tsx
index 4a7d0f0..0a653b5 100644
--- a/src/layout/DialogManager.tsx
+++ b/src/layout/DialogManager.tsx
@@ -15,9 +15,12 @@ import { ExternalNavKeplrDialog } from '@/views/dialogs/ExternalNavKeplrDialog';
import { MnemonicExportDialog } from '@/views/dialogs/MnemonicExportDialog';
import { MobileSignInDialog } from '@/views/dialogs/MobileSignInDialog';
import { OnboardingDialog } from '@/views/dialogs/OnboardingDialog';
+import { RateLimitDialog } from '@/views/dialogs/RateLimitDialog';
import { ReceiveDialog } from '@/views/dialogs/ReceiveDialog';
+import { RestrictedGeoDialog } from '@/views/dialogs/RestrictedGeoDialog';
import { TradeDialog } from '@/views/dialogs/TradeDialog';
import { TransferDialog } from '@/views/dialogs/TransferDialog';
+import { RestrictedWalletDialog } from '@/views/dialogs/RestrictedWalletDialog';
import { WithdrawDialog } from '@/views/dialogs/WithdrawDialog';
import { OrderDetailsDialog } from '@/views/dialogs/DetailsDialog/OrderDetailsDialog';
@@ -53,7 +56,10 @@ export const DialogManager = () => {
[DialogTypes.MobileSignIn]: ,
[DialogTypes.Onboarding]: ,
[DialogTypes.OrderDetails]: ,
+ [DialogTypes.RateLimit]: ,
[DialogTypes.Receive]: ,
+ [DialogTypes.RestrictedGeo]: ,
+ [DialogTypes.RestrictedWallet]: ,
[DialogTypes.Trade]: ,
[DialogTypes.Transfer]: ,
[DialogTypes.Withdraw]: ,
diff --git a/src/lib/abacus/rest.ts b/src/lib/abacus/rest.ts
index 2c5ca00..52e5ca4 100644
--- a/src/lib/abacus/rest.ts
+++ b/src/lib/abacus/rest.ts
@@ -47,6 +47,11 @@ class AbacusRest implements AbacusRestProtocol {
body,
};
+ if (!url) {
+ callback(null, 0);
+ return;
+ }
+
fetch(url, options)
.then(async (response) => {
const data = await response.text();
diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts
index fa2b9e9..de275da 100644
--- a/src/lib/abacus/stateNotification.ts
+++ b/src/lib/abacus/stateNotification.ts
@@ -25,6 +25,7 @@ import {
setFundingPayments,
setHistoricalPnl,
setLatestOrder,
+ setRestrictionType,
setSubaccount,
setTransfers,
setWallet,
@@ -121,6 +122,10 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol {
);
}
+ if (changes.has(Changes.restriction)) {
+ dispatch(setRestrictionType(updatedState.restriction));
+ }
+
subaccountNumbers?.forEach((subaccountId: number) => {
if (subaccountId !== null) {
if (changes.has(Changes.subaccount)) {
diff --git a/src/lib/network.ts b/src/lib/network.ts
new file mode 100644
index 0000000..56131de
--- /dev/null
+++ b/src/lib/network.ts
@@ -0,0 +1,4 @@
+import { type DydxNetwork, ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
+
+export const validateAgainstAvailableEnvironments = (value: DydxNetwork) =>
+ Object.keys(ENVIRONMENT_CONFIG_MAP).includes(value);
diff --git a/src/pages/portfolio/AccountDetailsAndHistory.tsx b/src/pages/portfolio/AccountDetailsAndHistory.tsx
index df77024..64954d1 100644
--- a/src/pages/portfolio/AccountDetailsAndHistory.tsx
+++ b/src/pages/portfolio/AccountDetailsAndHistory.tsx
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import styled, { AnyStyledComponent, css } from 'styled-components';
import { shallowEqual, useSelector } from 'react-redux';
import { TooltipContextType } from '@visx/xychart';
+import BigNumber from 'bignumber.js';
import type { Nullable } from '@/constants/abacus';
import { OnboardingState } from '@/constants/account';
@@ -25,7 +26,6 @@ import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton
import { isTruthy } from '@/lib/isTruthy';
import { MustBigNumber } from '@/lib/numbers';
-
export const usePortfolioValues = ({
equity,
visibleData,
@@ -140,7 +140,12 @@ export const AccountDetailsAndHistory = () => {
-
+
{pnlDiff && }
diff --git a/src/state/_store.ts b/src/state/_store.ts
index 28a0b4e..5d5588d 100644
--- a/src/state/_store.ts
+++ b/src/state/_store.ts
@@ -3,7 +3,6 @@ import { configureStore } from '@reduxjs/toolkit';
import abacusStateManager from '@/lib/abacus';
import appMiddleware from './appMiddleware';
import localizationMiddleware from './localizationMiddleware';
-import routerMiddleware from './routerMiddleware';
import { accountSlice } from './account';
import { appSlice } from './app';
@@ -13,7 +12,6 @@ import { dialogsSlice } from './dialogs';
import { inputsSlice } from './inputs';
import { layoutSlice } from './layout';
import { localizationSlice } from './localization';
-import { navigationSlice } from './navigation';
import { perpetualsSlice } from './perpetuals';
export const commandMenuSlices = [layoutSlice, localizationSlice];
@@ -28,7 +26,6 @@ export const store = configureStore({
inputs: inputsSlice.reducer,
layout: layoutSlice.reducer,
localization: localizationSlice.reducer,
- navigation: navigationSlice.reducer,
perpetuals: perpetualsSlice.reducer,
},
@@ -38,7 +35,6 @@ export const store = configureStore({
}),
appMiddleware,
localizationMiddleware,
- routerMiddleware,
],
devTools: process.env.NODE_ENV !== 'production',
diff --git a/src/state/account.ts b/src/state/account.ts
index e3178d8..aae7b2d 100644
--- a/src/state/account.ts
+++ b/src/state/account.ts
@@ -12,6 +12,7 @@ import type {
SubaccountTransfers,
HistoricalPnlPeriods,
SubAccountHistoricalPNLs,
+ UsageRestriction,
} from '@/constants/abacus';
import { OnboardingGuard, OnboardingState } from '@/constants/account';
@@ -21,34 +22,46 @@ import { WalletType } from '@/constants/wallets';
import { getLocalStorage } from '@/lib/localStorage';
export type AccountState = {
+ balances?: Record;
+ stakingBalances?: Record;
+ wallet?: Nullable;
+ walletType?: WalletType;
+
+ subaccount?: Nullable;
fills?: SubaccountFills;
fundingPayments?: SubaccountFundingPayments;
transfers?: SubaccountTransfers;
+ historicalPnl?: SubAccountHistoricalPNLs;
+
+ onboardingGuards: Record;
+ onboardingState: OnboardingState;
+
clearedOrderIds?: string[];
hasUnseenFillUpdates: boolean;
hasUnseenOrderUpdates: boolean;
- historicalPnl?: SubAccountHistoricalPNLs;
latestOrder?: Nullable;
- onboardingGuards: Record;
- onboardingState: OnboardingState;
- subaccount?: Nullable;
- uncommittedOrderClientIds: number[];
- wallet?: Nullable;
- walletType?: WalletType;
historicalPnlPeriod?: HistoricalPnlPeriods;
- balances?: Record;
- stakingBalances?: Record;
+ uncommittedOrderClientIds: number[];
+
+ restriction?: Nullable;
};
const initialState: AccountState = {
+ // Wallet
+ balances: undefined,
+ wallet: undefined,
+ walletType: getLocalStorage({
+ key: LocalStorageKey.OnboardingSelectedWalletType,
+ }),
+
+ // Subaccount
+ subaccount: undefined,
fills: undefined,
fundingPayments: undefined,
transfers: undefined,
- clearedOrderIds: undefined,
- hasUnseenFillUpdates: false,
- hasUnseenOrderUpdates: false,
historicalPnl: undefined,
- latestOrder: undefined,
+
+ // Onboarding
onboardingGuards: {
[OnboardingGuard.hasAcknowledgedTerms]: Boolean(
getLocalStorage({
@@ -59,12 +72,17 @@ const initialState: AccountState = {
[OnboardingGuard.hasPreviousTransactions]: undefined,
},
onboardingState: OnboardingState.Disconnected,
- subaccount: undefined,
+
+ // UI
+ clearedOrderIds: undefined,
+ hasUnseenFillUpdates: false,
+ hasUnseenOrderUpdates: false,
+ latestOrder: undefined,
uncommittedOrderClientIds: [],
- wallet: undefined,
- walletType: getLocalStorage({
- key: LocalStorageKey.OnboardingSelectedWalletType,
- }),
+ historicalPnlPeriod: undefined,
+
+ // Restriction
+ restriction: undefined,
};
export const accountSlice = createSlice({
@@ -124,6 +142,9 @@ export const accountSlice = createSlice({
state.historicalPnl = action.payload;
}
},
+ setRestrictionType: (state, action: PayloadAction>) => {
+ state.restriction = action.payload;
+ },
setSubaccount: (state, action: PayloadAction>) => {
const existingOrderIds = state.subaccount?.orders
? state.subaccount.orders.toArray().map((order) => order.id)
@@ -178,6 +199,7 @@ export const {
setOnboardingGuard,
setOnboardingState,
setHistoricalPnl,
+ setRestrictionType,
setSubaccount,
setWallet,
viewedFills,
diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts
index 4a81a5b..9594b46 100644
--- a/src/state/accountSelectors.ts
+++ b/src/state/accountSelectors.ts
@@ -342,3 +342,13 @@ export const getBalances = (state: RootState) => state.account?.balances;
* @returns user wallet staking balances
* */
export const getStakingBalances = (state: RootState) => state.account?.stakingBalances;
+
+/**
+ * @returns UsageRestriction of the current session
+ */
+export const getUsageRestriction = (state: RootState) => state.account.restriction;
+
+/**
+ * @returns RestrictionType from the current session
+ */
+export const getRestrictionType = (state: RootState) => state.account.restriction?.restriction;
diff --git a/src/state/app.ts b/src/state/app.ts
index 61e5d65..d29bfc6 100644
--- a/src/state/app.ts
+++ b/src/state/app.ts
@@ -2,10 +2,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AbacusApiState } from '@/constants/abacus';
import { LocalStorageKey } from '@/constants/localStorage';
-
import { DEFAULT_APP_ENVIRONMENT, type DydxNetwork } from '@/constants/networks';
import { getLocalStorage } from '@/lib/localStorage';
+import { validateAgainstAvailableEnvironments } from '@/lib/network';
export interface AppState {
apiState: AbacusApiState | undefined;
@@ -19,6 +19,7 @@ const initialState: AppState = {
selectedNetwork: getLocalStorage({
key: LocalStorageKey.SelectedNetwork,
defaultValue: DEFAULT_APP_ENVIRONMENT,
+ validateFn: validateAgainstAvailableEnvironments,
}),
};
diff --git a/src/state/dialogs.ts b/src/state/dialogs.ts
index 8f2656f..01ca53e 100644
--- a/src/state/dialogs.ts
+++ b/src/state/dialogs.ts
@@ -23,39 +23,36 @@ export const dialogsSlice = createSlice({
name: 'Dialogs',
initialState,
reducers: {
- addDialogToQueue: (state: DialogsState, action: PayloadAction>) => {
- const dialogQueue = state.dialogQueue;
- dialogQueue.push(action.payload);
-
- return {
- ...state,
- dialogQueue,
- };
+ openDialog: (state: DialogsState, action: PayloadAction>) => {
+ if (state.activeDialog) {
+ state.dialogQueue.push(action.payload);
+ } else {
+ state.activeDialog = action.payload;
+ }
+ },
+ closeDialog: (state: DialogsState) => {
+ state.activeDialog = state.dialogQueue.shift();
+ },
+ forceOpenDialog: (state: DialogsState, action: PayloadAction>) => {
+ if (state.activeDialog) {
+ state.dialogQueue.unshift(state.activeDialog);
+ }
+ state.activeDialog = action.payload;
+ },
+ closeDialogInTradeBox: (state: DialogsState) => {
+ state.activeTradeBoxDialog = undefined;
},
- closeDialog: (state: DialogsState) => ({
- ...state,
- activeDialog: state.dialogQueue.shift(),
- }),
- openDialog: (state: DialogsState, action: PayloadAction>) => ({
- ...state,
- activeDialog: action.payload,
- }),
- closeDialogInTradeBox: (state: DialogsState) => ({
- ...state,
- activeTradeBoxDialog: undefined,
- }),
openDialogInTradeBox: (
state: DialogsState,
action: PayloadAction>
- ) => ({
- ...state,
- activeTradeBoxDialog: action.payload,
- }),
+ ) => {
+ state.activeTradeBoxDialog = action.payload;
+ },
},
});
export const {
- addDialogToQueue,
+ forceOpenDialog,
closeDialog,
closeDialogInTradeBox,
openDialog,
diff --git a/src/state/navigation.ts b/src/state/navigation.ts
deleted file mode 100644
index 2cfc580..0000000
--- a/src/state/navigation.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { createSlice } from '@reduxjs/toolkit';
-
-export interface NavigationState {
- tradeLocation: {
- hash: string;
- key: string;
- pathname: string;
- search: string;
- state: any;
- };
-}
-
-export const CLEAR_TRADE_LOCATION = {
- hash: '',
- key: '',
- pathname: '',
- search: '',
- state: null,
-};
-
-const initialState: NavigationState = {
- tradeLocation: {
- hash: '',
- key: '',
- pathname: '',
- search: '',
- state: null,
- },
-};
-
-export const navigationSlice = createSlice({
- name: 'Navigation',
- initialState,
- reducers: {
- setTradeLocation: (state, { payload }) => ({
- ...state,
- tradeLocation: payload,
- }),
- },
-});
-
-export const { setTradeLocation } = navigationSlice.actions;
diff --git a/src/state/routerMiddleware.ts b/src/state/routerMiddleware.ts
deleted file mode 100644
index 06b2328..0000000
--- a/src/state/routerMiddleware.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { PayloadAction } from '@reduxjs/toolkit';
-
-import { LocalStorageKey } from '@/constants/localStorage';
-
-import { closeDialogInTradeBox } from '@/state/dialogs';
-import { setTradeLocation } from '@/state/navigation';
-
-import abacusStateManager from '@/lib/abacus';
-import { setLocalStorage } from '@/lib/localStorage';
-import { getMarketIdFromLocation } from '@/lib/tradeData';
-
-export default (store: any) => (next: any) => async (action: PayloadAction) => {
- next(action);
-
- const { type, payload } = action;
-
- switch (type) {
- case setTradeLocation.type: {
- const marketId = getMarketIdFromLocation(payload);
-
- if (marketId) {
- abacusStateManager.setMarket(marketId);
- store.dispatch(closeDialogInTradeBox());
- setLocalStorage({ key: LocalStorageKey.LastViewedMarket, value: marketId });
- }
-
- break;
- }
- default: {
- break;
- }
- }
-};
diff --git a/src/views/charts/PnlChart.tsx b/src/views/charts/PnlChart.tsx
index efa9bb0..1509277 100644
--- a/src/views/charts/PnlChart.tsx
+++ b/src/views/charts/PnlChart.tsx
@@ -25,7 +25,11 @@ import { TimeSeriesChart } from '@/components/visx/TimeSeriesChart';
import { AxisLabelOutput } from '@/components/visx/AxisLabelOutput';
// import { TooltipContent } from '@/components/visx/TooltipContent';
-import { getSubaccount, getSubaccountHistoricalPnl, getSubaccountId } from '@/state/accountSelectors';
+import {
+ getSubaccount,
+ getSubaccountHistoricalPnl,
+ getSubaccountId,
+} from '@/state/accountSelectors';
import abacusStateManager from '@/lib/abacus';
import { formatRelativeTime } from '@/lib/dateTime';
diff --git a/src/views/dialogs/RateLimitDialog.tsx b/src/views/dialogs/RateLimitDialog.tsx
new file mode 100644
index 0000000..c7dca6b
--- /dev/null
+++ b/src/views/dialogs/RateLimitDialog.tsx
@@ -0,0 +1,42 @@
+import styled, { AnyStyledComponent } from 'styled-components';
+
+import { STRING_KEYS } from '@/constants/localization';
+import { useStringGetter } from '@/hooks';
+import { layoutMixins } from '@/styles/layoutMixins';
+
+import { Dialog } from '@/components/Dialog';
+import { Icon, IconName } from '@/components/Icon';
+
+type ElementProps = {
+ preventClose?: boolean;
+ setIsOpen?: (open: boolean) => void;
+};
+
+export const RateLimitDialog = ({ preventClose, setIsOpen }: ElementProps) => {
+ const stringGetter = useStringGetter();
+
+ return (
+ }
+ >
+
+ {stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE })}
+
+
+ );
+};
+
+const Styled: Record = {};
+
+Styled.Icon = styled(Icon)`
+ color: var(--color-warning);
+`;
+
+Styled.Content = styled.div`
+ ${layoutMixins.column}
+ gap: 1rem;
+`;
diff --git a/src/views/dialogs/RestrictedGeoDialog.tsx b/src/views/dialogs/RestrictedGeoDialog.tsx
new file mode 100644
index 0000000..54b3dd5
--- /dev/null
+++ b/src/views/dialogs/RestrictedGeoDialog.tsx
@@ -0,0 +1,42 @@
+import styled, { AnyStyledComponent } from 'styled-components';
+
+import { STRING_KEYS } from '@/constants/localization';
+import { useStringGetter } from '@/hooks';
+import { layoutMixins } from '@/styles/layoutMixins';
+
+import { Dialog } from '@/components/Dialog';
+import { Icon, IconName } from '@/components/Icon';
+
+type ElementProps = {
+ preventClose?: boolean;
+ setIsOpen?: (open: boolean) => void;
+};
+
+export const RestrictedGeoDialog = ({ preventClose, setIsOpen }: ElementProps) => {
+ const stringGetter = useStringGetter();
+
+ return (
+ }
+ >
+
+ {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
+
+
+ );
+};
+
+const Styled: Record = {};
+
+Styled.Icon = styled(Icon)`
+ color: var(--color-warning);
+`;
+
+Styled.Content = styled.div`
+ ${layoutMixins.column}
+ gap: 1rem;
+`;
diff --git a/src/views/dialogs/RestrictedWalletDialog.tsx b/src/views/dialogs/RestrictedWalletDialog.tsx
new file mode 100644
index 0000000..d3a06ac
--- /dev/null
+++ b/src/views/dialogs/RestrictedWalletDialog.tsx
@@ -0,0 +1,42 @@
+import styled, { AnyStyledComponent } from 'styled-components';
+
+import { STRING_KEYS } from '@/constants/localization';
+import { useStringGetter } from '@/hooks';
+import { layoutMixins } from '@/styles/layoutMixins';
+
+import { Dialog } from '@/components/Dialog';
+import { Icon, IconName } from '@/components/Icon';
+
+type ElementProps = {
+ preventClose?: boolean;
+ setIsOpen?: (open: boolean) => void;
+};
+
+export const RestrictedWalletDialog = ({ preventClose, setIsOpen }: ElementProps) => {
+ const stringGetter = useStringGetter();
+
+ return (
+ }
+ >
+
+ {stringGetter({ key: STRING_KEYS.REGION_NOT_PERMITTED_SUBTITLE })}
+
+
+ );
+};
+
+const Styled: Record = {};
+
+Styled.Icon = styled(Icon)`
+ color: var(--color-warning);
+`;
+
+Styled.Content = styled.div`
+ ${layoutMixins.column}
+ gap: 1rem;
+`;
diff --git a/src/views/forms/AccountManagementForms/WithdrawForm.tsx b/src/views/forms/AccountManagementForms/WithdrawForm.tsx
index f24b628..81cc8e9 100644
--- a/src/views/forms/AccountManagementForms/WithdrawForm.tsx
+++ b/src/views/forms/AccountManagementForms/WithdrawForm.tsx
@@ -10,9 +10,16 @@ import { TransferInputField, TransferInputTokenResource, TransferType } from '@/
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
-import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
+import { NumberSign } from '@/constants/numbers';
-import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
+import {
+ useAccounts,
+ useDebounce,
+ useDydxClient,
+ useRestrictions,
+ useStringGetter,
+ useSubaccount,
+} from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -39,10 +46,11 @@ import { MustBigNumber } from '@/lib/numbers';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
+import { join } from 'path';
export const WithdrawForm = () => {
const stringGetter = useStringGetter();
- const [error, setError] = useState(null);
+ const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(false);
const { sendSquidWithdraw } = useSubaccount();
@@ -71,9 +79,11 @@ export const WithdrawForm = () => {
const { addTransferNotification } = useLocalNotifications();
// Async Data
- const debouncedAmountBN = MustBigNumber(debouncedAmount);
- const withdrawAmountBN = MustBigNumber(withdrawAmount);
- const freeCollateralBN = MustBigNumber(freeCollateral?.current);
+ const debouncedAmountBN = useMemo(() => MustBigNumber(debouncedAmount), [debouncedAmount]);
+ const freeCollateralBN = useMemo(
+ () => MustBigNumber(freeCollateral?.current),
+ [freeCollateral?.current]
+ );
useEffect(() => {
abacusStateManager.setTransferValue({
@@ -105,50 +115,85 @@ export const WithdrawForm = () => {
value: debouncedAmount,
field: TransferInputField.usdcSize,
});
- setError(null);
+ setError(undefined);
}
} catch (error) {
- setError(error);
+ setError(error.message);
} finally {
setIsLoading(false);
}
};
setTransferValue();
- }, [debouncedAmountBN.toNumber()]);
+ }, [debouncedAmountBN]);
+
+ const { screenAddresses } = useDydxClient();
+ const { dydxAddress } = useAccounts();
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
e.preventDefault();
- if (!requestPayload?.data || !debouncedAmountBN.toNumber()) {
+ if (!requestPayload?.data || !debouncedAmountBN.toNumber() || !toAddress || !dydxAddress) {
throw new Error('Invalid request payload');
}
setIsLoading(true);
- const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload?.data);
+ setError(undefined);
- if (txHash?.hash) {
- const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
+ const screenResults = await screenAddresses({
+ addresses: [toAddress, dydxAddress],
+ });
- addTransferNotification({
- txHash: hash,
- fromChainId: TESTNET_CHAIN_ID,
- toChainId: chainIdStr || undefined,
- toAmount: debouncedAmountBN.toNumber(),
- triggeredAt: Date.now(),
- });
- abacusStateManager.clearTransferInputValues();
- setWithdrawAmount('');
+ if (screenResults?.[dydxAddress]) {
+ setError(
+ stringGetter({
+ key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_ORIGINATION_ERROR_MESSAGE,
+ })
+ );
+ } else if (screenResults?.[toAddress]) {
+ setError(
+ stringGetter({
+ key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_DESTINATION_ERROR_MESSAGE,
+ })
+ );
+ } else {
+ const txHash = await sendSquidWithdraw(debouncedAmountBN.toNumber(), requestPayload.data);
+ if (txHash?.hash) {
+ const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
+ addTransferNotification({
+ txHash: hash,
+ fromChainId: TESTNET_CHAIN_ID,
+ toChainId: chainIdStr || undefined,
+ toAmount: debouncedAmountBN.toNumber(),
+ triggeredAt: Date.now(),
+ });
+ abacusStateManager.clearTransferInputValues();
+ setWithdrawAmount('');
+ }
}
} catch (error) {
- setError(error);
+ if (error?.code === 429) {
+ setError(stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE }));
+ } else {
+ setError(
+ error.message
+ ? stringGetter({
+ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
+ params: {
+ ERROR_MESSAGE:
+ error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
+ },
+ })
+ : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG })
+ );
+ }
} finally {
setIsLoading(false);
}
},
- [requestPayload, debouncedAmountBN, chainIdStr]
+ [requestPayload, debouncedAmountBN, chainIdStr, toAddress, screenAddresses, stringGetter]
);
const onChangeAddress = useCallback((e: ChangeEvent) => {
@@ -179,7 +224,7 @@ export const WithdrawForm = () => {
const onClickMax = useCallback(() => {
setWithdrawAmount(freeCollateralBN.toString());
- }, [freeCollateral?.current, setWithdrawAmount]);
+ }, [freeCollateralBN, setWithdrawAmount]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
@@ -215,7 +260,7 @@ export const WithdrawForm = () => {
value={freeCollateral?.current}
newValue={freeCollateral?.postOrder}
sign={NumberSign.Negative}
- hasInvalidNewValue={withdrawAmountBN.minus(freeCollateralBN).isNegative()}
+ hasInvalidNewValue={MustBigNumber(withdrawAmount).minus(freeCollateralBN).isNegative()}
withDiff={
Boolean(withdrawAmount) && !debouncedAmountBN.isNaN() && !debouncedAmountBN.isZero()
}
@@ -224,18 +269,23 @@ export const WithdrawForm = () => {
},
];
+ const { sanctionedAddresses } = useRestrictions();
+
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 });
+ return stringGetter({
+ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
+ params: { ERROR_MESSAGE: error },
+ });
}
if (!toAddress) return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ADDRESS });
+ if (sanctionedAddresses.has(toAddress))
+ return stringGetter({
+ key: STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS,
+ });
+
if (debouncedAmountBN) {
if (!chainIdStr) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN });
@@ -249,7 +299,15 @@ export const WithdrawForm = () => {
}
return undefined;
- }, [error, freeCollateralBN, chainIdStr, debouncedAmountBN, toToken]);
+ }, [
+ error,
+ freeCollateralBN,
+ chainIdStr,
+ debouncedAmountBN,
+ toToken,
+ toAddress,
+ sanctionedAddresses,
+ ]);
const isDisabled =
!!errorMessage ||
@@ -257,7 +315,8 @@ export const WithdrawForm = () => {
!chainIdStr ||
!toAddress ||
debouncedAmountBN.isNaN() ||
- debouncedAmountBN.isZero();
+ debouncedAmountBN.isZero() ||
+ isLoading;
return (
diff --git a/src/views/forms/TransferForm.tsx b/src/views/forms/TransferForm.tsx
index 1b19036..1af5694 100644
--- a/src/views/forms/TransferForm.tsx
+++ b/src/views/forms/TransferForm.tsx
@@ -3,8 +3,6 @@ import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import type { SyntheticInputEvent } from 'react-number-format/types/types';
-import { debounce } from 'lodash';
-import { StdFee } from '@cosmjs/stargate';
import { validation } from '@dydxprotocol/v4-client-js';
import { TransferInputField, TransferType } from '@/constants/abacus';
@@ -12,7 +10,7 @@ import { AlertType } from '@/constants/alerts';
import { ButtonShape, ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
-import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
+import { NumberSign } from '@/constants/numbers';
import {
DYDX_CHAIN_ASSET_COIN_DENOM,
DYDX_CHAIN_ASSET_TAGS,
@@ -22,6 +20,8 @@ import {
import {
useAccountBalance,
useAccounts,
+ useDydxClient,
+ useRestrictions,
useSelectedNetwork,
useStringGetter,
useSubaccount,
@@ -77,7 +77,7 @@ export const TransferForm = ({
const [asset, setAsset] = useState(selectedAsset);
// Form states
- const [error, setError] = useState();
+ const [error, setError] = useState();
const [isLoading, setIsLoading] = useState(false);
const balance = asset === DydxChainAsset.USDC ? freeCollateral?.current : nativeTokenBalance;
@@ -96,16 +96,6 @@ export const TransferForm = ({
const balanceBN = MustBigNumber(balance);
const newBalanceBN = MustBigNumber(newBalance);
- const isAddressValid = useMemo(
- () =>
- recipientAddress &&
- dydxAddress !== recipientAddress &&
- validation.isValidAddress(recipientAddress),
- [recipientAddress]
- );
-
- const isAmountValid = balance && amount && amountBN.gt(0) && newBalanceBN.gte(0);
-
useEffect(() => {
abacusStateManager.setTransferValue({
value: TransferType.transferOut.rawValue,
@@ -125,6 +115,21 @@ export const TransferForm = ({
});
}, [asset]);
+ const { sanctionedAddresses } = useRestrictions();
+
+ const isAddressValid = useMemo(
+ () =>
+ recipientAddress &&
+ dydxAddress !== recipientAddress &&
+ validation.isValidAddress(recipientAddress) &&
+ !sanctionedAddresses.has(recipientAddress),
+ [recipientAddress, sanctionedAddresses, dydxAddress]
+ );
+
+ const isAmountValid = balance && amount && amountBN.gt(0) && newBalanceBN.gte(0);
+
+ const { screenAddresses } = useDydxClient();
+
const onTransfer = async () => {
if (!isAmountValid || !isAddressValid || !fee) return;
setIsLoading(true);
@@ -136,20 +141,51 @@ export const TransferForm = ({
asset === DydxChainAsset.DYDX ? amountBN.minus(fee) : amountBN
).toNumber();
- const txResponse = await transfer(
- amountToTransfer,
- recipientAddress as string,
- DYDX_CHAIN_ASSET_COIN_DENOM[asset]
- );
+ const screenResults = await screenAddresses({
+ addresses: [recipientAddress!, dydxAddress!],
+ });
- if (txResponse?.code === 0) {
- console.log('TransferForm > txReceipt > ', txResponse?.hash);
- onDone?.();
+ if (screenResults?.[dydxAddress!]) {
+ setError(
+ stringGetter({
+ key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_ORIGINATION_ERROR_MESSAGE,
+ })
+ );
+ } else if (screenResults?.[recipientAddress!]) {
+ setError(
+ stringGetter({
+ key: STRING_KEYS.WALLET_RESTRICTED_WITHDRAWAL_TRANSFER_DESTINATION_ERROR_MESSAGE,
+ })
+ );
} else {
- throw new Error(txResponse?.rawLog ?? 'Transaction did not commit.');
+ const txResponse = await transfer(
+ amountToTransfer,
+ recipientAddress as string,
+ DYDX_CHAIN_ASSET_COIN_DENOM[asset]
+ );
+
+ if (txResponse?.code === 0) {
+ console.log('TransferForm > txReceipt > ', txResponse?.hash);
+ onDone?.();
+ } else {
+ throw new Error(txResponse?.rawLog ?? 'Transaction did not commit.');
+ }
}
} catch (error) {
- setError(error);
+ if (error?.code === 429) {
+ setError(stringGetter({ key: STRING_KEYS.RATE_LIMIT_REACHED_ERROR_MESSAGE }));
+ } else {
+ setError(
+ error.message
+ ? stringGetter({
+ key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
+ params: {
+ ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
+ },
+ })
+ : stringGetter({ key: STRING_KEYS.SOMETHING_WENT_WRONG })
+ );
+ }
log('TransferForm/onTransfer', error);
} finally {
setIsLoading(false);
@@ -231,13 +267,6 @@ export const TransferForm = ({
},
];
- const addressValidationErrorMessage = stringGetter({
- key:
- dydxAddress === recipientAddress
- ? STRING_KEYS.TRANSFER_TO_YOURSELF
- : STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS,
- });
-
const renderFormInputButton = ({
label,
isInputEmpty,
@@ -302,7 +331,12 @@ export const TransferForm = ({
{recipientAddress && !isAddressValid && (
- {addressValidationErrorMessage}
+ {stringGetter({
+ key:
+ dydxAddress === recipientAddress
+ ? STRING_KEYS.TRANSFER_TO_YOURSELF
+ : STRING_KEYS.TRANSFER_INVALID_DYDX_ADDRESS,
+ })}
)}
@@ -342,13 +376,13 @@ export const TransferForm = ({
)}
- {error && {error.message}}
+ {error && {error}}