Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: deposit pending and success screens #1481

Merged
merged 5 commits into from
Jan 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ import { usePrefetchedQueries } from './hooks/usePrefetchedQueries';
import { useReferralCode } from './hooks/useReferralCode';
import { useShouldShowFooter } from './hooks/useShouldShowFooter';
import { useTokenConfigs } from './hooks/useTokenConfigs';
import { useUpdateTransfers } from './hooks/useUpdateTransfers';
import { isTruthy } from './lib/isTruthy';
import { AffiliatesPage } from './pages/affiliates/AffiliatesPage';
import { persistor } from './state/_store';
@@ -85,6 +86,7 @@ const Content = () => {
useAnalytics();
useCommandMenu();
usePrefetchedQueries();
useUpdateTransfers();
useReferralCode();
useUnlimitedLaunchDialog();
useUiRefreshMigrations();
3 changes: 3 additions & 0 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -89,6 +89,7 @@ import {
SocialXIcon,
SpeechBubbleIcon,
StarIcon,
SuccessCircleIcon,
SunIcon,
SwitchIcon,
TerminalIcon,
@@ -197,6 +198,7 @@ export enum IconName {
Show = 'Show',
SpeechBubble = 'SpeechBubble',
Star = 'Star',
SuccessCircle = 'SuccessCircle',
Sun = 'Sun',
Switch = 'Switch',
Terminal = 'Terminal',
@@ -303,6 +305,7 @@ const icons = {
[IconName.Show]: ShowIcon,
[IconName.SpeechBubble]: SpeechBubbleIcon,
[IconName.Star]: StarIcon,
[IconName.SuccessCircle]: SuccessCircleIcon,
[IconName.Sun]: SunIcon,
[IconName.Switch]: SwitchIcon,
[IconName.Terminal]: TerminalIcon,
9 changes: 4 additions & 5 deletions src/components/Loading/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ type LoadingSpinnerProps = {
className?: string;
disabled?: boolean;
size?: string;
stroke?: string;
strokeWidth?: string;
} & ComponentProps<'div'>;

@@ -20,12 +19,11 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
className,
disabled = false,
size = '38',
stroke = 'var(--color-layer-1)',
strokeWidth = '5',
...rest
}: LoadingSpinnerProps) => {
return (
<div className={className} tw="leading-[0] text-color-text-0 [--spinner-width:auto]" {...rest}>
<div className={className} tw="leading-[0] [--spinner-width:auto]" {...rest}>
<$LoadingSpinnerSvg
id={id}
width={size}
@@ -38,14 +36,15 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
cx="19"
cy="19"
r="16"
stroke={stroke}
stroke="var(--color-accent)"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this updates for all loading spinners but this is the new design ANYWAY

strokeOpacity="0.3"
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
{!disabled && (
<path
d="M35 19.248C35 22.1935 34.1611 25.08 32.5786 27.5797C30.9961 30.0794 28.7334 32.0923 26.0474 33.3897C23.3614 34.6871 20.3597 35.217 17.3831 34.9194C14.4066 34.6217 11.5744 33.5084 9.20825 31.7058C6.84207 29.9032 5.03667 27.4835 3.99704 24.7216C2.95741 21.9596 2.7252 18.966 3.32678 16.0807C3.92836 13.1953 5.33963 10.5338 7.40035 8.39841C9.46107 6.26299 12.0887 4.73918 14.9848 4"
stroke="currentColor"
stroke="var(--color-accent)"
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
63 changes: 63 additions & 0 deletions src/hooks/useUpdateTransfers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useRef } from 'react';

import { StatusState } from '@skip-go/client';

import { useAppDispatch } from '@/state/appTypes';
import { updateDeposit } from '@/state/transfers';
import { selectPendingDeposits } from '@/state/transfersSelectors';

import { useSkipClient } from './transfers/skipClient';
import { useAccounts } from './useAccounts';
import { useParameterizedSelector } from './useParameterizedSelector';

export function useUpdateTransfers() {
const { dydxAddress } = useAccounts();
const dispatch = useAppDispatch();
const { skipClient } = useSkipClient();

// TODO: generalize this to withdrawals too
const pendingDeposits = useParameterizedSelector(selectPendingDeposits, dydxAddress);

// keep track of the transactions for which we've already started querying for statuses
const transactionToCallback = useRef<{ [key: string]: boolean }>({});

useEffect(() => {
if (!dydxAddress || !pendingDeposits.length) return;

for (let i = 0; i < pendingDeposits.length; i += 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be a .map or .forEach instead ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont be scared of for loops tyler!

const deposit = pendingDeposits[i]!;
const depositKey = `${deposit.chainId}-${deposit.txHash}`;
if (transactionToCallback.current[depositKey]) return;

transactionToCallback.current[depositKey] = true;
skipClient
.waitForTransaction({ chainID: deposit.chainId, txHash: deposit.txHash })
.then((response) => {
dispatch(
updateDeposit({
dydxAddress,
deposit: {
...deposit,
status: handleResponseStatus(response.status),
},
})
);
});
}
}, [dydxAddress, pendingDeposits, skipClient, dispatch]);
}

function handleResponseStatus(status: StatusState) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is typescript inferring the return type correctly? Could use LoadableStatus

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup its correct

switch (status) {
case 'STATE_ABANDONED':
case 'STATE_COMPLETED_ERROR':
case 'STATE_PENDING_ERROR':
case 'STATE_UNKNOWN':
return 'error';
case 'STATE_COMPLETED':
case 'STATE_COMPLETED_SUCCESS':
return 'success';
default:
return 'pending';
}
}
1 change: 1 addition & 0 deletions src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -79,6 +79,7 @@ export { default as ShowIcon } from './show.svg';
export { default as SocialXIcon } from './social-x.svg';
export { default as SpeechBubbleIcon } from './speech-bubble.svg';
export { default as StarIcon } from './star.svg';
export { default as SuccessCircleIcon } from './success-circle.svg';
export { default as SunIcon } from './sun.svg';
export { default as SwitchIcon } from './switch.svg';
export { default as TerminalIcon } from './terminal.svg';
4 changes: 4 additions & 0 deletions src/icons/success-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/state/_store.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import { notificationsSlice } from './notifications';
import { perpetualsSlice } from './perpetuals';
import { rawSlice } from './raw';
import { tradingViewSlice } from './tradingView';
import { transfersSlice } from './transfers';
import { vaultsSlice } from './vaults';
import { walletSlice } from './wallet';

@@ -46,6 +47,7 @@ const reducers = {
notifications: notificationsSlice.reducer,
perpetuals: perpetualsSlice.reducer,
tradingView: tradingViewSlice.reducer,
transfers: transfersSlice.reducer,
vaults: vaultsSlice.reducer,
wallet: walletSlice.reducer,
raw: rawSlice.reducer,
@@ -55,12 +57,13 @@ const rootReducer = combineReducers(reducers);

const persistConfig = {
key: 'root',
version: 4,
version: 5,
storage,
whitelist: [
'affiliates',
'dismissable',
'tradingView',
'transfers',
'wallet',
'appUiConfigs',
'accountUiMemory',
18 changes: 18 additions & 0 deletions src/state/migrations/5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PersistedState } from 'redux-persist';

type PersistAppStateV5 = PersistedState & {
transfers: {};
};

/**
* Initiates slice for withdraws and deposits
*
*/
export function migration5(state: PersistedState | undefined): PersistAppStateV5 {
if (!state) throw new Error('state must be defined');

return {
...state,
transfers: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary since there's some logic jared or you added somewhere that will make a thing initialState if it's undefined when the app starts up / after migrations maybe possibly probably

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we've seen it in the past for whatever reason, better to be safe/explicit i guess /shrug

};
}
76 changes: 76 additions & 0 deletions src/state/transfers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { DydxAddress } from '@/constants/wallets';

export type Deposit = {
type: 'deposit';
txHash: string;
chainId: string;
status: 'pending' | 'success' | 'error';
estimatedAmountUsd: string;
actualAmountUsd?: string;
isInstantDeposit: boolean;
};

export type Withdraw = {
type: 'withdraw';
// TODO: add withdraw details here
};

export type Transfer = Deposit | Withdraw;

export function isDeposit(transfer: Transfer): transfer is Deposit {
return transfer.type === 'deposit';
}

export function isWithdraw(transfer: Transfer): transfer is Withdraw {
return transfer.type === 'withdraw';
}

export interface TransferState {
transfersByDydxAddress: { [account: DydxAddress]: Transfer[] };
}

const initialState: TransferState = {
transfersByDydxAddress: {},
};

export const transfersSlice = createSlice({
name: 'Transfers',
initialState,
reducers: {
addDeposit: (state, action: PayloadAction<{ dydxAddress: DydxAddress; deposit: Deposit }>) => {
const { dydxAddress, deposit } = action.payload;
if (!state.transfersByDydxAddress[dydxAddress]) {
state.transfersByDydxAddress[dydxAddress] = [];
}

state.transfersByDydxAddress[dydxAddress].push(deposit);
},
updateDeposit: (
state,
action: PayloadAction<{
dydxAddress: DydxAddress;
deposit: Partial<Deposit> & { txHash: string; chainId: string };
}>
) => {
const { dydxAddress, deposit } = action.payload;
const accountTransfers = state.transfersByDydxAddress[dydxAddress];
if (!accountTransfers?.length) return;

state.transfersByDydxAddress[dydxAddress] = accountTransfers.map((transfer) => {
if (
isDeposit(transfer) &&
transfer.txHash === deposit.txHash &&
transfer.chainId === deposit.chainId
) {
return { ...transfer, ...deposit };
}

return transfer;
});
},
},
});

export const { addDeposit, updateDeposit } = transfersSlice.actions;
38 changes: 38 additions & 0 deletions src/state/transfersSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { DydxAddress } from '@/constants/wallets';

import { RootState } from './_store';
import { createAppSelector } from './appTypes';
import { Deposit, isDeposit } from './transfers';

const getTransfersByAddress = (state: RootState) => state.transfers.transfersByDydxAddress;

export const selectPendingDeposits = () =>
createAppSelector(
[getTransfersByAddress, (s, dydxAddress?: DydxAddress) => dydxAddress],
(transfersByAddress, dydxAddress): Deposit[] => {
if (!dydxAddress || !transfersByAddress[dydxAddress]) return [];

return transfersByAddress[dydxAddress].filter(
(transfer) => isDeposit(transfer) && transfer.status === 'pending'
) as Deposit[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to do this without the cast? Not sure if we could utilize a transfer is Deposit filter function :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that works for individual transfer items but not for the filtered array itself :/ i like the idea of adding some explicit type guard functions tho

}
);

const selectAllTransfers = createAppSelector([getTransfersByAddress], (transfersByDydxAddress) =>
Object.values(transfersByDydxAddress).flat()
);

export const selectDeposit = () =>
createAppSelector(
[
selectAllTransfers,
(s, txHash: string) => txHash,
(s, txHash: string, chainId: string) => chainId,
],
(allTransfers, txHash, chainId) => {
return allTransfers.find(
(transfer) =>
isDeposit(transfer) && transfer.txHash === txHash && transfer.chainId === chainId
) as Deposit | undefined;
}
);
Loading

Unchanged files with check annotations Beta

selector: selectParentSubaccountInfo,
getQueryKey: (data) => ['account', 'blockTradingRewards', data],
getQueryFn: (indexerClient, data) => {
if (!isTruthy(data.wallet) || data.subaccount == null) {

Check warning on line 23 in src/bonsai/rest/blockTradingRewards.ts

GitHub Actions / lint

Unnecessary conditional, the types have no overlap
return null;
}
return () => indexerClient.account.getHistoricalBlockTradingRewards(data.wallet!);
selector: selectParentSubaccountInfo,
getQueryKey: (data) => ['account', 'fills', data.wallet, data.subaccount],
getQueryFn: (indexerClient, data) => {
if (!isTruthy(data.wallet) || data.subaccount == null) {

Check warning on line 19 in src/bonsai/rest/fills.ts

GitHub Actions / lint

Unnecessary conditional, the types have no overlap
return null;
}
return () =>
setClient(undefined);
CompositeClientManager.markDone(clientConfig);
};
}, [selectedNetwork, indexerReady]);

Check warning on line 31 in src/bonsai/rest/lib/useIndexer.ts

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array
return { indexerClient: client, key: `${selectedNetwork}-${indexerReady}` };
}
setClient(undefined);
CompositeClientManager.markDone(clientConfig);
};
}, [selectedNetwork, compositeClientReady]);

Check warning on line 56 in src/bonsai/rest/lib/useIndexer.ts

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array
return { compositeClient: client, key: `${selectedNetwork}-${compositeClientReady}` };
}
selector: selectParentSubaccountInfo,
getQueryKey: (data) => ['account', 'orders', data.wallet, data.subaccount],
getQueryFn: (indexerClient, data) => {
if (!isTruthy(data.wallet) || data.subaccount == null) {

Check warning on line 21 in src/bonsai/rest/orders.ts

GitHub Actions / lint

Unnecessary conditional, the types have no overlap
return null;
}
return () =>
status: orders.status,
data:
orders.data != null
? keyBy(isParentSubaccountOrders(orders.data), (o) => o.id ?? '')

Check warning on line 44 in src/bonsai/rest/orders.ts

GitHub Actions / lint

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined
: orders.data,
error: orders.error,
})
selector: selectParentSubaccountInfo,
getQueryKey: (data) => ['account', 'transfers', data],
getQueryFn: (indexerClient, data) => {
if (!isTruthy(data.wallet) || data.subaccount == null) {

Check warning on line 20 in src/bonsai/rest/transfers.ts

GitHub Actions / lint

Unnecessary conditional, the types have no overlap
return null;
}
return () =>
export function setUpParentSubaccount(store: RootStore) {
return createStoreEffect(store, selectParentSubaccount, ({ subaccount, wallet, wsUrl }) => {
if (!isTruthy(wallet) || subaccount == null) {

Check warning on line 211 in src/bonsai/websocket/parentSubaccount.ts

GitHub Actions / lint

Unnecessary conditional, the types have no overlap
return undefined;
}
${({ action, buttonStyle }) => action && buttonStyle && buttonActionVariants[action][buttonStyle]}
${({ action, state, buttonStyle }) =>
state &&

Check warning on line 228 in src/components/Button.tsx

GitHub Actions / lint

Unnecessary conditional, value is always truthy
css`
// Ordered from lowest to highest priority (ie. Disabled should overwrite Active and Loading states)
${state[ButtonState.Loading] && buttonStateVariants(action, buttonStyle)[ButtonState.Loading]}
))}
{slotEmpty && searchValue.trim() !== '' && (
<Command.Empty tw="h-full p-1 text-color-text-0">
{slotEmpty ??

Check warning on line 203 in src/components/ComboboxMenu.tsx

GitHub Actions / lint

Unnecessary conditional, expected left-hand side of `??` operator to be possibly null or undefined
stringGetter({
key: STRING_KEYS.NO_RESULTS,
})}