Compare commits

...
Author SHA1 Message Date
Bill He 466cd85502 fix chain select 2024-01-29 15:05:22 -08:00
Bill He 4ced747e8d Address feedback 2024-01-29 14:56:48 -08:00
Bill He 8e1f497f43 add testflag 2024-01-26 14:14:49 -08:00
Bill He a4750921d6 fix lock 2024-01-26 14:12:06 -08:00
Bill He 51d056d346 bump packages 2024-01-26 14:10:52 -08:00
Bill He 103d1718ca Coinbase Deposit 2024-01-26 14:09:44 -08:00
Brendan Chou 5aeb01f968 allow viewer to use the address test-flag to have a view-only experience of a wallet (#250)
* allow viewer to use  test-flag to specify a view-only experience of a wallet

* respond to CR

* lint
2024-01-26 17:08:51 -05:00
moo-onthelawn c9596de9c3 TRCL-3322 Implement reduce-only checkbox on frontend (#246)
* implemented

* eol lint again

* update with correct abacus version

* update localization to latest

* fix localization v
2024-01-26 17:03:32 -05:00
Bill cc8c724f09 bump v4-client (#252) 2024-01-26 15:38:47 -05:00
aleka ac07abea07 make sure view more is hidden when not num rows not specified (#249) 2024-01-25 15:10:16 -05:00
Rui 54e81cca80 Change iOS app scheme to dydx-t-v4 (#248) 2024-01-25 09:19:49 -08:00
23 changed files with 424 additions and 108 deletions
+3 -3
View File
@@ -39,9 +39,9 @@
"@cosmjs/proto-signing": "^0.32.1", "@cosmjs/proto-signing": "^0.32.1",
"@cosmjs/stargate": "^0.32.1", "@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1",
"@dydxprotocol/v4-abacus": "^1.2.5", "@dydxprotocol/v4-abacus": "^1.3.2",
"@dydxprotocol/v4-client-js": "^1.0.14", "@dydxprotocol/v4-client-js": "^1.0.17",
"@dydxprotocol/v4-localization": "^1.1.11", "@dydxprotocol/v4-localization": "^1.1.17",
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3", "@js-joda/core": "^5.5.3",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
+12 -12
View File
@@ -27,14 +27,14 @@ dependencies:
specifier: ^0.32.1 specifier: ^0.32.1
version: 0.32.2 version: 0.32.2
'@dydxprotocol/v4-abacus': '@dydxprotocol/v4-abacus':
specifier: ^1.2.5 specifier: ^1.3.2
version: 1.2.5 version: 1.3.2
'@dydxprotocol/v4-client-js': '@dydxprotocol/v4-client-js':
specifier: ^1.0.14 specifier: ^1.0.17
version: 1.0.14 version: 1.0.17
'@dydxprotocol/v4-localization': '@dydxprotocol/v4-localization':
specifier: ^1.1.11 specifier: ^1.1.17
version: 1.1.11 version: 1.1.17
'@ethersproject/providers': '@ethersproject/providers':
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.7.2 version: 5.7.2
@@ -1086,12 +1086,12 @@ packages:
resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==}
dev: true dev: true
/@dydxprotocol/v4-abacus@1.2.5: /@dydxprotocol/v4-abacus@1.3.2:
resolution: {integrity: sha512-pB3Fv1bzJRUDp29kAN61daZ34gTSGrSTNh+/AP3TUB63/Q/gaXQVBf91xVcso4Hw23xM3FwIcrRK0cvOSWgXKg==} resolution: {integrity: sha512-zo0IHjGMlJRKOYDgNqNFQ9GtBJKJP4+Y9YY7V0X3Wt61ppKAYzodaYQhc9V/RYchcZTtS/xkicLug444YrvehQ==}
dev: false dev: false
/@dydxprotocol/v4-client-js@1.0.14: /@dydxprotocol/v4-client-js@1.0.17:
resolution: {integrity: sha512-nIjj9OKuXb3n3rrihDPkBE6OIBxXNB7gf0NMsrG1Pc9O7M0yjpitTUgnfIzplf0XMrtmozWRYZhlLgBfJcIbBg==} resolution: {integrity: sha512-PbTKbzcS7VapuFRofkirUxkF9ThNS2tWYyr0asFOMUfebsQWMvTC1sYC/s1NZIiBeq3cFz9vKdZFbfsKi7Z1bw==}
dependencies: dependencies:
'@cosmjs/amino': 0.32.2 '@cosmjs/amino': 0.32.2
'@cosmjs/encoding': 0.32.2 '@cosmjs/encoding': 0.32.2
@@ -1119,8 +1119,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: false dev: false
/@dydxprotocol/v4-localization@1.1.11: /@dydxprotocol/v4-localization@1.1.17:
resolution: {integrity: sha512-ZHnyrWD1bEpvY1tctkcmSV6A5+hQSYBFwyjbiyOqepqDqrv24J5a3hlZU94lDyYf+7Sdk3alOvy9Z7Lpk59nvw==} resolution: {integrity: sha512-kal1LrcihLMEv5YxaA/hd6Zl10Mp3x6jicoXDcXvtyNdrczAl3YapyI2nmeifRAPvfueOaY3W/sKkm2BiSKSsA==}
dev: false dev: false
/@dydxprotocol/v4-proto@3.0.0-dev.0: /@dydxprotocol/v4-proto@3.0.0-dev.0:
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"apps": { "apps": {
"ios": { "ios": {
"scheme": "dydx_t_v4" "scheme": "dydx-t-v4"
} }
}, },
"deployments": { "deployments": {
+8
View File
@@ -0,0 +1,8 @@
[
{
"name": "coinbase",
"label": "Coinbase",
"icon": "/exchanges/coinbase.png",
"depositType": "noble"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+10 -2
View File
@@ -5,7 +5,7 @@ import { Checkbox, CheckboxProps } from '@/components/Checkbox';
import { StoryWrapper } from '.ladle/components'; import { StoryWrapper } from '.ladle/components';
export const Checkboxes: Story<CheckboxProps> = (args) => { export const CheckboxStory: Story<CheckboxProps> = (args) => {
const [isChecked, setIsChecked] = useState(false); const [isChecked, setIsChecked] = useState(false);
return ( return (
@@ -21,4 +21,12 @@ export const Checkboxes: Story<CheckboxProps> = (args) => {
); );
}; };
Checkboxes.args = {}; CheckboxStory.args = {};
CheckboxStory.argTypes = {
disabled: {
options: [true, false],
control: { type: 'select' },
defaultValue: false,
}
}
+20 -4
View File
@@ -1,4 +1,4 @@
import styled, { type AnyStyledComponent } from 'styled-components'; import styled, { css, type AnyStyledComponent } from 'styled-components';
import { Root, Indicator } from '@radix-ui/react-checkbox'; import { Root, Indicator } from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons'; import { CheckIcon } from '@radix-ui/react-icons';
@@ -9,6 +9,7 @@ type ElementProps = {
onCheckedChange: (checked: boolean) => void; onCheckedChange: (checked: boolean) => void;
id?: string; id?: string;
label?: React.ReactNode; label?: React.ReactNode;
disabled?: boolean;
}; };
type StyleProps = { type StyleProps = {
@@ -23,14 +24,15 @@ export const Checkbox: React.FC<CheckboxProps> = ({
onCheckedChange, onCheckedChange,
id, id,
label, label,
disabled
}) => ( }) => (
<Styled.Container> <Styled.Container>
<Styled.Root className={className} checked={checked} onCheckedChange={onCheckedChange} id={id}> <Styled.Root className={className} checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} id={id}>
<Styled.Indicator> <Styled.Indicator>
<CheckIcon /> <CheckIcon />
</Styled.Indicator> </Styled.Indicator>
</Styled.Root> </Styled.Root>
{label && <label htmlFor={id}>{label}</label>} {label && <Styled.label disabled={disabled} htmlFor={id}>{label}</Styled.label>}
</Styled.Container> </Styled.Container>
); );
@@ -48,17 +50,23 @@ Styled.Container = styled.div`
Styled.Root = styled(Root)` Styled.Root = styled(Root)`
--checkbox-backgroundColor: var(--color-layer-0); --checkbox-backgroundColor: var(--color-layer-0);
--checkbox-borderColor: var(--color-border);
min-width: 1.25rem; min-width: 1.25rem;
height: 1.25rem; height: 1.25rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: var(--border-width) solid var(--color-border); border: var(--border-width) solid var(--checkbox-borderColor);
background-color: var(--checkbox-backgroundColor); background-color: var(--checkbox-backgroundColor);
&[data-state='checked'] { &[data-state='checked'] {
--checkbox-backgroundColor: var(--color-accent); --checkbox-backgroundColor: var(--color-accent);
} }
&[data-disabled] {
cursor: not-allowed;
--checkbox-backgroundColor: var(--color-layer-1);
}
`; `;
Styled.Indicator = styled(Indicator)` Styled.Indicator = styled(Indicator)`
@@ -68,3 +76,11 @@ Styled.Indicator = styled(Indicator)`
color: var(--color-text-2); color: var(--color-text-2);
`; `;
Styled.label = styled.div<{ disabled: boolean; }>`
color: var(--color-text-2);
${({disabled}) => disabled && css`
color: var(--color-text-0);
`}
`;
+3 -2
View File
@@ -11,6 +11,7 @@ type ElementProps = {
}; };
type StyleProps = { type StyleProps = {
className?: string;
hasLogo?: boolean; hasLogo?: boolean;
size?: number; size?: number;
}; };
@@ -18,7 +19,7 @@ type StyleProps = {
const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg'; const DARK_LOGO_MARK_URL = '/logos/logo-mark-dark.svg';
const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.svg'; const LIGHT_LOGO_MARK_URL = '/logos/logo-mark-light.svg';
export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps) => { export const QrCode = ({ className, value, hasLogo, size = 300 }: ElementProps & StyleProps) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const appTheme: AppTheme = useSelector(getAppTheme); const appTheme: AppTheme = useSelector(getAppTheme);
@@ -74,7 +75,7 @@ export const QrCode = ({ value, hasLogo, size = 300 }: ElementProps & StyleProps
} }
}, [appTheme, hasLogo]); }, [appTheme, hasLogo]);
return <Styled.QrCode ref={ref} />; return <Styled.QrCode className={className} ref={ref} />;
}; };
const Styled: Record<string, AnyStyledComponent> = {}; const Styled: Record<string, AnyStyledComponent> = {};
+5 -4
View File
@@ -125,7 +125,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
selectionMode = 'single', selectionMode = 'single',
selectionBehavior = 'toggle', selectionBehavior = 'toggle',
slotEmpty, slotEmpty,
initialNumRowsToShow = data.length, initialNumRowsToShow,
// shouldRowRender, // shouldRowRender,
// collection, // collection,
@@ -142,6 +142,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
}: ElementProps<TableRowData, TableRowKey> & StyleProps) => { }: ElementProps<TableRowData, TableRowKey> & StyleProps) => {
const [selectedKeys, setSelectedKeys] = useState(new Set<TableRowKey>()); const [selectedKeys, setSelectedKeys] = useState(new Set<TableRowKey>());
const [numRowsToShow, setNumRowsToShow] = useState(initialNumRowsToShow); const [numRowsToShow, setNumRowsToShow] = useState(initialNumRowsToShow);
const enableViewMore = numRowsToShow !== undefined;
const currentBreakpoints = useBreakpoints(); const currentBreakpoints = useBreakpoints();
const shownColumns = columns.filter( const shownColumns = columns.filter(
@@ -217,7 +218,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
} }
numColumns={shownColumns.length} numColumns={shownColumns.length}
onViewMoreClick={ onViewMoreClick={
numRowsToShow !== undefined && numRowsToShow < data.length enableViewMore && numRowsToShow < data.length
? () => setNumRowsToShow(data.length) ? () => setNumRowsToShow(data.length)
: undefined : undefined
} }
@@ -245,7 +246,7 @@ export const Table = <TableRowData extends object, TableRowKey extends Key>({
)} )}
</TableHeader> </TableHeader>
<TableBody items={list.items.slice(0, numRowsToShow)}> <TableBody items={enableViewMore ? list.items.slice(0, numRowsToShow) : list.items}>
{(item) => ( {(item) => (
<Row key={getRowKey(item)}> <Row key={getRowKey(item)}>
{(columnKey) => ( {(columnKey) => (
@@ -982,4 +983,4 @@ Styled.ViewMoreButton = styled(Button)`
width: 0.675rem; width: 0.675rem;
margin-left: 0.5ch; margin-left: 0.5ch;
} }
`; `;
+4
View File
@@ -8,6 +8,7 @@ import { Button, type ButtonStateConfig, type ButtonProps } from '@/components/B
type ElementProps = { type ElementProps = {
timeoutInSeconds: number; timeoutInSeconds: number;
slotFinal?: ReactNode;
} & ButtonProps; } & ButtonProps;
export type TimeoutButtonProps = ElementProps; export type TimeoutButtonProps = ElementProps;
@@ -15,6 +16,7 @@ export type TimeoutButtonProps = ElementProps;
export const TimeoutButton = ({ export const TimeoutButton = ({
children, children,
timeoutInSeconds, timeoutInSeconds,
slotFinal,
...otherProps ...otherProps
}: TimeoutButtonProps) => { }: TimeoutButtonProps) => {
const [timeoutDeadline] = useState(Date.now() + timeoutInSeconds * 1000); const [timeoutDeadline] = useState(Date.now() + timeoutInSeconds * 1000);
@@ -23,6 +25,8 @@ export const TimeoutButton = ({
const secondsLeft = Math.max(0, (timeoutDeadline - now) / 1000); const secondsLeft = Math.max(0, (timeoutDeadline - now) / 1000);
if (slotFinal && secondsLeft <= 0) return slotFinal;
return ( return (
<Button <Button
{...otherProps} {...otherProps}
+12
View File
@@ -138,6 +138,10 @@ export const tradeTooltips: TooltipStrings = {
title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TITLE }), title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_BODY }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_BODY }),
}), }),
'post-only-timeinforce-gtt': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TIMEINFORCE_GTT_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.POST_ONLY_TIMEINFORCE_GTT_BODY }),
}),
'price-impact': ({ stringGetter }) => ({ 'price-impact': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_TITLE }), title: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_BODY }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.PRICE_IMPACT_BODY }),
@@ -151,6 +155,14 @@ export const tradeTooltips: TooltipStrings = {
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_BODY }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_BODY }),
learnMoreLink: urlConfigs?.reduceOnlyLearnMore, learnMoreLink: urlConfigs?.reduceOnlyLearnMore,
}), }),
'reduce-only-execution-ioc-fok': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_EXECUTION_IOC_FOK_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_EXECUTION_IOC_FOK_BODY }),
}),
'reduce-only-timeinforce-ioc-fok': ({ stringGetter }) => ({
title: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_TIMEINFORCE_IOC_FOK_TITLE }),
body: stringGetter({ key: TOOLTIP_STRING_KEYS.REDUCE_ONLY_TIMEINFORCE_IOC_FOK_BODY }),
}),
spread: () => ({ spread: () => ({
title: 'Spread', title: 'Spread',
body: 'The difference in price between the highest bid (the price a buyer is willing to buy for) and lowest ask (the price a seller is willing to sell for) an asset.', body: 'The difference in price between the highest bid (the price a buyer is willing to buy for) and lowest ask (the price a seller is willing to sell for) an asset.',
+13 -1
View File
@@ -33,6 +33,7 @@ export enum WalletConnectionType {
CosmosSigner = 'CosmosSigner', CosmosSigner = 'CosmosSigner',
InjectedEip1193 = 'injectedEip1193', InjectedEip1193 = 'injectedEip1193',
WalletConnect2 = 'walletConnect2', WalletConnect2 = 'walletConnect2',
TestWallet = 'TestWallet',
} }
export enum WalletErrorType { export enum WalletErrorType {
@@ -68,6 +69,9 @@ export const walletConnectionTypes: Record<WalletConnectionType, WalletConnectio
[WalletConnectionType.CosmosSigner]: { [WalletConnectionType.CosmosSigner]: {
name: 'CosmosSigner', name: 'CosmosSigner',
}, },
[WalletConnectionType.TestWallet]: {
name: 'TestWallet',
},
}; };
// Wallets // Wallets
@@ -88,7 +92,7 @@ export enum WalletType {
TokenPocket = 'TOKEN_POCKET', TokenPocket = 'TOKEN_POCKET',
TrustWallet = 'TRUST_WALLET', TrustWallet = 'TRUST_WALLET',
WalletConnect2 = 'WALLETCONNECT_2', WalletConnect2 = 'WALLETCONNECT_2',
// TestWallet = 'TEST_WALLET', TestWallet = 'TEST_WALLET',
OtherWallet = 'OTHER_WALLET', OtherWallet = 'OTHER_WALLET',
} }
@@ -243,6 +247,12 @@ export const wallets: Record<WalletType, WalletConfig> = {
icon: KeplrIcon, icon: KeplrIcon,
connectionTypes: [WalletConnectionType.CosmosSigner], connectionTypes: [WalletConnectionType.CosmosSigner],
}, },
[WalletType.TestWallet]: {
type: WalletType.TestWallet,
stringKey: STRING_KEYS.TEST_WALLET,
icon: GenericWalletIcon,
connectionTypes: [WalletConnectionType.TestWallet],
},
}; };
// Injected EIP-1193 Providers // Injected EIP-1193 Providers
@@ -354,3 +364,5 @@ export enum DydxChainAsset {
USDC = 'usdc', USDC = 'usdc',
CHAINTOKEN = 'chain', CHAINTOKEN = 'chain',
} }
export const TEST_WALLET_EVM_ADDRESS: EvmAddress = '0x0000000000000000000000000000000000000000';
+37 -2
View File
@@ -6,14 +6,22 @@ import { NOBLE_BECH32_PREFIX, LocalWallet, type Subaccount } from '@dydxprotocol
import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account'; import { OnboardingGuard, OnboardingState, type EvmDerivedAddresses } from '@/constants/account';
import { DialogTypes } from '@/constants/dialogs'; import { DialogTypes } from '@/constants/dialogs';
import { STRING_KEYS } from '@/constants/localization';
import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage'; import { LocalStorageKey, LOCAL_STORAGE_VERSIONS } from '@/constants/localStorage';
import { DydxAddress, EvmAddress, PrivateInformation } from '@/constants/wallets'; import {
DydxAddress,
EvmAddress,
PrivateInformation,
TEST_WALLET_EVM_ADDRESS,
WalletType,
} from '@/constants/wallets';
import { setOnboardingState, setOnboardingGuard } from '@/state/account'; import { setOnboardingState, setOnboardingGuard } from '@/state/account';
import { forceOpenDialog } from '@/state/dialogs'; import { forceOpenDialog } from '@/state/dialogs';
import abacusStateManager from '@/lib/abacus'; import abacusStateManager from '@/lib/abacus';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { testFlags } from '@/lib/testFlags';
import { useDydxClient } from './useDydxClient'; import { useDydxClient } from './useDydxClient';
import { useLocalStorage } from './useLocalStorage'; import { useLocalStorage } from './useLocalStorage';
@@ -153,6 +161,7 @@ const useAccountsContext = () => {
// dYdX wallet / onboarding state // dYdX wallet / onboarding state
const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>(); const [localDydxWallet, setLocalDydxWallet] = useState<LocalWallet>();
const [localNobleWallet, setLocalNobleWallet] = useState<LocalWallet>();
const [hdKey, setHdKey] = useState<PrivateInformation>(); const [hdKey, setHdKey] = useState<PrivateInformation>();
const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]); const dydxAccounts = useMemo(() => localDydxWallet?.accounts, [localDydxWallet]);
@@ -162,6 +171,11 @@ const useAccountsContext = () => {
[localDydxWallet] [localDydxWallet]
); );
const nobleAddress = useMemo(
() => localNobleWallet?.address,
[localNobleWallet]
);
const setWalletFromEvmSignature = async (signature: string) => { const setWalletFromEvmSignature = async (signature: string) => {
const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({ const { wallet, mnemonic, privateKey, publicKey } = await getWalletFromEvmSignature({
signature, signature,
@@ -178,7 +192,26 @@ const useAccountsContext = () => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
if (connectedDydxAddress && signerGraz) { if (walletType === WalletType.TestWallet) {
// Get override values. Use the testFlags value if it exists, otherwise use the previously
// saved value where possible. If neither exist, use a default garbage value.
const addressOverride: DydxAddress = testFlags.addressOverride as DydxAddress ||
evmDerivedAddresses?.[TEST_WALLET_EVM_ADDRESS]?.dydxAddress as DydxAddress ||
'dydx1';
dispatch(setOnboardingState(OnboardingState.WalletConnected));
// Set variables.
saveEvmDerivedAccount({
evmAddress: TEST_WALLET_EVM_ADDRESS,
dydxAddress: addressOverride,
});
const wallet = new LocalWallet();
wallet.address = addressOverride;
setLocalDydxWallet(wallet);
dispatch(setOnboardingState(OnboardingState.AccountConnected));
} else if (connectedDydxAddress && signerGraz) {
dispatch(setOnboardingState(OnboardingState.WalletConnected)); dispatch(setOnboardingState(OnboardingState.WalletConnected));
try { try {
setLocalDydxWallet(await LocalWallet.fromOfflineSigner(signerGraz)); setLocalDydxWallet(await LocalWallet.fromOfflineSigner(signerGraz));
@@ -224,6 +257,7 @@ const useAccountsContext = () => {
if (hdKey?.mnemonic) { if (hdKey?.mnemonic) {
const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX); const nobleWallet = await LocalWallet.fromMnemonic(hdKey.mnemonic, NOBLE_BECH32_PREFIX);
abacusStateManager.setNobleWallet(nobleWallet); abacusStateManager.setNobleWallet(nobleWallet);
setLocalNobleWallet(nobleWallet);
} }
}; };
setNobleWallet(); setNobleWallet();
@@ -322,6 +356,7 @@ const useAccountsContext = () => {
localDydxWallet, localDydxWallet,
dydxAccounts, dydxAccounts,
dydxAddress, dydxAddress,
nobleAddress,
// Onboarding state // Onboarding state
saveHasAcknowledgedTerms, saveHasAcknowledgedTerms,
+13
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { EvmDerivedAddresses } from '@/constants/account'; import { EvmDerivedAddresses } from '@/constants/account';
import { STRING_KEYS } from '@/constants/localization';
import { LocalStorageKey } from '@/constants/localStorage'; import { LocalStorageKey } from '@/constants/localStorage';
import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks';
@@ -38,6 +39,7 @@ import { getWalletConnection, parseWalletError } from '@/lib/wallet';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { useStringGetter } from './useStringGetter'; import { useStringGetter } from './useStringGetter';
import { testFlags } from '@/lib/testFlags';
export const useWalletConnection = () => { export const useWalletConnection = () => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
@@ -138,6 +140,8 @@ export const useWalletConnection = () => {
walletType: cosmosWalletType, walletType: cosmosWalletType,
}); });
} }
} else if (walletConnection.type === WalletConnectionType.TestWallet) {
saveEvmAddress(STRING_KEYS.TEST_WALLET as EvmAddress);
} else { } else {
const isAccountConnected = Boolean( const isAccountConnected = Boolean(
evmAddress && evmDerivedAddresses[evmAddress]?.encryptedSignature evmAddress && evmDerivedAddresses[evmAddress]?.encryptedSignature
@@ -224,6 +228,15 @@ export const useWalletConnection = () => {
setSelectedWalletType(walletType); setSelectedWalletType(walletType);
}; };
// On page load, if testFlag.address is set, connect to the test wallet.
useEffect(() => {
(async () => {
if (testFlags.addressOverride) {
setSelectedWalletType(WalletType.TestWallet);
}
})();
}, []);
return { return {
// Wallet connection // Wallet connection
walletType, walletType,
+8
View File
@@ -22,6 +22,14 @@ class TestFlags {
get showMobileSignInOption() { get showMobileSignInOption() {
return !!this.queryParams.mobilesignin; return !!this.queryParams.mobilesignin;
} }
get addressOverride():string {
return this.queryParams.address;
}
get showCEXDepositOption() {
return !!this.queryParams.cexdeposit;
}
} }
export const testFlags = new TestFlags(); export const testFlags = new TestFlags();
+6
View File
@@ -62,6 +62,12 @@ export const getWalletConnection = ({
type: WalletConnectionType.CosmosSigner, type: WalletConnectionType.CosmosSigner,
}; };
} }
case WalletConnectionType.TestWallet: {
return {
type: WalletConnectionType.TestWallet,
};
}
} }
} }
}; };
+4
View File
@@ -96,6 +96,8 @@ export const useTradeFormData = () => {
needsGoodUntil, needsGoodUntil,
needsPostOnly, needsPostOnly,
needsReduceOnly, needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions, timeInForceOptions,
} = tradeOptions || {}; } = tradeOptions || {};
@@ -111,6 +113,8 @@ export const useTradeFormData = () => {
needsGoodUntil, needsGoodUntil,
needsPostOnly, needsPostOnly,
needsReduceOnly, needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions, timeInForceOptions,
tradeErrors, tradeErrors,
@@ -17,7 +17,6 @@ import type { EvmAddress } from '@/constants/wallets';
import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks'; import { useAccounts, useDebounce, useStringGetter, useSelectedNetwork } from '@/hooks';
import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance'; import { useAccountBalance, CHAIN_DEFAULT_TOKEN_ADDRESS } from '@/hooks/useAccountBalance';
import { useLocalNotifications } from '@/hooks/useLocalNotifications'; import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { useWalletConnection } from '@/hooks/useWalletConnection';
import { layoutMixins } from '@/styles/layoutMixins'; import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins'; import { formMixins } from '@/styles/formMixins';
@@ -41,10 +40,11 @@ import { getNobleChainId, NATIVE_TOKEN_ADDRESS } from '@/lib/squid';
import { log } from '@/lib/telemetry'; import { log } from '@/lib/telemetry';
import { parseWalletError } from '@/lib/wallet'; import { parseWalletError } from '@/lib/wallet';
import { ChainSelectMenu } from './ChainSelectMenu'; import { SourceSelectMenu } from './SourceSelectMenu';
import { TokenSelectMenu } from './TokenSelectMenu'; import { TokenSelectMenu } from './TokenSelectMenu';
import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt'; import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { NobleDeposit } from '../NobleDeposit';
type DepositFormProps = { type DepositFormProps = {
onDeposit?: () => void; onDeposit?: () => void;
@@ -57,13 +57,14 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { selectedNetwork } = useSelectedNetwork(); const { selectedNetwork } = useSelectedNetwork();
const { evmAddress, signerWagmi, publicClientWagmi } = useAccounts(); const { evmAddress, signerWagmi, publicClientWagmi, nobleAddress } = useAccounts();
const { addTransferNotification } = useLocalNotifications(); const { addTransferNotification } = useLocalNotifications();
const { const {
requestPayload, requestPayload,
token, token,
exchange,
chain: chainIdStr, chain: chainIdStr,
resources, resources,
summary, summary,
@@ -129,14 +130,21 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (error) onError?.(); if (error) onError?.();
}, [error]); }, [error]);
const onSelectChain = useCallback((chain: string) => { const onSelectChain = useCallback((name: string, type: 'chain' | 'exchange') => {
if (chain) { if (name) {
abacusStateManager.clearTransferInputValues(); abacusStateManager.clearTransferInputValues();
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
setFromAmount(''); setFromAmount('');
if (type === 'chain') {
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: name,
});
} else {
abacusStateManager.setTransferValue({
field: TransferInputField.exchange,
value: name,
});
}
} }
}, []); }, []);
@@ -369,39 +377,49 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
return ( return (
<Styled.Form onSubmit={onSubmit}> <Styled.Form onSubmit={onSubmit}>
<ChainSelectMenu selectedChain={chainIdStr || undefined} onSelectChain={onSelectChain} /> <SourceSelectMenu
<TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} /> selectedChain={chainIdStr || undefined}
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}> selectedExchange={exchange || undefined}
<FormInput onSelect={onSelectChain}
type={InputType.Number} />
onChange={onChangeAmount} {exchange && nobleAddress ? (
label={stringGetter({ key: STRING_KEYS.AMOUNT })} <NobleDeposit />
value={fromAmount} ) : (
slotRight={ <>
<Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}> <TokenSelectMenu selectedToken={sourceToken || undefined} onSelectToken={onSelectToken} />
{stringGetter({ key: STRING_KEYS.MAX })} <Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
</Styled.FormInputButton> <FormInput
} type={InputType.Number}
/> onChange={onChangeAmount}
</Styled.WithDetailsReceipt> label={stringGetter({ key: STRING_KEYS.AMOUNT })}
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>} value={fromAmount}
{requireUserActionInWallet && ( slotRight={
<AlertMessage type={AlertType.Warning}> <Styled.FormInputButton size={ButtonSize.XSmall} onClick={onClickMax}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })} {stringGetter({ key: STRING_KEYS.MAX })}
</AlertMessage> </Styled.FormInputButton>
}
/>
</Styled.WithDetailsReceipt>
{errorMessage && <AlertMessage type={AlertType.Error}>{errorMessage}</AlertMessage>}
{requireUserActionInWallet && (
<AlertMessage type={AlertType.Warning}>
{stringGetter({ key: STRING_KEYS.CHECK_WALLET_FOR_REQUEST })}
</AlertMessage>
)}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</>
)} )}
<Styled.Footer>
<DepositButtonAndReceipt
isDisabled={isDisabled}
isLoading={isLoading}
chainId={chainId || undefined}
setSlippage={onSetSlippage}
slippage={slippage}
sourceToken={sourceToken || undefined}
setRequireUserActionInWallet={setRequireUserActionInWallet}
setError={setError}
/>
</Styled.Footer>
</Styled.Form> </Styled.Form>
); );
}; };
@@ -3,6 +3,7 @@ import { shallowEqual, useSelector } from 'react-redux';
import { TransferType } from '@/constants/abacus'; import { TransferType } from '@/constants/abacus';
import { STRING_KEYS } from '@/constants/localization'; import { STRING_KEYS } from '@/constants/localization';
import { useStringGetter } from '@/hooks'; import { useStringGetter } from '@/hooks';
import { SearchSelectMenu } from '@/components/SearchSelectMenu'; import { SearchSelectMenu } from '@/components/SearchSelectMenu';
@@ -12,45 +13,78 @@ import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { testFlags } from '@/lib/testFlags';
type ElementProps = { type ElementProps = {
label?: string; label?: string;
selectedExchange?: string;
selectedChain?: string; selectedChain?: string;
onSelectChain: (chain: string) => void; onSelect: (name: string, type: 'chain' | 'exchange') => void;
}; };
export const ChainSelectMenu = ({ label, selectedChain, onSelectChain }: ElementProps) => { export const SourceSelectMenu = ({
label,
selectedExchange,
selectedChain,
onSelect,
}: ElementProps) => {
const stringGetter = useStringGetter(); const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } = const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {}; useSelector(getTransferInputs, shallowEqual) || {};
const chains = const chains =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || []; (type === TransferType.deposit ? depositOptions : withdrawalOptions)?.chains?.toArray() || [];
const exchanges =
(type === TransferType.deposit ? depositOptions : withdrawalOptions)?.exchanges?.toArray() ||
[];
const chainItems = Object.values(chains).map((chain) => ({ const chainItems = Object.values(chains).map((chain) => ({
value: chain.type, value: chain.type,
label: chain.stringKey, label: chain.stringKey,
onSelect: () => { onSelect: () => {
onSelectChain(chain.type); onSelect(chain.type, 'chain');
}, },
slotBefore: <Styled.Img src={chain.iconUrl} alt="" />, slotBefore: <Styled.Img src={chain.iconUrl} alt="" />,
})); }));
const selectedOption = chains.find((item) => item.type === selectedChain); const exchangeItems = Object.values(exchanges).map((exchange) => ({
value: exchange.type,
label: exchange.string,
onSelect: () => {
onSelect(exchange.type, 'exchange');
},
slotBefore: <Styled.Img src={exchange.iconUrl} alt="" />,
}));
const selectedChainOption = chains.find((item) => item.type === selectedChain);
const selectedExchangeOption = exchanges.find((item) => item.type === selectedExchange);
return ( return (
<SearchSelectMenu <SearchSelectMenu
items={[ items={[
{ exchangeItems.length > 0 && testFlags.showCEXDepositOption && {
group: 'exchanges',
groupLabel: stringGetter({ key: STRING_KEYS.EXCHANGES }),
items: exchangeItems,
},
chainItems.length > 0 && {
group: 'chains', group: 'chains',
groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }), groupLabel: stringGetter({ key: STRING_KEYS.CHAINS }),
items: chainItems, items: chainItems,
}, },
]} ].filter(isTruthy)}
label={label || (type === TransferType.deposit ? 'Source' : 'Destination')} label={label || (type === TransferType.deposit ? 'Source' : 'Destination')}
> >
<Styled.ChainRow> <Styled.ChainRow>
{selectedChain ? ( {selectedChainOption ? (
<> <>
<Styled.Img src={selectedOption?.iconUrl} alt="" /> {selectedOption?.stringKey} <Styled.Img src={selectedChainOption.iconUrl} alt="" /> {selectedChainOption.stringKey}
</>
) : selectedExchangeOption ? (
<>
<Styled.Img src={selectedExchangeOption.iconUrl} alt="" />{' '}
{selectedExchangeOption.string}
</> </>
) : ( ) : (
stringGetter({ key: STRING_KEYS.SELECT_CHAIN }) stringGetter({ key: STRING_KEYS.SELECT_CHAIN })
@@ -38,7 +38,7 @@ import { Tag } from '@/components/Tag';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt'; import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { Icon, IconName } from '@/components/Icon'; import { Icon, IconName } from '@/components/Icon';
import { ChainSelectMenu } from '@/views/forms/AccountManagementForms/ChainSelectMenu'; import { SourceSelectMenu } from '@/views/forms/AccountManagementForms/SourceSelectMenu';
import { getSubaccount } from '@/state/accountSelectors'; import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors'; import { getTransferInputs } from '@/state/inputsSelectors';
@@ -374,10 +374,10 @@ export const WithdrawForm = () => {
</span> </span>
} }
/> />
<ChainSelectMenu <SourceSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })} label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedChain={chainIdStr || undefined} selectedChain={chainIdStr || undefined}
onSelectChain={onSelectChain} onSelect={onSelectChain}
/> />
</Styled.DestinationRow> </Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} /> <TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />
+131
View File
@@ -0,0 +1,131 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { OpacityToken } from '@/constants/styles/base';
import { STRING_KEYS } from '@/constants/localization';
import { layoutMixins } from '@/styles/layoutMixins';
import { useAccounts, useStringGetter } from '@/hooks';
import { CopyButton } from '@/components/CopyButton';
import { QrCode } from '@/components/QrCode';
import { Checkbox } from '@/components/Checkbox';
import { Icon, IconName } from '@/components/Icon';
import { TimeoutButton } from '@/components/TimeoutButton';
import { WithDetailsReceipt } from '@/components/WithDetailsReceipt';
import { WithReceipt } from '@/components/WithReceipt';
import { generateFadedColorVariant } from '@/lib/styles';
export const NobleDeposit = () => {
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const stringGetter = useStringGetter();
const { nobleAddress } = useAccounts();
return (
<>
<WithDetailsReceipt
side="bottom"
detailItems={[
{
key: 'nobleAddress',
label: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS }),
value: nobleAddress,
},
]}
>
<Styled.QrCodeContainer>
<Styled.QrCode size={432} value={nobleAddress || ''} />
</Styled.QrCodeContainer>
</WithDetailsReceipt>
<Styled.WaitingSpan>
<Styled.CautionIconContainer>
<Icon iconName={IconName.CautionCircleStroked} />
</Styled.CautionIconContainer>
<p>{stringGetter({ key: STRING_KEYS.NOBLE_WARNING })}</p>
</Styled.WaitingSpan>
<Styled.WithReceipt
slotReceipt={
<Styled.CheckboxContainer>
<Checkbox
checked={hasAcknowledged}
onCheckedChange={setHasAcknowledged}
id="acknowledge-secret-phase-risk"
label={stringGetter({
key: STRING_KEYS.NOBLE_ACKNOWLEDGEMENT,
})}
/>
</Styled.CheckboxContainer>
}
>
<TimeoutButton
timeoutInSeconds={8}
slotFinal={<CopyButton state={{ isDisabled: !hasAcknowledged }} value={nobleAddress} />}
/>
</Styled.WithReceipt>
</>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.WaitingSpan = styled.span`
${layoutMixins.row}
gap: 1rem;
color: var(--color-text-1);
`;
Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.QrCodeContainer = styled.div`
display: flex;
justify-content: center;
padding: 0.5rem;
background-color: var(--color-layer-2);
border-radius: 0.5rem;
`;
Styled.QrCode = styled(QrCode)`
max-height: 20rem;
width: fit-content;
svg {
max-height: 20rem;
}
`;
Styled.CheckboxContainer = styled.div`
padding: 1rem;
color: var(--color-text-0);
`;
Styled.CautionIconContainer = styled.div`
${layoutMixins.stack}
min-width: 2.5rem;
height: 2.5rem;
align-items: center;
border-radius: 50%;
overflow: hidden;
color: var(--color-warning);
svg {
width: 1.125em;
height: 1.125em;
justify-self: center;
}
&:before {
content: '';
width: 2.5rem;
height: 2.5rem;
background-color: ${({ theme }) =>
generateFadedColorVariant(theme.warning, OpacityToken.Opacity16)};
}
`;
+3 -2
View File
@@ -10,7 +10,6 @@ import {
type HumanReadablePlaceOrderPayload, type HumanReadablePlaceOrderPayload,
type Nullable, type Nullable,
TradeInputErrorAction, TradeInputErrorAction,
TradeInputField,
ValidationError, ValidationError,
} from '@/constants/abacus'; } from '@/constants/abacus';
@@ -96,6 +95,8 @@ export const TradeForm = ({
needsGoodUntil, needsGoodUntil,
needsPostOnly, needsPostOnly,
needsReduceOnly, needsReduceOnly,
postOnlyTooltip,
reduceOnlyTooltip,
timeInForceOptions, timeInForceOptions,
tradeErrors, tradeErrors,
} = useTradeFormData(); } = useTradeFormData();
@@ -108,7 +109,7 @@ export const TradeForm = ({
const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues; const { limitPriceInput, triggerPriceInput, trailingPercentInput } = tradeFormInputValues;
const needsAdvancedOptions = const needsAdvancedOptions =
needsGoodUntil || timeInForceOptions || executionOptions || needsPostOnly || needsReduceOnly; needsGoodUntil || timeInForceOptions || executionOptions || (needsPostOnly || postOnlyTooltip) || (needsReduceOnly || reduceOnlyTooltip);
const tradeFormInputs: TradeBoxInputConfig[] = []; const tradeFormInputs: TradeBoxInputConfig[] = [];
@@ -31,12 +31,15 @@ export const AdvancedTradeOptions = () => {
const { execution, goodTil, postOnly, reduceOnly, timeInForce, type } = inputTradeData || {}; const { execution, goodTil, postOnly, reduceOnly, timeInForce, type } = inputTradeData || {};
const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, timeInForceOptions } = const { executionOptions, needsGoodUntil, needsPostOnly, needsReduceOnly, postOnlyTooltip, reduceOnlyTooltip, timeInForceOptions } =
currentTradeFormConfig || {}; currentTradeFormConfig || {};
const { duration, unit } = goodTil || {}; const { duration, unit } = goodTil || {};
const needsExecution = executionOptions || needsPostOnly || needsReduceOnly; const showPostOnly = (needsPostOnly || postOnlyTooltip);
const showReduceOnly = (needsReduceOnly || reduceOnlyTooltip);
const needsExecution = executionOptions || showPostOnly || showReduceOnly;
const hasTimeInForce = timeInForceOptions?.toArray()?.length; const hasTimeInForce = timeInForceOptions?.toArray()?.length;
return ( return (
@@ -129,26 +132,9 @@ export const AdvancedTradeOptions = () => {
))} ))}
</Styled.SelectMenu> </Styled.SelectMenu>
)} )}
{needsPostOnly && ( {showReduceOnly && <Checkbox
<Checkbox checked={(reduceOnly && !reduceOnlyTooltip) || false}
checked={postOnly || false} disabled={!!reduceOnlyTooltip}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<WithTooltip tooltip="post-only" side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
}
/>
)}
{needsReduceOnly && (
<Checkbox
checked={reduceOnly || false}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({ abacusStateManager.setTradeValue({
value: checked, value: checked,
@@ -157,11 +143,29 @@ export const AdvancedTradeOptions = () => {
} }
id="reduce-only" id="reduce-only"
label={ label={
<WithTooltip tooltip="reduce-only" side="right"> <WithTooltip tooltip={needsReduceOnly ? "reduce-only" : reduceOnlyTooltip?.titleStringKey.includes("REDUCE_ONLY_EXECUTION_IOC_FOK") ? "reduce-only-execution-ioc-fok" : "reduce-only-timeinforce-ioc-fok"} side="right">
{stringGetter({ key: STRING_KEYS.REDUCE_ONLY })} {stringGetter({ key: STRING_KEYS.REDUCE_ONLY })}
</WithTooltip> </WithTooltip>
} }
/> />
}
{showPostOnly && (
<Checkbox
checked={(postOnly && !postOnlyTooltip) || false}
disabled={!!postOnlyTooltip}
onCheckedChange={(checked) =>
abacusStateManager.setTradeValue({
value: checked,
field: TradeInputField.postOnly,
})
}
id="post-only"
label={
<WithTooltip tooltip={needsPostOnly ? "post-only" : "post-only-timeinforce-gtt"} side="right">
{stringGetter({ key: STRING_KEYS.POST_ONLY })}
</WithTooltip>
}
/>
)} )}
</> </>
)} )}