-
Notifications
You must be signed in to change notification settings - Fork 146
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: cancel stacks pending transaction #5501
Changes from all commits
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,13 @@ | ||
import type { StacksNetwork } from '@stacks/network'; | ||
import { ChainID } from '@stacks/transactions'; | ||
|
||
export function getBurnAddress(network: StacksNetwork): string { | ||
switch (network.chainId) { | ||
case ChainID.Mainnet: | ||
return 'SP00000000000003SCNSJTCSE62ZF4MSE'; | ||
case ChainID.Testnet: | ||
return 'ST000000000000000000002AMW42H'; | ||
default: | ||
return 'ST000000000000000000002AMW42H'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { HStack, styled } from 'leather-styles/jsx'; | ||
|
||
interface CancelTransactionButtonProps { | ||
isEnabled?: boolean; | ||
isSelected: boolean; | ||
onCancelTransaction(): void; | ||
} | ||
export function CancelTransactionButton(props: CancelTransactionButtonProps) { | ||
const { isEnabled, isSelected, onCancelTransaction } = props; | ||
const isActive = isEnabled && !isSelected; | ||
|
||
return ( | ||
isActive && ( | ||
<styled.button | ||
_hover={{ color: 'ink.text-subdued' }} | ||
bg="ink.background-primary" | ||
maxWidth="125px" | ||
ml="auto" | ||
onClick={e => { | ||
onCancelTransaction(); | ||
e.stopPropagation(); | ||
}} | ||
pointerEvents={!isActive ? 'none' : 'all'} | ||
position="relative" | ||
px="space.02" | ||
py="space.01" | ||
rounded="xs" | ||
zIndex={999} | ||
> | ||
<HStack gap="space.01"> | ||
<styled.span textStyle="label.03" color="yellow.action-primary-default"> | ||
Cancel transaction | ||
</styled.span> | ||
</HStack> | ||
</styled.button> | ||
) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import { createSearchParams, useLocation, useNavigate } from 'react-router-dom'; | ||
|
||
import { HStack } from 'leather-styles/jsx'; | ||
|
||
import { StacksTx, TxTransferDetails } from '@shared/models/transactions/stacks-transaction.model'; | ||
import { RouteUrls } from '@shared/route-urls'; | ||
|
||
|
@@ -19,6 +21,7 @@ import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/s | |
import { useRawTxIdState } from '@app/store/transactions/raw.hooks'; | ||
|
||
import { TransactionItemLayout } from '../transaction-item/transaction-item.layout'; | ||
import { CancelTransactionButton } from './cancel-transaction-button'; | ||
import { IncreaseFeeButton } from './increase-fee-button'; | ||
import { StacksTransactionIcon } from './stacks-transaction-icon'; | ||
import { StacksTransactionStatus } from './stacks-transaction-status'; | ||
|
@@ -64,6 +67,22 @@ export function StacksTransactionItem({ | |
})(); | ||
}; | ||
|
||
const onCancelTransaction = () => { | ||
if (!transaction) return; | ||
setRawTxId(transaction.tx_id); | ||
|
||
const urlSearchParams = `?${createSearchParams({ txId: transaction.tx_id })}`; | ||
|
||
whenWallet({ | ||
ledger: () => | ||
whenPageMode({ | ||
full: () => navigate(RouteUrls.CancelStxTransaction), | ||
popup: () => openIndexPageInNewTab(RouteUrls.CancelStxTransaction, urlSearchParams), | ||
})(), | ||
software: () => navigate(RouteUrls.CancelStxTransaction), | ||
})(); | ||
Comment on lines
+76
to
+83
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 think we can always just navigate? Not sure we need to handle ledger/software differently here. 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 coped this from increase fee logic not sure why it was in that way since increase fee was working fine i thought it's better to rely on already established code |
||
}; | ||
|
||
const isOriginator = transaction?.sender_address === currentAccount?.address; | ||
const isPending = transaction && isPendingTx(transaction); | ||
|
||
|
@@ -75,19 +94,30 @@ export function StacksTransactionItem({ | |
); | ||
const title = transaction ? getTxTitle(transaction) : transferDetails?.title || ''; | ||
const value = transaction ? getTxValue(transaction, isOriginator) : transferDetails?.value; | ||
const increaseFeeButton = ( | ||
<IncreaseFeeButton | ||
isEnabled={isOriginator && isPending} | ||
isSelected={pathname === RouteUrls.IncreaseStxFee} | ||
onIncreaseFee={onIncreaseFee} | ||
/> | ||
const actionButtonGroup = ( | ||
<HStack alignItems="start" gap="space.01"> | ||
<CancelTransactionButton | ||
isEnabled={isOriginator && isPending} | ||
isSelected={ | ||
pathname === RouteUrls.CancelStxTransaction || pathname === RouteUrls.IncreaseStxFee | ||
} | ||
onCancelTransaction={onCancelTransaction} | ||
/> | ||
<IncreaseFeeButton | ||
isEnabled={isOriginator && isPending} | ||
isSelected={ | ||
pathname === RouteUrls.IncreaseStxFee || pathname === RouteUrls.CancelStxTransaction | ||
} | ||
onIncreaseFee={onIncreaseFee} | ||
/> | ||
</HStack> | ||
); | ||
const txStatus = transaction && <StacksTransactionStatus transaction={transaction} />; | ||
|
||
return ( | ||
<TransactionItemLayout | ||
openTxLink={openTxLink} | ||
rightElement={isOriginator && isPending ? increaseFeeButton : undefined} | ||
actionButtonGroupElement={isOriginator && isPending ? actionButtonGroup : undefined} | ||
txCaption={caption} | ||
txIcon={txIcon} | ||
txStatus={txStatus} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { Suspense, useEffect } from 'react'; | ||
import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom'; | ||
|
||
import { microStxToStx, stxToMicroStx } from '@leather-wallet/utils'; | ||
import BigNumber from 'bignumber.js'; | ||
import { Formik } from 'formik'; | ||
import { Flex, Stack } from 'leather-styles/jsx'; | ||
|
||
import { RouteUrls } from '@shared/route-urls'; | ||
|
||
import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; | ||
import { stacksValue } from '@app/common/stacks-utils'; | ||
import { FeesRow } from '@app/components/fees-row/fees-row'; | ||
import { LoadingSpinner } from '@app/components/loading-spinner'; | ||
import { StacksTransactionItem } from '@app/components/stacks-transaction-item/stacks-transaction-item'; | ||
import { useToast } from '@app/features/toasts/use-toast'; | ||
import { Dialog } from '@app/ui/components/containers/dialog/dialog'; | ||
import { Footer } from '@app/ui/components/containers/footers/footer'; | ||
import { DialogHeader } from '@app/ui/components/containers/headers/dialog-header'; | ||
import { Spinner } from '@app/ui/components/spinner'; | ||
import { Caption } from '@app/ui/components/typography/caption'; | ||
|
||
import { CancelTransactionActions } from './components/cancel-transaction-actions'; | ||
import { useStxCancelTransaction } from './hooks/use-stx-cancel-transaction'; | ||
|
||
export function CancelStxTransactionDialog() { | ||
const { | ||
rawTx, | ||
rawTxId, | ||
setRawTxId, | ||
tx, | ||
setTxId, | ||
onSubmit, | ||
validationSchema, | ||
availableUnlockedBalance, | ||
stxFees, | ||
} = useStxCancelTransaction(); | ||
const { isLoading, setIsIdle } = useLoading(LoadingKeys.CANCEL_TRANSACTION_DRAWER); | ||
const navigate = useNavigate(); | ||
const location = useLocation(); | ||
const [searchParams] = useSearchParams(); | ||
const txIdFromParams = searchParams.get('txId'); | ||
const toast = useToast(); | ||
|
||
const fee = Number(rawTx?.auth.spendingCondition?.fee); | ||
|
||
useEffect(() => { | ||
if (tx?.tx_status !== 'pending' && rawTx) { | ||
setTxId(null); | ||
toast.info('Your transaction went through! Cancellation not possible.'); | ||
} | ||
}, [rawTx, tx?.tx_status, setTxId, toast]); | ||
|
||
useEffect(() => { | ||
if (!rawTxId && txIdFromParams) { | ||
setRawTxId(txIdFromParams); | ||
} | ||
if (isLoading && !rawTxId) { | ||
setIsIdle(); | ||
} | ||
}, [isLoading, rawTxId, setIsIdle, setRawTxId, txIdFromParams]); | ||
Comment on lines
+54
to
+61
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. Tip Codebase Verification The search results indicate that
There is no validation to check if Analysis chainConsider adding error handling for cases where Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Description: Verify the validation of `txIdFromParams`.
# Test: Search for validation logic for `txIdFromParams`. Expect: Proper validation checks.
rg --type typescript $'txIdFromParams' --context 5
Length of output: 84 Script: #!/bin/bash
# Description: Verify the validation of `txIdFromParams`.
# Test: Search for validation logic for `txIdFromParams`. Expect: Proper validation checks.
rg --type ts 'txIdFromParams' --context 5
Length of output: 6346 |
||
|
||
if (!tx || !fee) return <LoadingSpinner />; | ||
|
||
const onClose = () => { | ||
setRawTxId(null); | ||
navigate(RouteUrls.Home); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Formik | ||
initialValues={{ fee: new BigNumber(microStxToStx(fee)).toNumber() }} | ||
onSubmit={onSubmit} | ||
validateOnChange={false} | ||
validateOnBlur={false} | ||
validateOnMount={true} | ||
validationSchema={validationSchema} | ||
> | ||
{props => ( | ||
<> | ||
<Dialog | ||
isShowing={location.pathname === RouteUrls.CancelStxTransaction} | ||
onClose={onClose} | ||
header={<DialogHeader title="Cancel transaction" />} | ||
footer={ | ||
<Footer flexDirection="row"> | ||
<CancelTransactionActions | ||
onCancel={() => { | ||
setTxId(null); | ||
navigate(RouteUrls.Home); | ||
}} | ||
isDisabled={stxToMicroStx(props.values.fee).isEqualTo(fee)} | ||
/> | ||
</Footer> | ||
} | ||
> | ||
<Stack gap="space.05" px="space.05" pb="space.05"> | ||
<Suspense | ||
fallback={ | ||
<Flex alignItems="center" justifyContent="center" p="space.06"> | ||
<Spinner /> | ||
</Flex> | ||
} | ||
> | ||
<Caption> | ||
Canceling a transaction isn't guaranteed to work. A higher fee can help replace | ||
the old transaction | ||
</Caption> | ||
<Stack gap="space.06"> | ||
{tx && <StacksTransactionItem transaction={tx} />} | ||
<Stack gap="space.04"> | ||
<FeesRow fees={stxFees} defaultFeeValue={fee + 1} isSponsored={false} /> | ||
{availableUnlockedBalance?.amount && ( | ||
<Caption> | ||
Balance: | ||
{stacksValue({ | ||
value: availableUnlockedBalance.amount, | ||
fixedDecimals: true, | ||
})} | ||
</Caption> | ||
)} | ||
</Stack> | ||
</Stack> | ||
</Suspense> | ||
</Stack> | ||
</Dialog> | ||
<Outlet /> | ||
</> | ||
)} | ||
</Formik> | ||
</> | ||
); | ||
} |
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.
code here is pretty much similar to increase fee btn, can we create component to avoid code duplication?
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.
Yes that makes sense, i can work on it