Compare commits

...
Author SHA1 Message Date
John Huang 7621ca3ee2 TRCL-3553 Having the launch incentive api in env.json 2024-02-21 10:12:05 -08:00
Bill d2c5b7945b Remove CEX withdraw testflag (#316) 2024-02-21 09:50:11 -08:00
aleka fc5d8538fe bump abacus for new env config path usage (#314)
* bump abacus for new env config path usage

* bump abacus + localization for good til date
2024-02-20 22:42:18 -05:00
yujin512 fdc262f59f Update otherMarketExchangeConfig.json (#315)
* Update otherMarketExchangeConfig.json

add strk

* Update otherMarketExchangeConfig.json

remove huobi, add kraken

* Update otherMarketParameters.json

add strk to parameters

* add strk icon and metadata
2024-02-20 16:08:11 -05:00
Bill 7f947804bd Enable CCTP onboarding for avalanche, optimism (#310)
* Enable CCTP for arbitrum avalanche optimism

* Enable CCTP onboarding for arbitrum, avalanche, optimism

* remove arb
2024-02-19 20:18:57 -08:00
aleka 6620a70a5d fix parsing error and surface tx broadcast error (#313)
* fix parsing error and surface tx broadcast error

* fix network as chain id
2024-02-19 22:06:13 -05:00
Bill bff779631b Cex withdraw (#301)
* Coinbase withdrawal and deposit UI

* analytics

* packages bumps

* address comments

* address comments

* add test flag
2024-02-16 17:56:06 -08:00
29 changed files with 301 additions and 147 deletions
+2 -2
View File
@@ -40,9 +40,9 @@
"@cosmjs/proto-signing": "^0.32.1",
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@dydxprotocol/v4-abacus": "^1.4.5",
"@dydxprotocol/v4-abacus": "^1.4.9",
"@dydxprotocol/v4-client-js": "^1.0.20",
"@dydxprotocol/v4-localization": "^1.1.30",
"@dydxprotocol/v4-localization": "^1.1.34",
"@ethersproject/providers": "^5.7.2",
"@js-joda/core": "^5.5.3",
"@radix-ui/react-accordion": "^1.1.2",
+12 -12
View File
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
follow-redirects: 1.15.3
@@ -26,14 +30,14 @@ dependencies:
specifier: ^0.32.1
version: 0.32.2
'@dydxprotocol/v4-abacus':
specifier: ^1.4.5
version: 1.4.5
specifier: ^1.4.9
version: 1.4.9
'@dydxprotocol/v4-client-js':
specifier: ^1.0.20
version: 1.0.20
'@dydxprotocol/v4-localization':
specifier: ^1.1.30
version: 1.1.30
specifier: ^1.1.34
version: 1.1.34
'@ethersproject/providers':
specifier: ^5.7.2
version: 5.7.2
@@ -1286,8 +1290,8 @@ packages:
resolution: {integrity: sha512-Gg5t+eR7vPJMAmhkFt6CZrzPd0EKpAslWwk5rFVYZpJsM8JG5KT9XQ99hgNM3Ov6ScNoIWbXkpX27F6A9cXR4Q==}
dev: false
/@dydxprotocol/v4-abacus@1.4.5:
resolution: {integrity: sha512-LhJmpIaUkHCsSiHx+jdk+59euvGp2E+gGFelpfmOYpH1+enZgEXDQvWsgHSFEQxYP3mRiGG4uo+ZYNGdvffg7g==}
/@dydxprotocol/v4-abacus@1.4.9:
resolution: {integrity: sha512-OnrD5Db7AGSxyQE+gWRmkBuXbkZMfSEDB+v2s+ft0zgd4D5j/WTZkk1RG0fn8ISV4AffoN5T24jBWxLfqVlmZw==}
dev: false
/@dydxprotocol/v4-client-js@1.0.20:
@@ -1319,8 +1323,8 @@ packages:
- utf-8-validate
dev: false
/@dydxprotocol/v4-localization@1.1.30:
resolution: {integrity: sha512-TZfWWRSOxcjLHs972wlJVVHkE7+DVqAUnGZSs24HYHsPtUkPhZiNXMOA2Vk9YddQxumhM79xIRH0cmJSe5DDUg==}
/@dydxprotocol/v4-localization@1.1.34:
resolution: {integrity: sha512-I7zivjv8gS+6b9n7/wh7PY9QEUDIIyLx9ugZ5K6ybar0xT/06yupOVdVE6iLivQ/IAsVi/RFHkD8bQd0DWCetg==}
dev: false
/@dydxprotocol/v4-proto@4.0.0-dev.0:
@@ -16049,7 +16053,3 @@ packages:
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+21 -11
View File
@@ -1,12 +1,22 @@
[
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
}
]
{
"chainId": "1",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "Ethereum"
},
{
"chainId": "5",
"tokenAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F",
"name": "Ethereum Goerli"
},
{
"chainId": "43114",
"tokenAddress": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
"name": "Avalanche"
},
{
"chainId": "10",
"tokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
"name": "optimism"
}
]
+7
View File
@@ -428,6 +428,13 @@
"whitepaperLink": "https://solana.com/solana-whitepaper.pdf",
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/solana/"
},
"STRK-USD": {
"name": "Starknet",
"tags": ["Layer 2"],
"websiteLink": "https://www.starknet.io/en",
"whitepaperLink": "https://docs.starknet.io/documentation/",
"coinMarketCapsLink": "https://coinmarketcap.com/currencies/starknet-token/"
},
"STX-USD": {
"name": "Stacks",
"tags": ["Layer 2"],
@@ -513,6 +513,14 @@
{ "exchangeName": "Okx", "ticker": "SOL-USDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Mexc", "ticker": "SOL_USDT", "adjustByMarket": "USDT-USD" }
],
"STRK": [
{ "exchangeName": "Binance", "ticker": "STRKUSDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Bybit", "ticker": "STRKUSDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Kraken", "ticker": "STRKUSD" },
{ "exchangeName": "Kucoin", "ticker": "STRK-USDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Okx", "ticker": "STRK-USDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Gate", "ticker": "STRK_USDT", "adjustByMarket": "USDT-USD" }
],
"STX": [
{ "exchangeName": "Binance", "ticker": "STXUSDT", "adjustByMarket": "USDT-USD" },
{ "exchangeName": "Bybit", "ticker": "STXUSDT", "adjustByMarket": "USDT-USD" },
+17
View File
@@ -968,6 +968,23 @@
"minOrderSize": 1000000,
"quantumConversionExponent": -9
},
{
"baseAsset": "STRK",
"referencePrice": 2.05,
"numOracles": 6,
"liquidityTier": 2,
"assetName": "Starknet",
"p": 0.0,
"atomicResolution": -6.0,
"minExchanges": 3,
"minPriceChangePpm": 4000,
"priceExponent": -9.0,
"stepBaseQuantum": 1000000,
"ticksizeExponent": -3,
"subticksPerTick": 1000000,
"minOrderSize": 1000000,
"quantumConversionExponent": -9
},
{
"baseAsset": "STX",
"referencePrice": 1.5144048703611412,
+6 -3
View File
@@ -71,7 +71,8 @@
"keplrDashboard": "https://testnet.keplr.app/",
"strideZoneApp": "https://testnet.stride.zone",
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet",
"launchIncentive": "https://cloud.chaoslabs.co/"
},
"dydx-testnet-4": {
"tos": "https://dydx.exchange/v4-terms",
@@ -92,7 +93,8 @@
"keplrDashboard": "https://testnet.keplr.app/",
"strideZoneApp": "https://testnet.stride.zone",
"accountExportLearnMore": "https://help.dydx.exchange/en/articles/8565867-secret-phrase-on-dydx-chain",
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet"
"walletLearnMore": "https://www.dydx.academy/video/defi-wallet",
"launchIncentive": "https://cloud.chaoslabs.co/"
},
"[mainnet chain id]": {
"tos": "[HTTP link to TOS]",
@@ -113,7 +115,8 @@
"keplrDashboard": "[HTTP link to keplr dashboard, can be null]",
"strideZoneApp": "[HTTP link to stride zone app, can be null]",
"accountExportLearnMore": "[HTTP link to account export learn more, can be null]",
"walletLearnMore": "[HTTP link to wallet learn more, can be null]"
"walletLearnMore": "[HTTP link to wallet learn more, can be null]",
"launchIncentive": "[HTTP link to launch incentive host, can be null]"
}
},
"wallets": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+5 -3
View File
@@ -39,7 +39,7 @@ export const SearchSelectMenu = ({
disabled,
label,
items,
withSearch,
withSearch = true,
withReceiptItems,
}: SearchSelectMenuProps) => {
const [open, setOpen] = useState(false);
@@ -77,6 +77,7 @@ export const SearchSelectMenu = ({
withSearch={withSearch}
onItemSelected={() => setOpen(false)}
withStickyLayout
$withSearch={withSearch}
/>
</Styled.Popover>
</Styled.WithDetailsReceipt>
@@ -127,7 +128,7 @@ Styled.Popover = styled(Popover)`
box-shadow: none;
`;
Styled.ComboboxMenu = styled(ComboboxMenu)`
Styled.ComboboxMenu = styled(ComboboxMenu)<{ $withSearch?: boolean }>`
${layoutMixins.withInnerHorizontalBorders}
--comboboxMenu-backgroundColor: var(--color-layer-4);
@@ -140,7 +141,8 @@ Styled.ComboboxMenu = styled(ComboboxMenu)`
--comboboxMenu-item-checked-textColor: var(--color-text-2);
--comboboxMenu-item-highlighted-textColor: var(--color-text-2);
--stickyArea1-topHeight: var(--form-input-height);
--stickyArea1-topHeight: ${({ $withSearch }) =>
!$withSearch ? '0' : 'var(--form-input-height)'};
input:focus-visible {
outline: none;
+8 -1
View File
@@ -1,4 +1,4 @@
import { type ReactNode, useState } from 'react';
import { type ReactNode, useState, useEffect } from 'react';
import { ButtonAction, ButtonState } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
@@ -8,6 +8,7 @@ import { Button, type ButtonStateConfig, type ButtonProps } from '@/components/B
type ElementProps = {
timeoutInSeconds: number;
onTimeOut?: () => void;
slotFinal?: ReactNode;
} & ButtonProps;
@@ -16,6 +17,7 @@ export type TimeoutButtonProps = ElementProps;
export const TimeoutButton = ({
children,
timeoutInSeconds,
onTimeOut,
slotFinal,
...otherProps
}: TimeoutButtonProps) => {
@@ -25,6 +27,11 @@ export const TimeoutButton = ({
const secondsLeft = Math.max(0, (timeoutDeadline - now) / 1000);
useEffect(() => {
if (secondsLeft > 0) return;
onTimeOut?.();
}, [secondsLeft]);
if (slotFinal && secondsLeft <= 0) return slotFinal;
return (
+10 -2
View File
@@ -144,9 +144,17 @@ export type AnalyticsEventData<T extends AnalyticsEvent> =
validatorUrl: string;
}
: T extends AnalyticsEvent.TransferDeposit
? {}
? {
chainId?: string;
tokenAddress?: string;
tokenSymbol?: string;
}
: T extends AnalyticsEvent.TransferWithdraw
? {}
? {
chainId?: string;
tokenAddress?: string;
tokenSymbol?: string;
}
: // Trading
T extends AnalyticsEvent.TradeOrderTypeSelected
? {
+1
View File
@@ -140,6 +140,7 @@ export type TransferNotifcation = {
isCctp?: boolean;
errorCount?: number;
status?: StatusResponse;
isExchange?: boolean;
};
/**
+15 -9
View File
@@ -82,24 +82,30 @@ const useLocalNotificationsContext = () => {
isCctp,
errorCount,
status: currentStatus,
isExchange,
} = transferNotification;
// @ts-ignore status.errors is not in the type definition but can be returned
// also error can some time come back as an empty object so we need to ignore for that
const hasErrors = !!currentStatus?.errors ||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
const hasErrors =
// @ts-ignore status.errors is not in the type definition but can be returned
// also error can some time come back as an empty object so we need to ignore for that
!!currentStatus?.errors ||
(currentStatus?.error && Object.keys(currentStatus.error).length !== 0);
if (
!isExchange &&
!hasErrors &&
(!currentStatus?.squidTransactionStatus ||
currentStatus?.squidTransactionStatus === 'ongoing')
) {
try {
const status = await fetchSquidStatus({
transactionId: txHash,
toChainId,
fromChainId,
}, isCctp);
const status = await fetchSquidStatus(
{
transactionId: txHash,
toChainId,
fromChainId,
},
isCctp
);
if (status) {
transferNotification.status = status;
+3 -2
View File
@@ -179,8 +179,9 @@ export const notificationTypes: NotificationTypeConfig[] = [
useEffect(() => {
for (const transfer of transferNotifications) {
const { fromChainId, status, txHash, toAmount, type } = transfer;
const isFinished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing';
const { fromChainId, status, txHash, toAmount, type, isExchange } = transfer;
const isFinished =
(Boolean(status) && status?.squidTransactionStatus !== 'ongoing') || isExchange;
const icon = <Icon iconName={isFinished ? IconName.Transfer : IconName.Clock} />;
const transferType =
+6 -7
View File
@@ -34,7 +34,7 @@ import {
import { DEFAULT_TRANSACTION_MEMO } from '@/constants/analytics';
import { DialogTypes } from '@/constants/dialogs';
import { UNCOMMITTED_ORDER_TIMEOUT_MS } from '@/constants/trade';
import { DydxNetwork, isTestnet } from '@/constants/networks';
import { DydxChainId, isTestnet } from '@/constants/networks';
import { RootStore } from '@/state/_store';
import { addUncommittedOrderClientId, removeUncommittedOrderClientId } from '@/state/account';
@@ -44,7 +44,10 @@ import { StatefulOrderError } from '../errors';
import { bytesToBigInt } from '../numbers';
import { log } from '../telemetry';
import { hashFromTx, getMintscanTxLink } from '../txUtils';
import { getDydxChainIdFromNetwork } from '../network';
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
private compositeClient: CompositeClient | undefined;
@@ -239,10 +242,7 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
if (isTestnet) {
console.log(
getMintscanTxLink(
getDydxChainIdFromNetwork(this.compositeClient.network.getString() as DydxNetwork),
hash
)
getMintscanTxLink(this.compositeClient.network.getString() as DydxChainId, hash)
);
} else console.log(`txHash: ${hash}`);
@@ -538,7 +538,6 @@ class DydxChainTransactions implements AbacusDYDXChainTransactionsProtocol {
const result = await this.cctpWithdraw(params);
callback(result);
break;
break;
}
default: {
break;
+19
View File
@@ -25,3 +25,22 @@ export function convertBech32Address({
}): string {
return toBech32(bech32Prefix, fromHex(toHex(fromBech32(address).data)));
}
/**
* Validates a Cosmos address with a specific prefix.
* @param {string} address The Cosmos address to validate.
* @param {string} prefix The expected prefix for the address.
* @returns {boolean} True if the address is valid and matches the prefix, false otherwise.
*/
export function validateCosmosAddress(address: string, prefix: string) {
try {
// Decode the address to verify its structure and prefix
const { prefix: decodedPrefix } = fromBech32(address);
// Check if the decoded address has the expected prefix
return decodedPrefix === prefix;
} catch (error) {
// If decoding fails, the address is not valid
return false;
}
}
+1 -5
View File
@@ -1,8 +1,4 @@
import { type DydxNetwork, ENVIRONMENT_CONFIG_MAP, type DydxChainId } from '@/constants/networks';
export const validateAgainstAvailableEnvironments = (value: DydxNetwork) =>
Object.keys(ENVIRONMENT_CONFIG_MAP).includes(value);
export const getDydxChainIdFromNetwork = (network: DydxNetwork) => {
return ENVIRONMENT_CONFIG_MAP[network].dydxChainId as DydxChainId;
};
Object.keys(ENVIRONMENT_CONFIG_MAP).includes(value);
+3 -3
View File
@@ -31,13 +31,13 @@ export const fetchSquidStatus = async (
const response = await fetch(url, {
headers: {
"x-integrator-id": integratorId || 'dYdX-api'
'x-integrator-id': integratorId || 'dYdX-api',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error);
throw error;
}
return response.json();
@@ -45,4 +45,4 @@ export const fetchSquidStatus = async (
export const getNobleChainId = () => {
return isMainnet ? 'noble-1' : 'grand-1';
}
};
+2 -2
View File
@@ -19,10 +19,10 @@ class TestFlags {
return !!this.queryParams.displayinitializingmarkets;
}
get addressOverride():string {
get addressOverride(): string {
return this.queryParams.address;
}
get showTradingRewards() {
return !!this.queryParams.tradingrewards;
}
+1 -3
View File
@@ -13,8 +13,6 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
const stringGetter = useStringGetter();
const { isMobile } = useBreakpoints();
const closeDialog = () => setIsOpen?.(false);
return (
<Dialog
isOpen
@@ -22,7 +20,7 @@ export const DepositDialog = ({ setIsOpen }: ElementProps) => {
title={stringGetter({ key: STRING_KEYS.DEPOSIT })}
placement={isMobile ? DialogPlacement.FullScreen : DialogPlacement.Default}
>
<DepositDialogContent onDeposit={closeDialog} />
<DepositDialogContent />
</Dialog>
);
};
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { TransferInputField, TransferType } from '@/constants/abacus';
import { AnalyticsEvent } from '@/constants/analytics';
import { isMainnet } from '@/constants/networks';
import { layoutMixins } from '@/styles/layoutMixins';
@@ -9,6 +10,7 @@ import { DepositForm } from '@/views/forms/AccountManagementForms/DepositForm';
import { TestnetDepositForm } from '@/views/forms/AccountManagementForms/TestnetDepositForm';
import abacusStateManager from '@/lib/abacus';
import { track } from '@/lib/analytics';
type ElementProps = {
onDeposit?: () => void;
@@ -34,9 +36,19 @@ export const DepositDialogContent = ({ onDeposit }: ElementProps) => {
return (
<Styled.Content>
{isMainnet || !showFaucet ? (
<DepositForm onDeposit={onDeposit} />
<DepositForm
onDeposit={(event) => {
track(AnalyticsEvent.TransferDeposit, event);
onDeposit?.();
}}
/>
) : (
<TestnetDepositForm onDeposit={onDeposit} />
<TestnetDepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferFaucet);
onDeposit?.();
}}
/>
)}
{!isMainnet && (
<Styled.TextToggle onClick={() => setShowFaucet(!showFaucet)}>
+2 -2
View File
@@ -105,8 +105,8 @@ export const OnboardingDialog = ({ setIsOpen }: ElementProps) => {
<Styled.Content>
{isMainnet ? (
<DepositForm
onDeposit={() => {
track(AnalyticsEvent.TransferDeposit);
onDeposit={(event) => {
track(AnalyticsEvent.TransferDeposit, event);
}}
/>
) : (
@@ -7,6 +7,7 @@ import { Abi, parseUnits } from 'viem';
import erc20 from '@/abi/erc20.json';
import erc20_usdt from '@/abi/erc20_usdt.json';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AnalyticsEvent, AnalyticsEventData } from '@/constants/analytics';
import { AlertType } from '@/constants/alerts';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
@@ -48,7 +49,7 @@ import { DepositButtonAndReceipt } from './DepositForm/DepositButtonAndReceipt';
import { NobleDeposit } from '../NobleDeposit';
type DepositFormProps = {
onDeposit?: () => void;
onDeposit?: (event?: AnalyticsEventData<AnalyticsEvent.TransferDeposit>) => void;
onError?: () => void;
};
@@ -132,7 +133,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
if (error) onError?.();
}, [error]);
const onSelectChain = useCallback((name: string, type: 'chain' | 'exchange') => {
const onSelectNetwork = useCallback((name: string, type: 'chain' | 'exchange') => {
if (name) {
abacusStateManager.clearTransferInputValues();
setFromAmount('');
@@ -258,8 +259,6 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
};
const txHash = await signerWagmi.sendTransaction(tx);
onDeposit?.();
if (txHash) {
addTransferNotification({
txHash: txHash,
@@ -271,6 +270,12 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
});
abacusStateManager.clearTransferInputValues();
setFromAmount('');
onDeposit?.({
chainId: chainIdStr || undefined,
tokenAddress: sourceToken?.address || undefined,
tokenSymbol: sourceToken?.symbol || undefined,
});
}
} catch (error) {
log('DepositForm/onSubmit', error);
@@ -279,7 +284,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
setIsLoading(false);
}
},
[requestPayload, signerWagmi, chainId]
[requestPayload, signerWagmi, chainId, sourceToken, sourceChain]
);
const amountInputReceipt = [
@@ -378,7 +383,7 @@ export const DepositForm = ({ onDeposit, onError }: DepositFormProps) => {
<SourceSelectMenu
selectedChain={chainIdStr || undefined}
selectedExchange={exchange || undefined}
onSelect={onSelectChain}
onSelect={onSelectNetwork}
/>
{exchange && nobleAddress ? (
<NobleDeposit />
@@ -14,6 +14,7 @@ import { popoverMixins } from '@/styles/popoverMixins';
import { getTransferInputs } from '@/state/inputsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { testFlags } from '@/lib/testFlags';
type ElementProps = {
label?: string;
@@ -17,9 +17,10 @@ import { getTransferInputs } from '@/state/inputsSelectors';
type ElementProps = {
selectedToken?: TransferInputTokenResource;
onSelectToken: (token: TransferInputTokenResource) => void;
isExchange?: boolean;
};
export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps) => {
export const TokenSelectMenu = ({ selectedToken, onSelectToken, isExchange }: ElementProps) => {
const stringGetter = useStringGetter();
const { type, depositOptions, withdrawalOptions, resources } =
useSelector(getTransferInputs, shallowEqual) || {};
@@ -47,19 +48,24 @@ export const TokenSelectMenu = ({ selectedToken, onSelectToken }: ElementProps)
},
]}
label={stringGetter({ key: STRING_KEYS.ASSET })}
withReceiptItems={[
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: selectedToken && (
<>
<Tag>{type === TransferType.deposit ? selectedToken?.symbol : 'USDC'}</Tag>
<DiffArrow />
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
</>
),
},
]}
withSearch={!isExchange}
withReceiptItems={
!isExchange
? [
{
key: 'swap',
label: stringGetter({ key: STRING_KEYS.SWAP }),
value: selectedToken && (
<>
<Tag>{type === TransferType.deposit ? selectedToken?.symbol : 'USDC'}</Tag>
<DiffArrow />
<Tag>{type === TransferType.deposit ? 'USDC' : selectedToken?.symbol}</Tag>
</>
),
},
]
: undefined
}
>
<Styled.AssetRow>
{selectedToken ? (
@@ -7,6 +7,7 @@ import { isAddress } from 'viem';
import { TransferInputField, TransferInputTokenResource, TransferType } from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { AnalyticsEvent } from '@/constants/analytics';
import { ButtonSize } from '@/constants/buttons';
import { STRING_KEYS } from '@/constants/localization';
import { isMainnet } from '@/constants/networks';
@@ -55,6 +56,8 @@ import { getNobleChainId } from '@/lib/squid';
import { TokenSelectMenu } from './TokenSelectMenu';
import { WithdrawButtonAndReceipt } from './WithdrawForm/WithdrawButtonAndReceipt';
import { validateCosmosAddress } from '@/lib/addressUtils';
import { track } from '@/lib/analytics';
export const WithdrawForm = () => {
const stringGetter = useStringGetter();
@@ -68,6 +71,7 @@ export const WithdrawForm = () => {
const {
requestPayload,
token,
exchange,
chain: chainIdStr,
address: toAddress,
resources,
@@ -179,18 +183,27 @@ export const WithdrawForm = () => {
requestPayload.data,
isCctp
);
if (txHash) {
const nobleChainId = getNobleChainId();
const toChainId = Boolean(exchange) ? nobleChainId : chainIdStr || undefined;
if (txHash && toChainId) {
addTransferNotification({
txHash: txHash,
type: TransferNotificationTypes.Withdrawal,
fromChainId: !isCctp ? selectedDydxChainId : getNobleChainId(),
toChainId: chainIdStr || undefined,
fromChainId: !isCctp ? selectedDydxChainId : nobleChainId,
toChainId,
toAmount: debouncedAmountBN.toNumber(),
triggeredAt: Date.now(),
isCctp,
isExchange: Boolean(exchange),
});
abacusStateManager.clearTransferInputValues();
setWithdrawAmount('');
track(AnalyticsEvent.TransferWithdraw, {
chainId: toChainId,
tokenAddress: toToken?.address || undefined,
tokenSymbol: toToken?.symbol || undefined,
});
}
}
} catch (error) {
@@ -213,7 +226,17 @@ export const WithdrawForm = () => {
setIsLoading(false);
}
},
[requestPayload, debouncedAmountBN, chainIdStr, toAddress, screenAddresses, stringGetter]
[
requestPayload,
debouncedAmountBN,
chainIdStr,
toAddress,
selectedDydxChainId,
exchange,
toToken,
screenAddresses,
stringGetter,
]
);
const onChangeAddress = useCallback((e: ChangeEvent<HTMLInputElement>) => {
@@ -246,13 +269,20 @@ export const WithdrawForm = () => {
setWithdrawAmount(freeCollateralBN.toString());
}, [freeCollateralBN, setWithdrawAmount]);
const onSelectChain = useCallback((chain: string) => {
if (chain) {
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: chain,
});
const onSelectNetwork = useCallback((name: string, type: 'chain' | 'exchange') => {
if (name) {
setWithdrawAmount('');
if (type === 'chain') {
abacusStateManager.setTransferValue({
field: TransferInputField.chain,
value: name,
});
} else {
abacusStateManager.setTransferValue({
field: TransferInputField.exchange,
value: name,
});
}
}
}, []);
@@ -313,7 +343,7 @@ export const WithdrawForm = () => {
});
if (debouncedAmountBN) {
if (!chainIdStr) {
if (!chainIdStr && !exchange) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_CHAIN });
} else if (!toToken) {
return stringGetter({ key: STRING_KEYS.WITHDRAW_MUST_SPECIFY_ASSET });
@@ -360,14 +390,19 @@ export const WithdrawForm = () => {
summary,
]);
const isInvalidNobleAddress = Boolean(
exchange && toAddress && !validateCosmosAddress(toAddress, 'noble')
);
const isDisabled =
!!errorMessage ||
!toToken ||
!chainIdStr ||
(!chainIdStr && !exchange) ||
!toAddress ||
debouncedAmountBN.isNaN() ||
debouncedAmountBN.isZero() ||
isLoading;
isLoading ||
isInvalidNobleAddress;
return (
<Styled.Form onSubmit={onSubmit}>
@@ -385,12 +420,21 @@ export const WithdrawForm = () => {
}
/>
<SourceSelectMenu
label={stringGetter({ key: STRING_KEYS.NETWORK })}
selectedExchange={exchange || undefined}
selectedChain={chainIdStr || undefined}
onSelect={onSelectChain}
onSelect={onSelectNetwork}
/>
</Styled.DestinationRow>
<TokenSelectMenu selectedToken={toToken || undefined} onSelectToken={onSelectToken} />
{isInvalidNobleAddress && (
<AlertMessage type={AlertType.Error}>
{stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS_VALIDATION })}
</AlertMessage>
)}
<TokenSelectMenu
selectedToken={toToken || undefined}
onSelectToken={onSelectToken}
isExchange={Boolean(exchange)}
/>
<Styled.WithDetailsReceipt side="bottom" detailItems={amountInputReceipt}>
<FormInput
type={InputType.Number}
@@ -27,6 +27,8 @@ import { calculateCanAccountTrade } from '@/state/accountCalculators';
import { getSubaccount } from '@/state/accountSelectors';
import { getTransferInputs } from '@/state/inputsSelectors';
import { isTruthy } from '@/lib/isTruthy';
import { SlippageEditor } from '../SlippageEditor';
type ElementProps = {
@@ -55,7 +57,7 @@ export const WithdrawButtonAndReceipt = ({
const stringGetter = useStringGetter();
const { leverage } = useSelector(getSubaccount, shallowEqual) || {};
const { summary, requestPayload } = useSelector(getTransferInputs, shallowEqual) || {};
const { summary, requestPayload, exchange } = useSelector(getTransferInputs, shallowEqual) || {};
const canAccountTrade = useSelector(calculateCanAccountTrade, shallowEqual);
const { usdcLabel } = useTokenConfigs();
@@ -92,7 +94,7 @@ export const WithdrawButtonAndReceipt = ({
value: <Output type={OutputType.Fiat} value={totalFees} />,
subitems: feeSubitems,
},
{
!exchange && {
key: 'exchange-rate',
label: <span>{stringGetter({ key: STRING_KEYS.EXCHANGE_RATE })}</span>,
value: withdrawToken && typeof summary?.exchangeRate === 'number' && (
@@ -133,7 +135,9 @@ export const WithdrawButtonAndReceipt = ({
{withdrawToken && <Tag>{withdrawToken?.symbol}</Tag>}
</span>
),
value: <Output type={OutputType.Asset} value={summary?.toAmount} fractionDigits={TOKEN_DECIMALS} />,
value: (
<Output type={OutputType.Asset} value={summary?.toAmount} fractionDigits={TOKEN_DECIMALS} />
),
subitems: [
{
key: 'minimum-amount-received',
@@ -144,13 +148,17 @@ export const WithdrawButtonAndReceipt = ({
</span>
),
value: (
<Output type={OutputType.Asset} value={summary?.toAmountMin} fractionDigits={TOKEN_DECIMALS} />
<Output
type={OutputType.Asset}
value={summary?.toAmountMin}
fractionDigits={TOKEN_DECIMALS}
/>
),
tooltip: 'minimum-amount-received',
},
],
},
{
!exchange && {
key: 'slippage',
label: <span>{stringGetter({ key: STRING_KEYS.MAX_SLIPPAGE })}</span>,
value: (
@@ -175,7 +183,7 @@ export const WithdrawButtonAndReceipt = ({
/>
),
},
];
].filter(isTruthy);
const isFormValid = !isDisabled && !isEditingSlippage;
+25 -31
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import styled, { type AnyStyledComponent, css } from 'styled-components';
import { OpacityToken } from '@/constants/styles/base';
import { STRING_KEYS } from '@/constants/localization';
@@ -10,7 +10,6 @@ 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';
@@ -19,6 +18,7 @@ import { generateFadedColorVariant } from '@/lib/styles';
export const NobleDeposit = () => {
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const [hasTimedout, setHasTimedout] = useState(false);
const stringGetter = useStringGetter();
const { nobleAddress } = useAccounts();
@@ -30,23 +30,21 @@ export const NobleDeposit = () => {
{
key: 'nobleAddress',
label: stringGetter({ key: STRING_KEYS.NOBLE_ADDRESS }),
value: nobleAddress,
value:
hasAcknowledged && hasTimedout
? nobleAddress
: stringGetter({ key: STRING_KEYS.ACKNOWLEDGE_TO_REVEAL }),
},
]}
>
<Styled.QrCodeContainer>
<Styled.QrCode size={432} value={nobleAddress || ''} />
</Styled.QrCodeContainer>
<Styled.QrCode
hasLogo
size={432}
value={nobleAddress || ''}
blurred={!hasAcknowledged || !hasTimedout}
/>
</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>
@@ -63,7 +61,12 @@ export const NobleDeposit = () => {
>
<TimeoutButton
timeoutInSeconds={8}
slotFinal={<CopyButton state={{ isDisabled: !hasAcknowledged }} value={nobleAddress} />}
onTimeOut={() => setHasTimedout(true)}
slotFinal={
<CopyButton state={{ isDisabled: !hasAcknowledged }} value={nobleAddress}>
{!hasAcknowledged ? stringGetter({ key: STRING_KEYS.ACKNOWLEDGE_RISKS }) : undefined}
</CopyButton>
}
/>
</Styled.WithReceipt>
</>
@@ -82,23 +85,14 @@ Styled.WithReceipt = styled(WithReceipt)`
--withReceipt-backgroundColor: var(--color-layer-2);
`;
Styled.QrCodeContainer = styled.div`
display: flex;
justify-content: center;
Styled.QrCode = styled(QrCode)<{ blurred: boolean }>`
border-radius: 0.5em;
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;
}
${({ blurred }) =>
blurred &&
css`
filter: blur(8px);
`}
`;
Styled.CheckboxContainer = styled.div`
@@ -40,7 +40,7 @@ export const TransferStatusNotification = ({
const stringGetter = useStringGetter();
const [open, setOpen] = useState<boolean>(false);
const [secondsLeft, setSecondsLeft] = useState<number>(0);
const { fromChainId, status, txHash, toAmount } = transfer;
const { fromChainId, status, txHash, toAmount, isExchange } = transfer;
// @ts-ignore status.errors is not in the type definition but can be returned
const error = status?.errors?.length ? status?.errors[0] : status?.error;
@@ -54,6 +54,8 @@ export const TransferStatusNotification = ({
useInterval({ callback: updateSecondsLeft });
const isComplete = status?.squidTransactionStatus === 'success' || isExchange;
const inProgressStatusString =
type === TransferNotificationTypes.Deposit
? secondsLeft > 0
@@ -65,10 +67,10 @@ export const TransferStatusNotification = ({
const statusString =
type === TransferNotificationTypes.Deposit
? status?.squidTransactionStatus === 'success'
? isComplete
? STRING_KEYS.DEPOSIT_COMPLETE
: inProgressStatusString
: status?.squidTransactionStatus === 'success'
: isComplete
? STRING_KEYS.WITHDRAW_COMPLETE
: inProgressStatusString;
@@ -108,12 +110,12 @@ export const TransferStatusNotification = ({
slotIcon={isToast && slotIcon}
slotTitle={slotTitle}
slotCustomContent={
!status ? (
!status && !isExchange ? (
<LoadingDots size={3} />
) : (
<Styled.BridgingStatus>
{content}
{!isToast && status?.squidTransactionStatus !== 'success' && !hasError && (
{!isToast && !isComplete && !hasError && (
<Styled.TransferStatusSteps status={status} type={type} />
)}
</Styled.BridgingStatus>