-
Notifications
You must be signed in to change notification settings - Fork 77
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
Changes from all commits
c50ca43
b9e009c
3b6f1d2
41f2dd1
83c5676
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this be a .map or .forEach instead ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is typescript inferring the return type correctly? Could use LoadableStatus There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
} | ||
} |
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: {}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
}; | ||
} |
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; |
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[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that works for individual |
||
} | ||
); | ||
|
||
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; | ||
} | ||
); |
Unchanged files with check annotations Beta
selector: selectParentSubaccountInfo, | ||
getQueryKey: (data) => ['account', 'blockTradingRewards', data], | ||
getQueryFn: (indexerClient, data) => { | ||
if (!isTruthy(data.wallet) || data.subaccount == null) { | ||
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) { | ||
return null; | ||
} | ||
return () => |
setClient(undefined); | ||
CompositeClientManager.markDone(clientConfig); | ||
}; | ||
}, [selectedNetwork, indexerReady]); | ||
return { indexerClient: client, key: `${selectedNetwork}-${indexerReady}` }; | ||
} | ||
setClient(undefined); | ||
CompositeClientManager.markDone(clientConfig); | ||
}; | ||
}, [selectedNetwork, compositeClientReady]); | ||
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) { | ||
return null; | ||
} | ||
return () => | ||
status: orders.status, | ||
data: | ||
orders.data != null | ||
? keyBy(isParentSubaccountOrders(orders.data), (o) => o.id ?? '') | ||
: orders.data, | ||
error: orders.error, | ||
}) |
selector: selectParentSubaccountInfo, | ||
getQueryKey: (data) => ['account', 'transfers', data], | ||
getQueryFn: (indexerClient, data) => { | ||
if (!isTruthy(data.wallet) || data.subaccount == null) { | ||
return null; | ||
} | ||
return () => |
export function setUpParentSubaccount(store: RootStore) { | ||
return createStoreEffect(store, selectParentSubaccount, ({ subaccount, wallet, wsUrl }) => { | ||
if (!isTruthy(wallet) || subaccount == null) { | ||
return undefined; | ||
} | ||
${({ action, buttonStyle }) => action && buttonStyle && buttonActionVariants[action][buttonStyle]} | ||
${({ action, state, buttonStyle }) => | ||
state && | ||
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 ?? | ||
stringGetter({ | ||
key: STRING_KEYS.NO_RESULTS, | ||
})} |
There was a problem hiding this comment.
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