Compare commits

..
Author SHA1 Message Date
Bill He d17a9005c0 bump v4-client 2023-09-08 16:10:16 -07:00
James Jia - Test 4b86068d8f Initial commit 2023-09-08 13:52:13 -07:00
75 changed files with 1448 additions and 2120 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
# Contributing to Trader-Fe
# Contributing to V4-web
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
@@ -18,10 +18,10 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu
4. Issue that pull request!
## Any contributions you make will be under the same License
When you submit code changes, your submissions are understood to be under the same [License](https://github.com/dydxprotocol/trader-fe/blob/master/LICENSE) that covers the project.
When you submit code changes, your submissions are understood to be under the same [License](https://github.com/dydxprotocol/v4-web/blob/master/LICENSE) that covers the project.
## Report bugs using Github's [issues](https://github.com/dydxprotocol/trader-fe/issues)
Report a bug by [opening a new issue](https://github.com/dydxprotocol/trader-fe/issues/new).
## Report bugs using Github's [issues](https://github.com/dydxprotocol/v4-web/issues)
Report a bug by [opening a new issue](https://github.com/dydxprotocol/v4-web/issues/new).
**Great Bug Reports** tend to have:
+9 -6
View File
@@ -1,16 +1,19 @@
Business Source License 1.1
License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
Copyright (C) 2023 dYdX Trading Inc.
Subject to your compliance with applicable law, you are granted the right to use the Licensed Work (defined below) under the terms of the below licenses; provided, however, that if you violate any such applicable law in your use of the Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Licensed Work will automatically and immediately terminate.
Use of this software before the Change Date specified in the LICENSE file is governed by the Business Source License included in the LICENSE file and at https://spdx.org/licenses/BUSL-1.1.html.
On and after the Change Date specified in the LICENSE file, use of this software will be governed by the Change License (GNU Affero GPL v3) as specified in the LICENSE file.
Business Source License 1.1
Business Source License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
Copyright (C) 2023 dYdX Trading Inc.
Parameters
Licensor: dYdX Trading Inc.
Licensed Work: dydxprotocol/cosmos-sdk, dydxprotocol/cometbft, dydxprotocol/v4-chain, dydxprotocol/v4-clients, dydxprotocol/v4-web, dydxprotocol/v4-abacus, dydxprotocol/v4-localization, dydxprotocol/v4-documentation, and any dYdX or dYdX Trading Inc. github repository reflecting a copy of this license, or link to this license.
Additional Use Grant: None
Change Date: On the later to occur of (i) the release of the Licensed Work labeled “dYdX Version 1.0 GPL”, and which includes a document uploaded to the relevant github repository stating “The software in this repository is Version 1.0, subject to GPL license”, and (ii) September 30, 2023 (approximate projected release of “Version 1.0” of this software).
Change Date: On the later to occur of (i) the release of the Licensed Work labeled “dYdX Version 1.0 Affero GPL”, and which includes a document uploaded to the relevant github repository stating “The software in this repository is Version 1.0, subject to Affero GPL license”, and (ii) September 30, 2023 (approximate projected release of “Version 1.0” of this software).
Change License: GNU Affero GPL License v3
License text copyright © 2023 MariaDB plc, All Rights Reserved. “Business Source License” is a trademark of MariaDB plc.
@@ -39,7 +42,7 @@ OPEN SOURCE LICENSE BELOW - TO BE APPLIED AFTER THE CHANGE DATE.
Copyright (C) 2023 dYdX Trading Inc.
Use of this software is governed by the The GNU Affero General Public License as set forth below.
Subject to your compliance with applicable law, you are granted the right to use the Licensed Work (defined below) under the terms of the GNU Affero General Public License as set forth below; provided, however, that if you violate any such applicable law in your use of the Licensed Work, all of your rights and licenses to use (including any rights to reproduce, distribute, install or modify) the Licensed Work will automatically and immediately terminate.
The GNU Affero General Public License
@@ -835,4 +838,4 @@ copy of the Program in return for a fee.
For more information about this software, see https://dydx.exchange.
Copyright (C) 2023 dYdX Trading Inc.
Copyright (C) 2023 dYdX Trading Inc.
+46 -2
View File
@@ -22,8 +22,8 @@ Alternatively, follow the [IPFS Kubo installation guide](https://docs.ipfs.tech
Clone the repository and navigate to its directory:
```bash
git clone https://github.com/dydxprotocol/trader-fe.git
cd trader-fe
git clone https://github.com/dydxprotocol/v4-web.git
cd v4-web
```
### Step 2: Install pnpm and dependencies
@@ -102,3 +102,47 @@ To access your content on IPFS:
2. **Public IPFS gateway:** Access your content via a public IPFS gateway, such as [https://dweb.link](https://dweb.link/) or [https://w3s.link/](https://w3s.link/). Use the gateway URL with your CID appended, like `https://dweb.link/ipfs/your_cid`.
Replace `your_cid` with the actual CID.
## Part 5: Customization
### Using CircularXX font
1. **Procure a license for CircularXX:** Visit [Lineto](https://lineto.com/shop/select?family=circular&set=DOPhtVQR-8m) and add `Circular/Family Package` + `Circular Mono/Regular` to your cart. Check the applicable boxes in the `Configure` step to fill out licensing information. After the `Payment` step, download the `.woff2` files.
2. **Add the fonts to `v4-web`:** Add `CircularXXWeb-Bold.woff2`, `CircularXXWeb-Book.woff2`, `CircularXXWeb-Medium.woff2`, and `CircularXXMonoWeb-Regular.woff2` to the `src/styles/fonts` directory.
3. **Add font-family to stylesheets:** In `src/styles/fonts.css`, add the following code to import your newly purchased `Circular` font.
```css
@font-face {
src: url("fonts/CircularXXWeb-Book.woff2") format("woff2");
font-family: "Circular";
font-weight: 450;
}
@font-face {
src: url("fonts/CircularXXWeb-Medium.woff2") format("woff2");
font-family: "Circular";
font-weight: 500;
}
@font-face {
src: url("fonts/CircularXXWeb-Bold.woff2") format("woff2");
font-family: "Circular";
font-weight: 700;
}
@font-face {
src: url("fonts/CircularXXMonoWeb-Regular.woff2") format("woff2");
font-family: "CircularMono";
font-weight: 400;
}
```
In `src/styles/text.css`, update lines `4` and `5` to
```css
--fontFamily-base: "Circular", "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: "CircularMono", Courier, monospace, var(--fontFamily-base);
```
Enjoy the newly installed font.
+4 -4
View File
@@ -30,16 +30,16 @@
},
"packageManager": "pnpm@8.6.6",
"dependencies": {
"@0xsquid/sdk": "^1.7.2",
"@0xsquid/sdk": "^1.10.0",
"@cosmjs/amino": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@dydxprotocol/abacus": "^0.4.1",
"@dydxprotocol/v4-client-js": "^0.32.0",
"@dydxprotocol/v4-localization": "^0.0.21",
"@dydxprotocol/abacus": "^0.4.16",
"@dydxprotocol/v4-client-js": "^0.33.1",
"@dydxprotocol/v4-localization": "^0.0.25",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-collapsible": "^1.0.3",
+223 -1802
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+30 -25
View File
@@ -13,8 +13,9 @@ import { AccountsProvider } from '@/hooks/useAccounts';
import { DialogAreaProvider, useDialogArea } from './hooks/useDialogArea';
import { LocaleProvider } from './hooks/useLocaleSeparators';
import { NotificationsProvider } from './hooks/useNotifications';
import { LocalNotificationsProvider } from './hooks/useLocalNotifications';
import { SubaccountProvider } from './hooks/useSubaccount';
import { SquidProvider } from '@/hooks/useSquidRouter';
import { SquidProvider } from '@/hooks/useSquid';
import { GuardedMobileRoute } from '@/components/GuardedMobileRoute';
@@ -98,29 +99,31 @@ const Content = () => {
);
};
const App = () => (
<QueryClientProvider client={queryClient}>
<GrazProvider>
<WagmiConfig config={config}>
<LocaleProvider>
<DydxProvider>
<AccountsProvider>
<SubaccountProvider>
<NotificationsProvider>
<DialogAreaProvider>
<SquidProvider>
<Content />
</SquidProvider>
</DialogAreaProvider>
</NotificationsProvider>
</SubaccountProvider>
</AccountsProvider>
</DydxProvider>
</LocaleProvider>
</WagmiConfig>
</GrazProvider>
</QueryClientProvider>
);
const wrapProvider = (Component: React.ComponentType<any>, props?: any) => {
return ({ children }: { children: React.ReactNode }) => (
<Component {...props}>{children}</Component>
);
};
const providers = [
wrapProvider(QueryClientProvider, { client: queryClient }),
wrapProvider(GrazProvider),
wrapProvider(WagmiConfig, { config }),
wrapProvider(LocaleProvider),
wrapProvider(DydxProvider),
wrapProvider(AccountsProvider),
wrapProvider(SubaccountProvider),
wrapProvider(SquidProvider),
wrapProvider(LocalNotificationsProvider),
wrapProvider(NotificationsProvider),
wrapProvider(DialogAreaProvider),
];
const App = () => {
return [...providers].reverse().reduce((children, Provider) => {
return <Provider>{children}</Provider>;
}, <Content />);
};
const Styled: Record<string, AnyStyledComponent> = {};
@@ -192,7 +195,9 @@ Styled.NotificationsToastArea = styled(NotificationsToastArea)`
`;
Styled.DialogArea = styled.aside`
position: absolute;
position: fixed;
height: 100vh;
z-index: 1;
inset: 0;
overflow: clip;
${layoutMixins.noPointerEvents}
+222
View File
@@ -0,0 +1,222 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]
+14 -4
View File
@@ -82,7 +82,9 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Item
// value={item.value} // search by both value and textContent
// value={[group.groupLabel, item.label, item.tag].filter(Boolean).join('|')} // exclude item.value from searchable terms (not guaranteed to be unique)
value={[group.groupLabel, item.value, item.description, item.label, item.tag].filter(Boolean).join('|')}
value={[group.groupLabel, item.value, item.description, item.label, item.tag]
.filter(Boolean)
.join('|')}
onSelect={() => {
if (item.subitems) {
// open submenu
@@ -116,7 +118,15 @@ export const ComboboxMenu = <MenuItemValue extends string, MenuGroupValue extend
<Styled.Item
// value={subitem.value} // search by both value and textContent
// value={[group.groupLabel, item.label, subitem.label, subitem.tag].filter(Boolean).join('|')}
value={[group.groupLabel, item.value, item.description, item.label, item.tag].filter(Boolean).join('|')}
value={[
group.groupLabel,
item.value,
item.description,
item.label,
item.tag,
]
.filter(Boolean)
.join('|')}
onSelect={() => {
subitem.onSelect?.(subitem.value);
onItemSelected?.();
@@ -168,7 +178,7 @@ Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>`
background-color: var(--comboboxMenu-backgroundColor);
border-radius: inherit;
input:focus {
input:focus-visible {
outline: none;
}
@@ -272,7 +282,7 @@ Styled.Item = styled(Command.Item)`
align-items: center;
gap: 0.5rem;
&[aria-disabled="true"] {
&[aria-disabled='true'] {
opacity: 0.75;
cursor: not-allowed;
}
+2 -5
View File
@@ -90,10 +90,7 @@ export const Dialog = ({
return (
<Root modal={showOverlay} open={isOpen} onOpenChange={setIsOpen}>
{slotTrigger && <Trigger asChild>{slotTrigger}</Trigger>}
<DialogPortal
withPortal={placement !== DialogPlacement.Inline}
container={portalContainer}
>
<DialogPortal withPortal={placement !== DialogPlacement.Inline} container={portalContainer}>
{showOverlay && <Styled.Overlay />}
<Styled.Container
placement={placement}
@@ -425,7 +422,7 @@ Styled.Close = styled(Close)`
}
&:hover,
&:focus {
&:focus-visible {
color: var(--color-text-2);
}
+1 -1
View File
@@ -137,7 +137,7 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
--stickyArea1-topHeight: var(--form-input-height);
input:focus {
input:focus-visible {
outline: none;
}
+2 -2
View File
@@ -743,7 +743,7 @@ Styled.Tr = styled.tr<{
cursor: pointer;
&:hover,
&:focus,
&:focus-visible,
&:focus-within {
--tableRow-currentBackgroundColor: var(--tableRow-hover-backgroundColor);
filter: brightness(1.1);
@@ -753,7 +753,7 @@ Styled.Tr = styled.tr<{
${({ withFocusStickyRows }) =>
withFocusStickyRows &&
css`
&:focus,
&:focus-visible,
&:focus-within {
${layoutMixins.sticky}
z-index: 1;
+13 -11
View File
@@ -17,6 +17,7 @@ type ElementProps = {
slotIcon?: React.ReactNode;
slotTitle?: React.ReactNode;
slotDescription?: React.ReactNode;
slotCustomContent?: React.ReactNode;
slotAction?: React.ReactNode;
actionDescription?: string;
actionAltText?: string;
@@ -33,6 +34,7 @@ export const Toast = ({
slotIcon,
slotTitle,
slotDescription,
slotCustomContent,
slotAction,
actionDescription = '',
actionAltText = actionDescription,
@@ -78,12 +80,13 @@ export const Toast = ({
<$Title>{slotTitle}</$Title>
</$Header>
<$Description>{slotDescription}</$Description>
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
{!slotCustomContent && <$Description>{slotDescription}</$Description>}
{slotCustomContent}
{actionDescription && (
<$Action asChild altText={actionAltText}>
{slotAction}
</$Action>
)}
</$Container>
</div>
</$Root>
@@ -118,8 +121,7 @@ const $Root = styled(Root)`
align-items: end;
transform-origin: left bottom;
animation:
${keyframes`
animation: ${keyframes`
from {
/* scale: 0; */
grid-template-rows: 0fr; // height transition
@@ -150,7 +152,7 @@ const $Root = styled(Root)`
}
}
&:focus {
&:focus-visible {
outline: none;
}
@@ -203,8 +205,8 @@ const $Container = styled.div`
// Rules
${popoverMixins.popover}
padding: 1rem;
box-shadow:
0 0 0 var(--border-width) var(--color-border), // border
box-shadow: 0 0 0 var(--border-width) var(--color-border),
// border
0 0 0.5rem 0.1rem var(--color-layer-2); // shadow
${$Root}:focus:not([data-swipe='end']) & {
+1 -1
View File
@@ -91,7 +91,7 @@ const Styled: Record<string, AnyStyledComponent> = {};
Styled.Content = styled(Content)`
z-index: 1;
&:focus {
&:focus-visible {
outline: none;
}
`;
+2 -2
View File
@@ -49,11 +49,11 @@ export enum EvmDerivedAccountStatus {
Derived,
}
import type { DydxAddress, EthereumAddress } from './wallets';
import type { DydxAddress, EvmAddress } from './wallets';
export type EvmDerivedAddresses = {
version?: string;
[EthereumAddress: EthereumAddress]: {
[EvmAddress: EvmAddress]: {
encryptedSignature?: string;
dydxAddress?: DydxAddress;
};
+3 -2
View File
@@ -1,7 +1,7 @@
import type { SupportedLocales } from './localization';
import type { DydxNetwork } from './networks';
import type { OnboardingState, OnboardingSteps } from './account';
import type { DydxAddress, WalletType, WalletConnectionType, EthereumAddress } from './wallets';
import type { DydxAddress, WalletType, WalletConnectionType, EvmAddress } from './wallets';
import type { DialogTypes } from './dialogs';
import type { TradeTypes } from './trade';
import type { AbacusApiStatus, HumanReadablePlaceOrderPayload } from './abacus';
@@ -40,7 +40,7 @@ export type AnalyticsUserPropertyValue<T extends AnalyticsUserProperty> =
: T extends AnalyticsUserProperty.WalletConnectionType
? WalletConnectionType | undefined
: T extends AnalyticsUserProperty.WalletAddress
? EthereumAddress | DydxAddress | undefined
? EvmAddress | DydxAddress | undefined
: // Account
T extends AnalyticsUserProperty.DydxAddress
? DydxAddress | undefined
@@ -67,6 +67,7 @@ export enum AnalyticsEvent {
// Onboarding
OnboardingStepChanged = 'OnboardingStepChanged',
OnboardingAccountDerived = 'OnboardingAccountDerived',
OnboardingWalletIsNonDeterministic = 'OnboardingWalletIsNonDeterministic',
// Transfers
TransferFaucet = 'TransferFaucet',
+1
View File
@@ -12,6 +12,7 @@ export enum LocalStorageKey {
NotificationsLastUpdated = 'dydx.NotificationsLastUpdated',
PushNotificationsEnabled = 'dydx.PushNotificationsEnabled',
PushNotificationsLastUpdated = 'dydx.PushNotificationsLastUpdated',
TransferNotifications = 'dydx.TransferNotifications',
// UI State
LastViewedMarket = 'dydx.LastViewedMarket',
+15 -13
View File
@@ -34,6 +34,8 @@ export const STRING_KEYS = {
...WARNINGS_STRING_KEYS,
};
export type StringKey = keyof typeof STRING_KEYS;
export type LocaleData = typeof EN_LOCALE_DATA;
export type StringGetterFunction = (a: {
@@ -88,17 +90,17 @@ export type TooltipStrings = {
};
export const ORDER_ERROR_CODE_MAP: { [key: number]: string } = {
2000: '2000_FILL_OR_KILL_ORDER_COULD_NOT_BE_FULLY_FILLED',
2001: '2001_REDUCE_ONLY_WOULD_INCREASE_POSITION_SIZE',
2002: '2002_REDUCE_ONLY_WOULD_CHANGE_POSITION_SIDE',
2003: '2003_POST_ONLY_WOULD_CROSS_MAKER_ORDER',
3000: '3000_INVALID_ORDER_FLAGS',
3001: '3001_INVALID_STATEFUL_ORDER_GOOD_TIL_BLOCK_TIME',
3002: '3002_STATEFUL_ORDERS_CANNOT_REQUIRE_IMMEDIATE_EXECUTION',
3003: '3003_TIME_EXCEEDS_GOOD_TIL_BLOCK_TIME',
3004: '3004_GOOD_TIL_BLOCK_TIME_EXCEEDS_STATEFUL_ORDER_TIME_WINDOW',
3005: '3005_STATEFUL_ORDER_ALREADY_EXISTS',
3006: '3006_STATEFUL_ORDER_DOES_NOT_EXIST',
3007: '3007_STATEFUL_ORDER_COLLATERALIZATION_CHECK_FAILED',
3008: '3008_STATEFUL_ORDER_PREVIOUSLY_CANCELLED',
2000: STRING_KEYS['2000_FILL_OR_KILL_ORDER_COULD_NOT_BE_FULLY_FILLED'],
2001: STRING_KEYS['2001_REDUCE_ONLY_WOULD_INCREASE_POSITION_SIZE'],
2002: STRING_KEYS['2002_REDUCE_ONLY_WOULD_CHANGE_POSITION_SIDE'],
2003: STRING_KEYS['2003_POST_ONLY_WOULD_CROSS_MAKER_ORDER'],
3000: STRING_KEYS['3000_INVALID_ORDER_FLAGS'],
3001: STRING_KEYS['3001_INVALID_STATEFUL_ORDER_GOOD_TIL_BLOCK_TIME'],
3002: STRING_KEYS['3002_STATEFUL_ORDERS_CANNOT_REQUIRE_IMMEDIATE_EXECUTION'],
3003: STRING_KEYS['3003_TIME_EXCEEDS_GOOD_TIL_BLOCK_TIME'],
3004: STRING_KEYS['3004_GOOD_TIL_BLOCK_TIME_EXCEEDS_STATEFUL_ORDER_TIME_WINDOW'],
3005: STRING_KEYS['3005_STATEFUL_ORDER_ALREADY_EXISTS'],
3006: STRING_KEYS['3006_STATEFUL_ORDER_DOES_NOT_EXIST'],
3007: STRING_KEYS['3007_STATEFUL_ORDER_COLLATERALIZATION_CHECK_FAILED'],
3008: STRING_KEYS['3008_STATEFUL_ORDER_PREVIOUSLY_CANCELLED'],
};
-30
View File
@@ -1,36 +1,6 @@
import { Asset, PerpetualMarket } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization';
export interface Market {
market: string;
status: string;
baseAsset: string;
quoteAsset: string;
stepSize: string;
tickSize: string;
oraclePrice: string;
priceChange24H: string;
nextFundingRate: string;
nextFundingAt: string;
minOrderSize: string;
type: string;
initialMarginFraction: string;
maintenanceMarginFraction: string;
volume24H: string;
trades24H: string;
openInterest: string;
incrementalInitialMarginFraction: string;
incrementalPositionSize: string;
maxPositionSize: string;
baselinePositionSize: string;
assetResolution: string;
syntheticAssetId: string;
}
export type AllMarkets = {
[market: string]: Market;
};
export type MarketData = {
asset: Asset;
tickSizeDecimals: number;
+29 -6
View File
@@ -1,16 +1,22 @@
import { StatusResponse } from "@0xsquid/sdk";
/** implemented in useNotificationTypes */
export enum NotificationType {
OrderStatusChanged = 'OrderStatusChanged',
SquidTransfer = 'SquidTransfer',
}
export enum NotificationComponentType {}
export type NotificationId = string | number
export type NotificationId = string | number;
export type NotificationTypeConfig<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
export type NotificationTypeConfig<
_NotificationId extends NotificationId = string,
NotificationUpdateKey = any
> = {
type: NotificationType;
/** React hook to trigger notifications based on app state */
/** React hook to trigger notifications based on app state */
useTrigger: (_: {
trigger: (
/** Unique ID for the triggered notification */
@@ -30,7 +36,7 @@ export type NotificationTypeConfig<_NotificationId extends NotificationId = stri
* @param true (default): Notification initialized with status NotificationStatus.Triggered
* @param false: Notification initialized with status NotificationStatus.Cleared
*/
isNew?: boolean,
isNew?: boolean
) => void;
lastUpdated: number;
@@ -58,7 +64,10 @@ export enum NotificationStatus {
}
/** Notification state. Serialized and cached into localStorage. */
export type Notification<_NotificationId extends NotificationId = string, NotificationUpdateKey = any> = {
export type Notification<
_NotificationId extends NotificationId = string,
NotificationUpdateKey = any
> = {
id: _NotificationId;
type: NotificationType;
status: NotificationStatus;
@@ -74,7 +83,11 @@ export type NotificationDisplayData = {
title?: string;
description?: string;
description?: React.ReactNode;
customContent?: React.ReactNode;
customMenuContent?: React.ReactNode;
actionDescription?: string;
@@ -101,3 +114,13 @@ export type NotificationDisplayData = {
*/
toastDuration?: number;
};
// Notification types
export type TransferNotifcation = {
txHash: string;
toChainId?: string;
fromChainId?: string;
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
+13 -1
View File
@@ -32,6 +32,18 @@ export enum WalletConnectionType {
WalletConnect2 = 'walletConnect2',
}
export enum WalletErrorType {
// General
ChainMismatch,
UserCanceled,
// Non-Deterministic
NonDeterministicWallet,
// Misc
Unknown,
}
type WalletConnectionTypeConfig = {
name: string;
wagmiConnectorId?: string;
@@ -325,7 +337,7 @@ export const SIGN_TYPED_DATA = {
export type PrivateInformation = ReturnType<typeof onboarding.deriveHDKeyFromEthereumSignature>;
export type EthereumAddress = `0x${string}`;
export type EvmAddress = `0x${string}`;
export type DydxAddress = `dydx${string}`;
export const DYDX_CHAIN_INFO: Parameters<typeof suggestChain>[0] = {
+7 -2
View File
@@ -7,7 +7,7 @@ import { formatUnits } from 'viem';
import { CLIENT_NETWORK_CONFIGS } from '@/constants/networks';
import { QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { EthereumAddress } from '@/constants/wallets';
import { EvmAddress } from '@/constants/wallets';
import { convertBech32Address } from '@/lib/addressUtils';
import { MustBigNumber } from '@/lib/numbers';
@@ -16,6 +16,7 @@ import { getSelectedNetwork } from '@/state/appSelectors';
import { useAccounts } from './useAccounts';
import { usePollNativeTokenBalance } from './usePollNativeTokenBalance';
import { usePollUSDCBalance } from './usePollUSDCBalance';
type UseAccountBalanceProps = {
// Token Items
@@ -58,7 +59,7 @@ export const useAccountBalance = ({
token:
addressOrDenom === CHAIN_DEFAULT_TOKEN_ADDRESS
? undefined
: (addressOrDenom as EthereumAddress),
: (addressOrDenom as EvmAddress),
watch: true,
});
@@ -96,9 +97,13 @@ export const useAccountBalance = ({
.div(QUANTUM_MULTIPLIER)
.toNumber();
const usdcCoinBalance = usePollUSDCBalance({ dydxAddress });
const usdcBalance = MustBigNumber(usdcCoinBalance?.amount).div(QUANTUM_MULTIPLIER).toNumber();
return {
balance,
nativeTokenBalance,
usdcBalance,
queryStatus: isCosmosChain ? cosmosQuery.status : evmQuery.status,
isQueryFetching: isCosmosChain ? cosmosQuery.isFetching : evmQuery.fetchStatus === 'fetching',
};
+3 -3
View File
@@ -6,7 +6,7 @@ import { LocalWallet, USDC_DENOM, type Subaccount } from '@dydxprotocol/v4-clien
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EthereumAddress, PrivateInformation } from '@/constants/wallets';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets';
import {
setOnboardingState,
@@ -82,7 +82,7 @@ const useAccountsContext = () => {
evmAddress,
dydxAddress,
}: {
evmAddress: EthereumAddress;
evmAddress: EvmAddress;
dydxAddress?: DydxAddress;
}) => {
saveEvmDerivedAddresses({
@@ -192,7 +192,7 @@ const useAccountsContext = () => {
} catch (error) {
log('useAccounts/setLocalDydxWallet', error);
}
} else if (evmAddress && signerWagmi) {
} else if (evmAddress) {
if (!localDydxWallet) {
dispatch(setOnboardingState(OnboardingState.WalletConnected));
+1 -1
View File
@@ -4,7 +4,7 @@ import { useMatch, useNavigate } from 'react-router-dom';
import { LocalStorageKey } from '@/constants/localStorage';
import { DEFAULT_MARKETID } from '@/constants/markets';
import { AppRoute, DEFAULT_TRADE_ROUTE } from '@/constants/routes';
import { AppRoute } from '@/constants/routes';
import { getSelectedNetwork } from '@/state/appSelectors';
import { closeDialogInTradeBox } from '@/state/dialogs';
+7 -4
View File
@@ -57,8 +57,8 @@ const useDydxClientContext = () => {
networkConfig?.indexerSocketUrl &&
networkConfig?.validatorUrl
) {
setCompositeClient(
await CompositeClient.connect(
try {
const initializedClient = await CompositeClient.connect(
new Network(
selectedNetwork,
new IndexerConfig(networkConfig.indexerUrl, networkConfig.indexerSocketUrl),
@@ -67,8 +67,11 @@ const useDydxClientContext = () => {
broadcastTimeoutMs: 60_000,
})
)
)
);
);
setCompositeClient(initializedClient);
} catch (error) {
log('useDydxClient/initializeCompositeClient', error);
}
} else {
setCompositeClient(undefined);
}
+98
View File
@@ -0,0 +1,98 @@
import { createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import type { StatusResponse } from '@0xsquid/sdk';
import { LocalStorageKey } from '@/constants/localStorage';
import { type TransferNotifcation } from '@/constants/notifications';
import { useAccounts } from '@/hooks/useAccounts';
import { useSquid } from '@/hooks/useSquid';
import { useLocalStorage } from './useLocalStorage';
const LocalNotificationsContext = createContext<
ReturnType<typeof useLocalNotificationsContext> | undefined
>(undefined);
LocalNotificationsContext.displayName = 'LocalNotifications';
export const LocalNotificationsProvider = ({ ...props }) => (
<LocalNotificationsContext.Provider value={useLocalNotificationsContext()} {...props} />
);
export const useLocalNotifications = () => useContext(LocalNotificationsContext)!;
const TRANSFER_STATUS_FETCH_INTERVAL = 10_000;
const useLocalNotificationsContext = () => {
// transfer notifications
const [allTransferNotifications, setAllTransferNotifications] = useLocalStorage<{
[key: `dydx${string}`]: TransferNotifcation[];
}>({
key: LocalStorageKey.TransferNotifications,
defaultValue: {},
});
const { dydxAddress } = useAccounts();
const transferNotifications = dydxAddress ? allTransferNotifications[dydxAddress] || [] : [];
const setTransferNotifications = useCallback(
(notifications: TransferNotifcation[]) => {
if (!dydxAddress) return;
const updatedNotifications = { ...allTransferNotifications };
updatedNotifications[dydxAddress] = notifications;
setAllTransferNotifications(updatedNotifications);
},
[setAllTransferNotifications, dydxAddress]
);
const addTransferNotification = useCallback(
(notification: TransferNotifcation) =>
setTransferNotifications([...transferNotifications, notification]),
[transferNotifications]
);
const squid = useSquid();
const { data: transferStatuses } = useQuery({
queryKey: ['getTransactionStatus', transferNotifications],
queryFn: async () => {
const statuses: { [key: string]: StatusResponse } = {};
for (const {
txHash,
toChainId,
fromChainId,
status: currentStatus,
} of transferNotifications) {
try {
if (currentStatus && currentStatus?.squidTransactionStatus !== 'ongoing') continue;
const status = await squid?.getStatus({ transactionId: txHash, toChainId, fromChainId });
if (status) statuses[txHash] = status;
} catch (error) {
console.error(error);
}
}
return statuses;
},
refetchInterval: TRANSFER_STATUS_FETCH_INTERVAL,
});
useEffect(() => {
if (!transferStatuses) return;
const newTransferNotifications = transferNotifications.map((notification) => {
const status = transferStatuses[notification.txHash];
if (!status) return notification;
return {
...notification,
status,
};
});
setTransferNotifications(newTransferNotifications);
}, [transferStatuses]);
return {
transferNotifications,
addTransferNotification,
};
};
+56 -1
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector, shallowEqual, useDispatch } from 'react-redux';
import { groupBy } from 'lodash';
@@ -8,12 +8,20 @@ import { STRING_KEYS } from '@/constants/localization';
import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications';
import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { Icon, IconName } from '@/components/Icon';
import { Output, OutputType } from '@/components/Output';
import { TransferStatusToast } from '@/views/TransferStatus';
import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors';
import { openDialog } from '@/state/dialogs';
import { OrderStatusIcon } from '@/views/OrderStatusIcon';
import { useStringGetter } from './useStringGetter';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
export const notificationTypes = [
{
@@ -93,4 +101,51 @@ export const notificationTypes = [
};
},
} as NotificationTypeConfig<string, [string, number]>,
{
type: NotificationType.SquidTransfer,
useTrigger: ({ trigger }) => {
const stringGetter = useStringGetter();
const { transferNotifications } = useLocalNotifications();
const getTitleStringKey = useCallback((type: 'deposit' | 'withdraw', finished: boolean) => {
if (type === 'deposit' && !finished) return STRING_KEYS.DEPOSIT_IN_PROGRESS;
if (type === 'deposit' && finished) return STRING_KEYS.DEPOSIT;
if (type === 'withdraw' && !finished) return STRING_KEYS.WITHDRAW_IN_PROGRESS;
return STRING_KEYS.WITHDRAW;
}, []);
useEffect(() => {
for (const transfer of transferNotifications) {
const { toChainId, status, txHash, toAmount } = transfer;
const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw';
trigger(
txHash,
{
icon: <Icon iconName={finished ? IconName.Transfer : IconName.Clock} />,
title: stringGetter({ key: getTitleStringKey(type, finished) }),
// TODO: confirm with design what the description should be
description: (
<>
<span>{type === 'deposit' ? 'Deposit of ' : 'Withdraw of'}</span>
<Output type={OutputType.Fiat} value={toAmount} />
</>
),
customContent: (
<TransferStatusToast
toAmount={transfer.toAmount}
triggeredAt={transfer.triggeredAt}
status={transfer.status}
/>
),
customMenuContent: !finished && <TransferStatusSteps status={transfer.status} />,
toastSensitivity: 'foreground',
},
[]
);
}
}, [transferNotifications]);
},
},
] satisfies NotificationTypeConfig[];
+7 -3
View File
@@ -106,7 +106,10 @@ const useNotificationsContext = () => {
updateKey,
} as Notification);
updateStatus(notification, isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared);
updateStatus(
notification,
isNew ? NotificationStatus.Triggered : NotificationStatus.Cleared
);
}
// updateKey changed - update existing notification
@@ -141,7 +144,9 @@ const useNotificationsContext = () => {
// Push notifications
const [hasEnabledPush, setHasEnabledPush] = useLocalStorage({
key: LocalStorageKey.PushNotificationsEnabled,
defaultValue: Boolean(globalThis.Notification && globalThis.Notification.permission === 'granted'),
defaultValue: Boolean(
globalThis.Notification && globalThis.Notification.permission === 'granted'
),
});
const [isEnablingPush, setIsEnablingPush] = useState(false);
@@ -236,4 +241,3 @@ const useNotificationsContext = () => {
setIsMenuOpen,
};
};
// }
@@ -1,17 +1,13 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { Squid } from '@0xsquid/sdk';
import { DydxV4Network, isDydxV4Network } from '@/constants/networks';
import { CLIENT_NETWORK_CONFIGS, DydxV4Network, isDydxV4Network } from '@/constants/networks';
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',
};
export const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
const useSquidContext = () => {
const selectedNetwork = useSelector(getSelectedNetwork);
@@ -27,7 +23,7 @@ const useSquidContext = () => {
const squid = useMemo(
() =>
isDydxV4Network(selectedNetwork)
? new Squid({ baseUrl: SQUID_BASE_URL[selectedNetwork as DydxV4Network] })
? new Squid({ baseUrl: CLIENT_NETWORK_CONFIGS[selectedNetwork]?.endpoints['0xsquid'] })
: undefined,
[selectedNetwork]
);
+2 -1
View File
@@ -551,7 +551,8 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo
} as HumanReadablePlaceOrderPayload & { isClosePosition: boolean });
onSuccess?.();
} catch (error) {
const errorStringKey = error?.code && ORDER_ERROR_CODE_MAP[error.code as number];
const errorCode: number | undefined = error?.code;
const errorStringKey = errorCode ? ORDER_ERROR_CODE_MAP[errorCode] : undefined;
onError?.({ errorStringKey });
log('useSubaccount/placeOrder', error, {
+19 -10
View File
@@ -4,17 +4,20 @@ import { LocalStorageKey } from '@/constants/localStorage';
import {
type DydxAddress,
type EthereumAddress,
type EvmAddress,
WalletConnectionType,
WalletType,
wallets,
DYDX_CHAIN_INFO,
} from '@/constants/wallets';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import {
useConnect as useConnectWagmi,
useAccount as useAccountWagmi,
useDisconnect as useDisconnectWagmi,
usePublicClient as usePublicClientWagmi,
useWalletClient as useWalletClientWagmi,
} from 'wagmi';
import {
@@ -26,21 +29,21 @@ import {
} from 'graz';
import { resolveWagmiConnector } from '@/lib/wagmi';
import { getWalletConnection } from '@/lib/wallet';
import { getWalletConnection, parseWalletError } from '@/lib/wallet';
import { log } from '@/lib/telemetry';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { useStringGetter } from './useStringGetter';
export const useWalletConnection = () => {
const stringGetter = useStringGetter();
// EVM wallet connection
const [evmAddress, saveEvmAddress] = useLocalStorage<EthereumAddress | undefined>({
const [evmAddress, saveEvmAddress] = useLocalStorage<EvmAddress | undefined>({
key: LocalStorageKey.EvmAddress,
defaultValue: undefined,
});
const { address: evmAddressWagmi, isConnected: isConnectedWagmi } = useAccountWagmi();
const publicClientWagmi = usePublicClientWagmi();
const { data: signerWagmi } = useWalletClientWagmi();
const { disconnectAsync: disconnectWagmi } = useDisconnectWagmi();
@@ -129,8 +132,6 @@ export const useWalletConnection = () => {
}
}
} catch (error) {
log('useWalletConnection/connectWallet', error);
throw Object.assign(
new Error([error.message, error.cause?.message].filter(Boolean).join('\n')),
{
@@ -158,7 +159,7 @@ export const useWalletConnection = () => {
// Wallet selection
const [selectedWalletType, setSelectedWalletType] = useState<WalletType | undefined>(walletType);
const [selectedWalletError, setSelectedWalletError] = useState<Error>();
const [selectedWalletError, setSelectedWalletError] = useState<string>();
useEffect(() => {
(async () => {
@@ -173,8 +174,15 @@ export const useWalletConnection = () => {
setWalletType(walletType);
setWalletConnectionType(walletConnectionType);
} catch (error) {
log('useWalletConnection/connectWallet', error);
setSelectedWalletError(error);
const { walletErrorType, message } = parseWalletError({
error,
stringGetter,
});
if (message) {
log('useWalletConnection/connectWallet', error, { walletErrorType });
setSelectedWalletError(message);
}
}
} else {
setWalletType(undefined);
@@ -208,7 +216,8 @@ export const useWalletConnection = () => {
evmAddress,
evmAddressWagmi,
signerWagmi,
publicClientWagmi,
// Wallet connection (Cosmos)
dydxAddress,
dydxAddressGraz,
+1 -1
View File
@@ -54,7 +54,7 @@ button:disabled {
cursor: pointer;
}
:focus {
:focus-visible{
outline: var(--color-accent) 1px solid;
outline-offset: -1px;
/* outline-offset: -2px; */
+1
View File
@@ -41,6 +41,7 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
slotIcon={displayData.icon}
slotTitle={displayData.title}
slotDescription={displayData.description}
slotCustomContent={displayData.customContent}
slotAction={
<Button size={ButtonSize.Small} onClick={() => onNotificationAction(notification)}>
{displayData.actionDescription}
+1
View File
@@ -153,6 +153,7 @@ class AbacusStateManager {
// ------ Set Data ------ //
setStore = (store: RootStore) => {
this.store = store;
this.stateNotifier.setStore(store);
};
+6
View File
@@ -72,3 +72,9 @@ export const getTimeTillNextUnit = (unit: 'minute' | 'hour' | 'day') => {
};
export const getTimeString = (time: number) => time.toString().padStart(2, '0');
export const formatSeconds = (seconds: number) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${getTimeString(minutes)}:${getTimeString(remainingSeconds)}`;
}
+66
View File
@@ -1,7 +1,10 @@
import { STRING_KEYS, StringGetterFunction } from '@/constants/localization';
import {
type WalletConnection,
wallets,
WalletConnectionType,
WalletErrorType,
WalletType,
} from '@/constants/wallets';
@@ -68,3 +71,66 @@ export const getWalletConnection = ({
}
}
};
export const getWalletErrorType = ({ error }: { error: Error }) => {
const { message } = error;
const messageLower = message.toLowerCase();
// General - Cancelled
if (
messageLower.includes('connection request reset') ||
messageLower.includes('rejected') ||
messageLower.includes('reject') ||
messageLower.includes('cancelled') ||
messageLower.includes('canceled') ||
messageLower.includes('user denied')
) {
return WalletErrorType.UserCanceled;
}
if (messageLower.includes('chain mismatch')) {
return WalletErrorType.ChainMismatch;
}
// ImToken - User canceled
if (messageLower.includes('用户取消了操作')) {
return WalletErrorType.UserCanceled;
}
if (messageLower.includes('does not support deterministic signing')) {
return WalletErrorType.NonDeterministicWallet;
}
return WalletErrorType.Unknown;
};
export const parseWalletError = ({
error,
stringGetter,
}: {
error: Error;
stringGetter: StringGetterFunction;
}) => {
const walletErrorType = getWalletErrorType({ error });
let message;
switch (walletErrorType) {
case WalletErrorType.ChainMismatch:
case WalletErrorType.UserCanceled: {
break;
}
default: {
message = stringGetter({
key: STRING_KEYS.SOMETHING_WENT_WRONG_WITH_MESSAGE,
params: {
ERROR_MESSAGE: error.message || stringGetter({ key: STRING_KEYS.UNKNOWN_ERROR }),
},
});
}
}
return {
walletErrorType,
message,
};
};
+1 -1
View File
@@ -49,7 +49,7 @@ export const DYDXBalancePanel = () => {
size={ButtonSize.Small}
onClick={() => dispatch(openDialog({ type: DialogTypes.Receive }))}
>
{stringGetter({ key: STRING_KEYS.EXCHANGE_RECEIVED })}
{stringGetter({ key: STRING_KEYS.RECEIVE })}
</Styled.ReceiveButton>
<Button
slotLeft={<Icon iconName={IconName.Send} />}
+26 -22
View File
@@ -1,38 +1,42 @@
/*
* Legal Disclaimer for Web licence
/**
* @license
*
* Licence to use self-hosted webfonts for displaying dynamic text on specified website domains. Our package includes WOFF and WOFF2 font formats.
* Font Family: Satoshi
* Designed by: Deni Anggara
* URL: https://www.fontshare.com/fonts/satoshi
* © 2023 Indian Type Foundry
*
* Should the license page views limit be exceeded the license holder will be required to contact Lineto.com for an upgrade.
*
* It is strictly prohibited to rename the font and to download or use these fonts in any other media.
*
* These Web fonts are licensed exclusively for the use on the following domain(s) and their subdomains:
*
* dydx.exchange (monthly pageviews: <50K)
*
*/
* Font Styles:
* Satoshi Regular
* Satoshi Medium
* Satoshi Bold
* Satoshi Black
*/
@font-face {
src: url("fonts/CircularXXWeb-Book.woff2") format("woff2");
font-family: "Circular";
font-weight: 450;
src: url("fonts/Satoshi-Regular.woff2") format("woff2");
font-family: "Satoshi";
font-weight: 400;
}
@font-face {
src: url("fonts/CircularXXWeb-Medium.woff2") format("woff2");
font-family: "Circular";
src: url("fonts/Satoshi-Medium.woff2") format("woff2");
font-family: "Satoshi";
font-weight: 500;
}
@font-face {
src: url("fonts/CircularXXWeb-Bold.woff2") format("woff2");
font-family: "Circular";
src: url("fonts/Satoshi-Bold.woff2") format("woff2");
font-family: "Satoshi";
font-weight: 700;
}
@font-face {
src: url("fonts/CircularXXMonoWeb-Regular.woff2") format("woff2");
font-family: "CircularMono";
font-weight: 400;
src: url("fonts/Satoshi-Black.woff2") format("woff2");
font-family: "Satoshi";
font-weight: 900;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -214,7 +214,7 @@ export const popoverMixins = {
/* &:hover, */
/* :not(:has(* > [data-highlighted])) > &:hover, */
&[data-radix-collection-item]:hover, // @radix-ui/react-navigation-menu
/* &:focus, */
/* &:focus-visible, */
&[aria-selected="true"], // cmdk
&[data-highlighted] // @radix-ui
{
+2 -8
View File
@@ -1,22 +1,16 @@
/* Constants */
:root {
--fontFamily-base: "Circular", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: "CircularMono", Courier, monospace, var(--fontFamily-base);
--fontFamily-base: "Satoshi", system-ui, -apple-system, Helvetica, Arial, sans-serif;
--fontFamily-monospace: Courier, monospace, var(--fontFamily-base);
--fontWeight-base-0: 450;
--fontWeight-base-1: 500;
--fontWeight-base-2: 700;
--fontWeight-monospace: 400;
--letter-spacing-base: -0.02em;
/* New Typography system */
/**
* @size 10px/11px
* @weight 400
*/
--fontSize-tiny: 0.625rem; /* 10px/11px */
--fontSize-mini: 0.75rem; /* 12px/13px */
--fontSize-small: 0.8438rem; /* 13.5px/14.5px */
+22 -39
View File
@@ -2,7 +2,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import styled, { type AnyStyledComponent } from 'styled-components';
import { ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { STRING_KEYS, type StringKey } from '@/constants/localization';
import { useBreakpoints, useStringGetter } from '@/hooks';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -34,10 +34,7 @@ export const MarketDetails: React.FC = () => {
initialMarginFraction,
maintenanceMarginFraction,
minOrderSize,
maxPositionSize,
baselinePositionSize,
incrementalInitialMarginFraction,
incrementalPositionSize,
stepSizeDecimals,
tickSizeDecimals,
} = configs;
@@ -72,12 +69,28 @@ export const MarketDetails: React.FC = () => {
key: 'step-size',
label: stringGetter({ key: STRING_KEYS.STEP_SIZE }),
tooltip: 'step-size',
value: <Output useGrouping value={stepSize} type={OutputType.Asset} tag={symbol} />,
value: (
<Output
useGrouping
value={stepSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
},
{
key: 'min-order-size',
label: stringGetter({ key: STRING_KEYS.MINIMUM_ORDER_SIZE }),
value: <Output useGrouping value={minOrderSize} type={OutputType.Asset} tag={symbol} />,
value: (
<Output
useGrouping
value={minOrderSize}
type={OutputType.Asset}
tag={symbol}
fractionDigits={stepSizeDecimals}
/>
),
},
{
key: 'max-leverage',
@@ -105,36 +118,6 @@ export const MarketDetails: React.FC = () => {
tooltip: 'initial-margin-fraction',
value: <Output useGrouping value={initialMarginFraction} type={OutputType.SmallPercent} />,
},
{
key: 'incremental-initial-margin-fraction',
label: stringGetter({ key: STRING_KEYS.INCREMENTAL_INITIAL_MARGIN_FRACTION }),
value: (
<Output
useGrouping
value={incrementalInitialMarginFraction}
type={OutputType.SmallPercent}
/>
),
},
{
key: 'incremental-position-size',
label: stringGetter({ key: STRING_KEYS.INCREMENTAL_POSITION_SIZE }),
value: (
<Output useGrouping value={incrementalPositionSize} type={OutputType.Asset} tag={symbol} />
),
},
{
key: 'baseline-position-size',
label: stringGetter({ key: STRING_KEYS.BASELINE_POSITION_SIZE }),
value: (
<Output useGrouping value={baselinePositionSize} type={OutputType.Asset} tag={symbol} />
),
},
{
key: 'max-position-size',
label: stringGetter({ key: STRING_KEYS.MAXIMUM_POSITION_SIZE }),
value: <Output useGrouping value={maxPositionSize} type={OutputType.Asset} tag={symbol} />,
},
];
return (
@@ -150,10 +133,10 @@ export const MarketDetails: React.FC = () => {
<Styled.MarketDescription>
{primaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey] })}</p>
<p>{stringGetter({ key: STRING_KEYS[primaryDescriptionKey as StringKey] })}</p>
)}
{secondaryDescriptionKey && (
<p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey] })}</p>
<p>{stringGetter({ key: STRING_KEYS[secondaryDescriptionKey as StringKey] })}</p>
)}
</Styled.MarketDescription>
+1 -1
View File
@@ -304,7 +304,7 @@ Styled.Popover = styled(Popover)`
}
`} 0.2s;
}
&:focus {
&:focus-visible {
outline: none;
}
`;
+2 -2
View File
@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react';
import { useInterval } from '@/hooks';
import { getTimeTillNextUnit, getTimeString } from '@/lib/timeUtils';
import { getTimeTillNextUnit, formatSeconds } from '@/lib/timeUtils';
import { Output, OutputType } from '@/components/Output';
export const NextFundingTimer = () => {
@@ -18,7 +18,7 @@ export const NextFundingTimer = () => {
type={OutputType.Text}
value={
secondsLeft !== undefined
? `${getTimeString(Math.floor(secondsLeft / 60))}:${getTimeString(secondsLeft % 60)}`
? formatSeconds(secondsLeft)
: undefined
}
/>
+156
View File
@@ -0,0 +1,156 @@
import { useCallback, useState, useMemo } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { Root, Trigger, Content } from '@radix-ui/react-collapsible';
import { StatusResponse } from '@0xsquid/sdk';
import { useInterval, useStringGetter } from '@/hooks';
import { STRING_KEYS } from '@/constants/localization';
import { formatSeconds } from '@/lib/timeUtils';
import { Output, OutputType } from '@/components/Output';
import { WithReceipt } from '@/components/WithReceipt';
import { Icon, IconName } from '@/components/Icon';
import { TransferStatusSteps } from '@/views/TransferStatusSteps';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { layoutMixins } from '@/styles/layoutMixins';
type ElementProps = {
toAmount?: number;
triggeredAt?: number;
status?: StatusResponse;
};
export const TransferStatusToast = ({
toAmount,
triggeredAt = Date.now(),
status,
}: ElementProps) => {
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number | undefined>();
const type = useMemo(
() => (status?.toChain?.chainData?.chainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal'),
[status]
);
const updateSecondsLeft = useCallback(() => {
const fromChainEta = (status?.fromChain?.chainData?.estimatedRouteDuration || 0) * 1000;
const toChainEta = (status?.toChain?.chainData?.estimatedRouteDuration || 0) * 1000;
setSecondsLeft(Math.floor((triggeredAt + fromChainEta + toChainEta - Date.now()) / 1000));
}, [status]);
useInterval({ callback: updateSecondsLeft });
if (!status) return <LoadingDots size={3} />;
return (
<Styled.Root open={open} onOpenChange={setOpen}>
<WithReceipt
hideReceipt={!open}
side="bottom"
slotReceipt={
<Styled.Receipt>
<TransferStatusSteps status={status} />
</Styled.Receipt>
}
>
<Styled.BridgingStatus>
<Styled.Status>
{stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_STATUS : STRING_KEYS.WITHDRAW_STATUS,
params: {
AMOUNT_USD: <Styled.InlineOutput type={OutputType.Fiat} value={toAmount} />,
ESTIMATED_DURATION: (
<Styled.InlineOutput
type={OutputType.Text}
value={formatSeconds(Math.max(secondsLeft || 0, 0))}
/>
),
},
})}
</Styled.Status>
<Styled.Trigger>
<Styled.TriggerIcon>
<Icon iconName={IconName.Caret} />
</Styled.TriggerIcon>
{stringGetter({ key: open ? STRING_KEYS.HIDE_DETAILS : STRING_KEYS.VIEW_DETAILS })}
</Styled.Trigger>
</Styled.BridgingStatus>
</WithReceipt>
</Styled.Root>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.Root = styled(Root)`
margin: 0.5rem -1rem -1rem -1rem;
color: var(--color-text-0);
font: var(--font-small-book);
`;
Styled.Receipt = styled.div`
padding: 0 1rem;
`;
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
background-color: var(--color-layer-3);
gap: 0.5rem;
padding: 0 1rem 1rem 1rem;
border-radius: 0.5rem;
`;
Styled.Status = styled.div`
color: var(--color-text-0);
font-size: 0.875rem;
`;
Styled.InlineOutput = styled(Output)`
display: inline-block;
color: var(--color-text-1);
`;
Styled.Content = styled(Content)`
${layoutMixins.flexColumn};
gap: 0.5rem;
padding: 1rem;
`;
Styled.Step = styled.div`
${layoutMixins.row};
gap: 0.5rem;
`;
Styled.Trigger = styled(Trigger)`
display: flex;
align-items: center;
gap: 0.5em;
color: var(--color-accent);
user-select: none;
&:focus {
outline: none;
}
`;
Styled.TriggerIcon = styled.span`
width: 0.75em;
display: inline-flex;
transition: transform 0.3s var(--ease-out-expo);
${Styled.Trigger}[data-state='open'] & {
rotate: -0.5turn;
}
`;
+175
View File
@@ -0,0 +1,175 @@
import { useMemo } from 'react';
import styled, { css, keyframes, type AnyStyledComponent } from 'styled-components';
import { StatusResponse } from '@0xsquid/sdk';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { useStringGetter } from '@/hooks';
import { Link } from '@/components/Link';
import { Icon, IconName } from '@/components/Icon';
import { LoadingDots } from '@/components/Loading/LoadingDots';
import { LoadingSpinner } from '@/components/Loading/LoadingSpinner';
import { layoutMixins } from '@/styles/layoutMixins';
import { STRING_KEYS } from '@/constants/localization';
type ElementProps = {
status?: StatusResponse;
};
enum TransferStatusStep {
FromChain,
Bridge,
ToChain,
Complete,
}
export const TransferStatusSteps = ({ status }: ElementProps) => {
const stringGetter = useStringGetter();
const { currentStep, steps, type } = useMemo(() => {
const routeStatus = status?.routeStatus;
const fromChain = status?.fromChain?.chainData?.chainId;
const toChain = status?.toChain?.chainData?.chainId;
const type = toChain === TESTNET_CHAIN_ID ? 'deposit' : 'withdrawal';
const steps = [
{
label: stringGetter({
key:
type === 'deposit' ? STRING_KEYS.INITIATED_DEPOSIT : STRING_KEYS.INITIATED_WITHDRAWAL,
}),
step: TransferStatusStep.FromChain,
link: status?.fromChain?.transactionUrl,
},
{
label: stringGetter({ key: STRING_KEYS.BRIDGING_TOKENS }),
step: TransferStatusStep.Bridge,
link: status?.axelarTransactionUrl,
},
{
label: stringGetter({
key: type === 'deposit' ? STRING_KEYS.DEPOSIT_TO_CHAIN : STRING_KEYS.WITHDRAW_TO_CHAIN,
params: {
CHAIN: status?.toChain?.chainData?.chainName,
},
}),
step: TransferStatusStep.ToChain,
link: status?.toChain?.transactionUrl,
},
];
const currentStatus = routeStatus[routeStatus?.length - 1];
let currentStep = TransferStatusStep.Bridge;
if (!routeStatus?.length) {
currentStep = TransferStatusStep.FromChain;
} else if (currentStatus.chainId === toChain) {
currentStep =
currentStatus.status !== 'success'
? TransferStatusStep.ToChain
: TransferStatusStep.Complete;
} else if (currentStatus.chainId === fromChain && currentStatus.status !== 'success') {
currentStep = TransferStatusStep.FromChain;
}
return {
currentStep,
steps,
type,
};
}, [status, stringGetter]);
if (!status) return <LoadingDots size={3} />;
return (
<Styled.BridgingStatus>
{steps.map((step) => (
<Styled.Step key={step.step}>
<Styled.row>
{step.step === currentStep ? (
<Styled.Icon>
<Styled.Spinner />
</Styled.Icon>
) : step.step < currentStep ? (
<Styled.Icon state="complete">
<Icon iconName={IconName.Check} />
</Styled.Icon>
) : (
<Styled.Icon state="default">{step.step + 1}</Styled.Icon>
)}
{step.link && currentStep >= step.step ? (
<Link href={step.link}>
<Styled.Label highlighted={currentStep >= step.step}>
{step.label}
<Icon iconName={IconName.LinkOut} />
</Styled.Label>
</Link>
) : (
<Styled.Label highlighted={currentStep >= step.step}>{step.label}</Styled.Label>
)}
</Styled.row>
</Styled.Step>
))}
</Styled.BridgingStatus>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.BridgingStatus = styled.div`
${layoutMixins.flexColumn};
gap: 1rem;
padding: 1rem 0;
`;
Styled.Step = styled.div`
${layoutMixins.spacedRow};
`;
Styled.row = styled.div`
${layoutMixins.inlineRow};
gap: 0.5rem;
`;
Styled.Icon = styled.div<{ state: 'complete' | 'default' }>`
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
width: 2rem;
border-radius: 50%;
background-color: var(--color-layer-3);
${({ state }) =>
({
['complete']: css`
color: var(--color-positive);
`,
['default']: css`
color: var(--color-text-0);
`,
}[state])}
`;
Styled.Spinner = styled(LoadingSpinner)`
--spinner-width: 1.25rem;
color: var(--color-accent);
`;
Styled.Label = styled(Styled.row)<{ highlighted?: boolean }>`
${({ highlighted }) =>
highlighted
? css`
color: var(--color-text-2);
`
: css`
color: var(--color-text-0);
`}
`;
@@ -32,8 +32,12 @@ export const ChooseWallet = () => {
<>
{selectedWalletType && selectedWalletError && (
<Styled.AlertMessage type={AlertType.Error}>
{<h4>Couldn't connect to {stringGetter({ key: wallets[selectedWalletType].stringKey })}.</h4>}
<code>{selectedWalletError.message}</code>
{
<h4>
Couldn't connect to {stringGetter({ key: wallets[selectedWalletType].stringKey })}.
</h4>
}
{selectedWalletError}
</Styled.AlertMessage>
)}
@@ -25,11 +25,12 @@ import { Switch } from '@/components/Switch';
import { WithReceipt } from '@/components/WithReceipt';
import { WithTooltip } from '@/components/WithTooltip';
import { getSelectedNetwork } from '@/state/appSelectors';
import { track } from '@/lib/analytics';
import { isTruthy } from '@/lib/isTruthy';
import { log } from '@/lib/telemetry';
import { getSelectedNetwork } from '@/state/appSelectors';
import { parseWalletError } from '@/lib/wallet';
type ElementProps = {
status: EvmDerivedAccountStatus;
@@ -66,8 +67,12 @@ export const GenerateKeys = ({
try {
await matchNetwork?.();
} catch (error) {
setError(error.message);
log('GenerateKeys/switchNetwork', error);
const { message, walletErrorType } = parseWalletError({ error, stringGetter });
if (message) {
log('GenerateKeys/switchNetwork', error, { walletErrorType });
setError(message);
}
}
};
@@ -124,7 +129,12 @@ export const GenerateKeys = ({
}
}
} catch (error) {
log('GenerateKeys/getSubaccounts', error);
const { message } = parseWalletError({ error, stringGetter });
if (message) {
track(AnalyticsEvent.OnboardingWalletIsNonDeterministic);
setError(message);
}
}
await setWalletFromEvmSignature(signature);
@@ -140,48 +150,48 @@ export const GenerateKeys = ({
setStatus(EvmDerivedAccountStatus.Derived);
} catch (error) {
setStatus(EvmDerivedAccountStatus.NotDerived);
setError(error?.message);
const { message, walletErrorType } = parseWalletError({ error, stringGetter });
log('GenerateKeys/deriveKeys', error);
throw error;
if (message) {
setError(message);
log('GenerateKeys/deriveKeys', error, { walletErrorType });
}
}
};
return (
<>
{isMobile && (
<Styled.MobileStatusCards>
{[
{
status: EvmDerivedAccountStatus.Deriving,
title: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
description: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
},
status === EvmDerivedAccountStatus.EnsuringDeterminism && {
status: EvmDerivedAccountStatus.EnsuringDeterminism,
title: stringGetter({ key: STRING_KEYS.VERIFY_WALLET_COMPATIBILITY }),
description: stringGetter({ key: STRING_KEYS.ENSURES_WALLET_SUPPORT }),
},
]
.filter(isTruthy)
.map((step) => (
<Styled.StatusCard key={step.status} active={status === step.status}>
{status < step.status ? (
<LoadingSpinner disabled />
) : status === step.status ? (
<LoadingSpinner />
) : (
<Styled.GreenCheckCircle />
)}
<div>
<h3>{step.title}</h3>
<p>{step.description}</p>
</div>
</Styled.StatusCard>
))}
</Styled.MobileStatusCards>
)}
<Styled.StatusCardsContainer>
{[
{
status: EvmDerivedAccountStatus.Deriving,
title: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
description: stringGetter({ key: STRING_KEYS.GENERATE_COSMOS_WALLET }),
},
status === EvmDerivedAccountStatus.EnsuringDeterminism && {
status: EvmDerivedAccountStatus.EnsuringDeterminism,
title: stringGetter({ key: STRING_KEYS.VERIFY_WALLET_COMPATIBILITY }),
description: stringGetter({ key: STRING_KEYS.ENSURES_WALLET_SUPPORT }),
},
]
.filter(isTruthy)
.map((step) => (
<Styled.StatusCard key={step.status} active={status === step.status}>
{status < step.status ? (
<LoadingSpinner disabled />
) : status === step.status ? (
<LoadingSpinner />
) : (
<Styled.GreenCheckCircle />
)}
<div>
<h3>{step.title}</h3>
<p>{step.description}</p>
</div>
</Styled.StatusCard>
))}
</Styled.StatusCardsContainer>
<Styled.Footer>
<Styled.RememberMe htmlFor="remember-me">
<WithTooltip withIcon tooltip="remember-me">
@@ -251,7 +261,7 @@ export const GenerateKeys = ({
const Styled: Record<string, AnyStyledComponent> = {};
Styled.MobileStatusCards = styled.div`
Styled.StatusCardsContainer = styled.div`
display: grid;
margin-top: 1rem;
gap: 1rem;
@@ -3,16 +3,21 @@ import styled, { type AnyStyledComponent } from 'styled-components';
import { type NumberFormatValues } from 'react-number-format';
import { shallowEqual, useSelector } from 'react-redux';
import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js';
import { ethers } from 'ethers';
import { parseUnits } from 'viem'
import erc20 from '@/abi/erc20.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { NumberSign } from '@/constants/numbers';
import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter } from '@/hooks';
import { useAccountBalance } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { NATIVE_TOKEN_ADDRESS, useSquid } from '@/hooks/useSquid';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -32,6 +37,7 @@ import { getTransferInputs } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
import { log } from '@/lib/telemetry';
import { ChainSelectMenu } from './ChainSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu';
@@ -48,13 +54,17 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { signerWagmi } = useAccounts();
const { evmAddress, signerWagmi } = useAccounts();
const { publicClientWagmi } = useWalletConnection();
const { addTransferNotification } = useLocalNotifications();
const {
requestPayload,
token,
chain: chainIdStr,
resources,
summary,
} = useSelector(getTransferInputs, shallowEqual) || {};
const chainId = chainIdStr ? parseInt(chainIdStr) : undefined;
@@ -64,13 +74,16 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
[token, resources]
);
const sourceChain = useMemo(
() => (chainIdStr ? resources?.chainResources?.get(chainIdStr) : undefined),
[chainId, 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,
@@ -157,6 +170,39 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
}
}, [balance, setFromAmount]);
const validateTokenApproval = useCallback(async () => {
if (!signerWagmi || !publicClientWagmi) throw new Error('Missing signer');
if (!sourceToken?.address || !sourceToken.decimals) throw new Error('Missing source token address');
if (!sourceChain?.rpc) throw new Error('Missing source chain rpc');
if (!requestPayload?.targetAddress) throw new Error('Missing target address');
if (!requestPayload?.value) throw new Error('Missing transaction value');
if (sourceToken?.address === NATIVE_TOKEN_ADDRESS) return;
const allowance = await publicClientWagmi.readContract({
address: sourceToken.address as EvmAddress,
abi: erc20,
functionName: 'allowance',
args: [evmAddress as EvmAddress, requestPayload.targetAddress as EvmAddress]
});
const sourceAmountBN = parseUnits(debouncedAmount, sourceToken.decimals);
if (sourceAmountBN > (allowance as bigint)) {
const { request } = await publicClientWagmi.simulateContract({
account: evmAddress,
address: sourceToken.address as EvmAddress,
abi: erc20,
functionName: 'approve',
args: [requestPayload.targetAddress as EvmAddress, sourceAmountBN],
})
const approveTx = await signerWagmi.writeContract(request);
await publicClientWagmi.waitForTransactionReceipt({
hash: approveTx,
})
}
}, [signerWagmi, sourceToken, sourceChain, requestPayload, publicClientWagmi]);
const onSubmit = useCallback(
async (e: FormEvent) => {
try {
@@ -178,27 +224,32 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
setIsLoading(true);
await validateTokenApproval();
let tx = {
to: requestPayload.targetAddress as `0x${string}`,
data: requestPayload.data as `0x${string}`,
gasLimit: ethers.toBigInt(requestPayload.gasLimit),
to: requestPayload.targetAddress as EvmAddress,
data: requestPayload.data as EvmAddress,
gasLimit: BigInt(requestPayload.gasLimit),
value:
requestPayload.routeType !== 'SEND' ? ethers.toBigInt(requestPayload.value) : undefined,
requestPayload.routeType !== 'SEND' ? BigInt(requestPayload.value) : undefined,
};
const txHash = await signerWagmi.sendTransaction(tx);
onDeposit?.();
if (txHash) {
setTransactionHash(txHash);
abacusStateManager.setTransferStatus({
hash: txHash,
addTransferNotification({
txHash: txHash,
toChainId: TESTNET_CHAIN_ID,
fromChainId: chainId?.toString(),
fromChainId: chainIdStr || undefined,
toAmount: summary?.usdcSize || undefined,
triggeredAt: Date.now(),
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
}
} catch (error) {
log('DepositForm/onSubmit', error);
setError(error);
} finally {
setIsLoading(false);
@@ -286,17 +337,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
}
/>
</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>
</AlertMessage>
)
)}
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
@@ -12,6 +12,7 @@ import { STRING_KEYS } from '@/constants/localization';
import { NumberSign, QUANTUM_MULTIPLIER } from '@/constants/numbers';
import { useDebounce, useStringGetter, useSubaccount } from '@/hooks';
import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
@@ -63,9 +64,9 @@ export const WithdrawForm = () => {
[token, resources]
);
// Async Data
const [transactionHash, setTransactionHash] = useState<string>();
const { addTransferNotification } = useLocalNotifications();
// Async Data
const debouncedAmountBN = MustBigNumber(debouncedAmount);
const withdrawAmountBN = MustBigNumber(withdrawAmount);
const freeCollateralBN = MustBigNumber(freeCollateral?.current);
@@ -133,11 +134,12 @@ export const WithdrawForm = () => {
if (txHash?.hash) {
const hash = `0x${Buffer.from(txHash.hash).toString('hex')}`;
setTransactionHash(hash);
abacusStateManager.setTransferStatus({
hash,
addTransferNotification({
txHash: hash,
fromChainId: TESTNET_CHAIN_ID,
toChainId: chainIdStr || undefined,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
@@ -148,7 +150,7 @@ export const WithdrawForm = () => {
setIsLoading(false);
}
},
[setTransactionHash, requestPayload, debouncedAmountBN, chainIdStr]
[requestPayload, debouncedAmountBN, chainIdStr]
);
const onChangeAddress = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@@ -291,17 +293,7 @@ export const WithdrawForm = () => {
}
/>
</Styled.WithDetailsReceipt>
{errorMessage ? (
<AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>
) : (
transactionHash && (
<AlertMessage type={AlertType.Success}>
<Styled.TransactionInfo>
{stringGetter({ key: STRING_KEYS.WITHDRAW_IN_PROGRESS })}
</Styled.TransactionInfo>
</AlertMessage>
)
)}
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
<WithdrawButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
+12 -1
View File
@@ -84,7 +84,7 @@ export const TransferForm = ({
const { dydxAddress } = useAccounts();
const { address: recipientAddress, size } = useSelector(getTransferInputs, shallowEqual) || {};
const { transfer, simulateTransfer } = useSubaccount();
const { nativeTokenBalance } = useAccountBalance();
const { nativeTokenBalance, usdcBalance } = useAccountBalance();
const { selectedNetwork } = useSelectedNetwork();
// User Input
@@ -104,6 +104,8 @@ export const TransferForm = ({
.toNumber();
const amount = asset === DydxChainAsset.USDC ? size?.usdcSize : size?.size;
const showNotEnoughGasWarning = fees && asset === DydxChainAsset.USDC && usdcBalance < fees;
// BN
const amountBN = MustBigNumber(amount);
const balanceBN = MustBigNumber(balance);
@@ -359,6 +361,15 @@ export const TransferForm = ({
/>
</WithDetailsReceipt>
{showNotEnoughGasWarning && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({
key: STRING_KEYS.TRANSFER_INSUFFICIENT_GAS,
params: { USDC_BALANCE: `(${usdcBalance})` },
})}
</AlertMessage>
)}
{error && <AlertMessage type={AlertType.Error}>{error.message}</AlertMessage>}
<Styled.Footer>
+3 -3
View File
@@ -89,9 +89,9 @@ export const NotificationsMenu = ({
.map(({ notification, key, displayData }) => ({
value: key,
label: displayData.title ?? '',
description: displayData.description,
slotBefore: displayData.icon,
slotAfter: (
description: displayData.customMenuContent || displayData.description,
slotBefore: !displayData.customMenuContent && displayData.icon,
slotAfter: !displayData.customMenuContent && (
<>
<$Output
type={OutputType.RelativeTime}
+1 -1
View File
@@ -510,7 +510,7 @@ Styled.OrderbookTable = styled(OrderbookTradesTable)<StyleProps>`
content: '';
}
&:not(:active):is(:focus, :focus-within) {
&:not(:active):is(:focus-visible, :focus-within) {
${orderbookMixins.scrollSnapItem}
z-index: 2;
+2 -2
View File
@@ -72,8 +72,8 @@ export const OrderbookTradesTable = styled(Table)<{ histogramSide: 'left' | 'rig
}
tbody tr:hover,
tr:focus,
td:focus {
tr:focus-visible,
td:focus-visible {
color: var(--color-text-2);
background-color: var(--color-layer-3);
}