From 1b1a53a44c00a7f9eb010636c45c9d8aed32a223 Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Thu, 27 Jun 2024 16:08:54 +0200 Subject: [PATCH 001/219] [ENG-4343] SEND BTC Transaction review screen - New Design --- .../confirmBtcTransaction/index.tsx | 32 +++++++++---------- .../confirmBtcTransaction/itemRow/amount.tsx | 16 ++++++---- .../transactionSummary.tsx | 21 ++++++++++-- .../confirmBtcTransaction/transferSection.tsx | 7 ++-- .../txInOutput/txInOutput.tsx | 4 +-- .../confirmBtcTransactionComponent/index.tsx | 26 +++++++-------- src/app/components/confirmScreen/index.tsx | 14 ++++---- .../transactionDetailComponent/index.tsx | 2 +- src/app/screens/sendBtc/amountSelector.tsx | 2 +- src/app/screens/sendRune/amountSelector.tsx | 2 +- src/app/ui-components/selectFeeRate/index.tsx | 2 +- src/locales/en.json | 4 ++- 12 files changed, 79 insertions(+), 53 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index 388e447bb..122ccd259 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -1,9 +1,9 @@ import { delay } from '@common/utils/ledger'; -import ActionButton from '@components/button'; import { Tab } from '@components/tabBar'; import useSelectedAccount from '@hooks/useSelectedAccount'; import TransportFactory from '@ledgerhq/hw-transport-webusb'; import { RuneSummary, Transport, btcTransaction } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; import Callout from '@ui-library/callout'; import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; @@ -217,13 +217,13 @@ function ConfirmBtcTransaction({ /> {!isLoading && ( - - + + - - diff --git a/src/app/components/confirmScreen/index.tsx b/src/app/components/confirmScreen/index.tsx index 3f30270e8..6e064b19e 100644 --- a/src/app/components/confirmScreen/index.tsx +++ b/src/app/components/confirmScreen/index.tsx @@ -1,4 +1,4 @@ -import ActionButton from '@components/button'; +import Button from '@ui-library/button'; import { ReactNode } from 'react'; import styled from 'styled-components'; @@ -54,14 +54,14 @@ function ConfirmScreen({ <> {children} - + - ({ - marginRight: props.theme.spacing(4), + marginRight: props.theme.space.xs, width: 32, height: 32, borderRadius: 30, @@ -33,18 +33,18 @@ const AddressContainer = styled.div({ justifyContent: 'flex-end', }); -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const TitleText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_200, })); -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const ValueText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_0, })); -const AmountText = styled.h1((props) => ({ - ...props.theme.body_medium_m, +const AmountText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, color: props.theme.colors.white_0, })); @@ -53,15 +53,15 @@ const ColumnContainer = styled.div({ flexDirection: 'column', }); -const ScriptOutputHeadingText = styled.h1((props) => ({ +const ScriptOutputHeadingText = styled.p((props) => ({ ...props.theme.headline_s, - color: props.theme.colors.white[0], - marginBottom: props.theme.spacing(12), + color: props.theme.colors.white_0, + marginBottom: props.theme.space.l, })); -const ScriptText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white[200], +const ScriptText = styled.p((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_200, width: 320, wordWrap: 'break-word', })); @@ -90,7 +90,7 @@ const ShowScriptBackgroundContainer = styled.div((props) => ({ position: 'fixed', background: props.theme.colors.background.modalBackdrop2, backdropFilter: 'blur(16px)', - padding: props.theme.spacing(8), + padding: props.theme.space.m, display: 'flex', flexDirection: 'column', })); diff --git a/src/locales/en.json b/src/locales/en.json index a081f7a52..3d00c7230 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -342,7 +342,7 @@ "SIGN_TRANSACTIONS": "Sign {{count}} transactions", "AMOUNT": "Amount", "INSUFFICIENT_BALANCE": "Insufficient balance", - "YOUR_ADDRESS": "Your address", + "YOUR_ADDRESS": "My address", "FROM": "From", "INPUT": "Inputs", "INPUT_AND_OUTPUT": "Inputs & Outputs", @@ -439,7 +439,7 @@ "ORDINAL": "Ordinal", "RECIPIENT": "Recipient", "TO": "To", - "YOUR_ADDRESS": "Your address" + "YOUR_ADDRESS": "My address" }, "YOUR_ORDINAL_ADDRESS": "Your ordinals address", "YOUR_PAYMENT_ADDRESS": "Your payment address", @@ -680,7 +680,7 @@ "ACTIVATE_ORDINALS_INFO": "You have Bitcoin Ordinals in your wallet. Would you like to display them? This is an experimental feature. You can change this setting at anytime.", "ACTIVATE": "Activate", "DENY": "No", - "COPIED": "Copied!", + "COPIED": "Copied", "MINT": "Mint", "TRANSFER": "Transfer", "DEPLOY": "Deploy", @@ -1095,7 +1095,7 @@ "ORDINAL": "Ordinal", "RECIPIENT": "Recipient", "TO": "To", - "YOUR_ADDRESS": "Your address" + "YOUR_ADDRESS": "My address" }, "PREVIEW": { "SATS": { @@ -1311,13 +1311,13 @@ "LESS_THAN_OR_EQUAL_TO": "Less than or equal to", "FROM": "From", "TO": "To", - "YOUR_ADDRESS": "Your address", + "YOUR_ADDRESS": "My address", "CANCEL": "Cancel", "CONFIRM": "Confirm", "FEES": "Fees", "ROUTE": "Route", "COPIED": "Copied", - "COPY_YOUR_ADDRESS": "copy your address", + "COPY_YOUR_ADDRESS": "Copy my address", "ADVANCED_SETTING": "Advanced settings", "THIS_IS_A_SPONSORED_TRANSACTION": "This is a sponsored transaction, no transaction fees will be deducted from your account.", "SWAP_TRANSACTION_CANNOT_BE_SPONSORED": "Swap transaction cannot be sponsored while you have a pending transaction.", From 2c8e9d8db9a2036c2b5768b5e9061e027205f9ab Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Tue, 9 Jul 2024 20:31:34 +0200 Subject: [PATCH 008/219] Update the tx inputs/outputs icons --- .../confirmBtcTransaction/txInOutput/transactionInput.tsx | 4 ++-- .../confirmBtcTransaction/txInOutput/transactionOutput.tsx | 2 +- .../confirmBtcTransaction/txInOutput/txInOutput.tsx | 2 +- src/assets/img/transactions/arrowUpRed.svg | 4 ++++ 4 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 src/assets/img/transactions/arrowUpRed.svg diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index e8cb9bfb4..dbe3b3f07 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -1,4 +1,4 @@ -import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg'; +import InputIcon from '@assets/img/transactions/arrowUpRed.svg'; import TransferDetailView from '@components/transferDetailView'; import useSelectedAccount from '@hooks/useSelectedAccount'; import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; @@ -62,7 +62,7 @@ function TransactionInput({ input }: Props) { return ( ({ - marginBottom: props.theme.space.s, + marginBottom: props.theme.space.m, })); const ExpandedContainer = styled(animated.div)((props) => ({ diff --git a/src/assets/img/transactions/arrowUpRed.svg b/src/assets/img/transactions/arrowUpRed.svg new file mode 100644 index 000000000..6811318c8 --- /dev/null +++ b/src/assets/img/transactions/arrowUpRed.svg @@ -0,0 +1,4 @@ + + + + From 3976d2107e80668e7fa5cf7229186a081962f47b Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Tue, 9 Jul 2024 21:02:57 +0200 Subject: [PATCH 009/219] Move the signBatchPsbtRequest styles to a separate file --- .../signBatchPsbtRequest/index.styled.ts | 86 +++++++++++++++ .../screens/signBatchPsbtRequest/index.tsx | 103 +++--------------- 2 files changed, 99 insertions(+), 90 deletions(-) create mode 100644 src/app/screens/signBatchPsbtRequest/index.styled.ts diff --git a/src/app/screens/signBatchPsbtRequest/index.styled.ts b/src/app/screens/signBatchPsbtRequest/index.styled.ts new file mode 100644 index 000000000..643d3501f --- /dev/null +++ b/src/app/screens/signBatchPsbtRequest/index.styled.ts @@ -0,0 +1,86 @@ +import BottomModal from '@components/bottomModal'; +import styled from 'styled-components'; + +export const OuterContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginTop: props.theme.spacing(11), + marginLeft: props.theme.spacing(8), + marginRight: props.theme.spacing(8), +})); + +export const LoaderContainer = styled.div((props) => ({ + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', + marginTop: props.theme.spacing(12), +})); + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + padding: props.theme.spacing(8), + paddingTop: props.theme.spacing(12), + paddingBottom: props.theme.spacing(20), +})); + +export const TransparentButtonContainer = styled.div((props) => ({ + marginRight: props.theme.spacing(6), + width: '100%', +})); + +export const ReviewTransactionText = styled.h1((props) => ({ + ...props.theme.headline_s, + color: props.theme.colors.white_0, + marginBottom: props.theme.spacing(12), + textAlign: 'left', +})); + +export const BundleLinkContainer = styled.button((props) => ({ + display: 'flex', + alignItems: 'center', + backgroundColor: 'transparent', + color: props.theme.colors.tangerine, + transition: 'color 0.2s ease', + marginBottom: props.theme.spacing(6), + ':hover': { + color: props.theme.colors.tangerine_light, + }, +})); + +export const BundleLinkText = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + marginRight: props.theme.spacing(2), +})); + +export const CustomizedModal = styled(BottomModal)` + display: flex; + flex-direction: column; + height: 100%; + max-height: 100% !important; + background-color: #181818 !important; +`; + +export const CustomizedModalContainer = styled(Container)` + margin-top: 0; +`; + +export const TxReviewModalControls = styled.div((props) => ({ + display: 'flex', + columnGap: props.theme.spacing(6), + padding: props.theme.spacing(8), + paddingTop: props.theme.spacing(12), + paddingBottom: props.theme.spacing(20), +})); diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index 306d39991..a6e8805b5 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -2,7 +2,6 @@ import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types' import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; import AssetModal from '@components/assetModal'; -import BottomModal from '@components/bottomModal'; import ActionButton from '@components/button'; import BurnSection from '@components/confirmBtcTransaction/burnSection'; import DelegateSection from '@components/confirmBtcTransaction/delegateSection'; @@ -38,95 +37,19 @@ import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -const OuterContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - marginTop: props.theme.spacing(11), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const LoaderContainer = styled.div((props) => ({ - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: props.theme.spacing(12), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(20), -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), - width: '100%', -})); - -const WarningCallout = styled(Callout)` - margin-bottom: ${(props) => props.theme.space.m}; -`; - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), - textAlign: 'left', -})); - -const BundleLinkContainer = styled.button((props) => ({ - display: 'flex', - alignItems: 'center', - backgroundColor: 'transparent', - color: props.theme.colors.tangerine, - transition: 'color 0.2s ease', - marginBottom: props.theme.spacing(6), - ':hover': { - color: props.theme.colors.tangerine_light, - }, -})); - -const BundleLinkText = styled.div((props) => ({ - ...props.theme.body_medium_m, - marginRight: props.theme.spacing(2), -})); - -const CustomizedModal = styled(BottomModal)` - display: flex; - flex-direction: column; - height: 100%; - max-height: 100% !important; - background-color: #181818 !important; -`; - -const CustomizedModalContainer = styled(Container)` - margin-top: 0; -`; - -const TxReviewModalControls = styled.div((props) => ({ - display: 'flex', - columnGap: props.theme.spacing(6), - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(20), -})); +import { + BundleLinkContainer, + BundleLinkText, + ButtonContainer, + Container, + CustomizedModal, + CustomizedModalContainer, + LoaderContainer, + OuterContainer, + ReviewTransactionText, + TransparentButtonContainer, + TxReviewModalControls, +} from './index.styled'; interface TxResponse { txId: string; From a1ec5c831a26b66bc12bb5f85b8012b4d0d5303e Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Tue, 9 Jul 2024 21:43:29 +0200 Subject: [PATCH 010/219] Make some text style tweaks for tx inputs/outputs --- .../transactionSummary.tsx | 3 ++- .../txInOutput/transactionInput.tsx | 22 +++++++------------ .../txInOutput/transactionOutput.tsx | 18 ++++++--------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 6208a8b56..9a4943c20 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -135,6 +135,7 @@ function TransactionSummary({ )} {hasRuneDelegation && } + {/* TODO: Add handling for the array in case of sending to multiple recipients */} )} - {t('FEES')} + {feeOutput && showFeeSelector && {t('FEES')}} {feeOutput && showFeeSelector && ( diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx index dbe3b3f07..438843d94 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionInput.tsx @@ -16,19 +16,11 @@ const SubValueText = styled(StyledP)((props) => ({ color: props.theme.colors.white_400, })); -const TxIdText = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -const YourAddressText = styled(StyledP)((props) => ({ - marginRight: props.theme.space.xxs, -})); - const TxIdContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', - justifyContent: 'space-between', gap: props.theme.space.xxs, + marginTop: props.theme.space.xxxs, })); type Props = { @@ -50,13 +42,15 @@ function TransactionInput({ input }: Props) { const renderAddress = (addressToBeDisplayed: string) => addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( - + {getTruncatedAddress(addressToBeDisplayed)} - ({t('YOUR_ADDRESS')}) + ({t('YOUR_ADDRESS')}) ) : ( - {getTruncatedAddress(addressToBeDisplayed)} + + {getTruncatedAddress(addressToBeDisplayed)} + ); return ( @@ -73,10 +67,10 @@ function TransactionInput({ input }: Props) { > {isExternalInput ? ( - + {getTruncatedAddress(input.extendedUtxo.utxo.txid)} - (txid) + (txid) ) : ( renderAddress(input.extendedUtxo.address) diff --git a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx index 3bcb252a2..c684fb8e6 100644 --- a/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx +++ b/src/app/components/confirmBtcTransaction/txInOutput/transactionOutput.tsx @@ -22,15 +22,11 @@ const HighlightText = styled(StyledP)((props) => ({ color: props.theme.colors.white_0, })); -const YourAddressText = styled(StyledP)((props) => ({ - color: props.theme.colors.white_0, - marginRight: props.theme.space.xxs, -})); - const TxIdContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', gap: props.theme.space.xxs, + marginTop: props.theme.space.xxxs, })); type Props = { @@ -53,7 +49,7 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { const detailView = () => { if (isOutputWithScript) { return ( - {`${t( + {`${t( 'SCRIPT_OUTPUT', )} #${scriptOutputCount}`} ); @@ -64,9 +60,9 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { output.pubKeys?.includes(btcPublicKey) || output.pubKeys?.includes(ordinalsPublicKey); const toOwnString = toOwnKey ? ` (${t('YOUR_PUBLIC_KEY')})` : ''; return ( - + {outputType} - {toOwnString} + {toOwnString} ); } @@ -74,16 +70,16 @@ function TransactionOutput({ output, scriptOutputCount }: Props) { if (output.address === btcAddress || output.address === ordinalsAddress) { return ( - + {getTruncatedAddress(output.address)} - ({t('YOUR_ADDRESS')}) + ({t('YOUR_ADDRESS')}) ); } return ( - + {getTruncatedAddress(output.address)} ); From 061872ae0ce032b56d0bd022816942aae27bbbcc Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Tue, 9 Jul 2024 22:17:52 +0200 Subject: [PATCH 011/219] Update the batch psbt tx review screen ui --- .../transactionSummary.tsx | 5 +- src/app/components/transferFeeView/index.tsx | 8 +- .../signBatchPsbtRequest/index.styled.ts | 35 ++++----- .../screens/signBatchPsbtRequest/index.tsx | 78 +++++++++---------- src/app/ui-library/button.tsx | 5 +- src/locales/en.json | 3 + 6 files changed, 68 insertions(+), 66 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 9a4943c20..7e09388ff 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -161,6 +161,9 @@ function TransactionSummary({ {hasOutputScript && !runeSummary && } + + {feeOutput && {t('FEES')}} + {feeOutput && !showFeeSelector && ( )} - {feeOutput && showFeeSelector && {t('FEES')}} - {feeOutput && showFeeSelector && ( - - {title ?? t('FEES')} + + {title ?? t('NETWORK_FEE')} {customFeeClick && ( - Custom + {t('CUSTOM')} {}}> - Edit + {t('EDIT')} diff --git a/src/app/screens/signBatchPsbtRequest/index.styled.ts b/src/app/screens/signBatchPsbtRequest/index.styled.ts index 643d3501f..3139da8bb 100644 --- a/src/app/screens/signBatchPsbtRequest/index.styled.ts +++ b/src/app/screens/signBatchPsbtRequest/index.styled.ts @@ -1,4 +1,4 @@ -import BottomModal from '@components/bottomModal'; +import Sheet from '@ui-library/sheet'; import styled from 'styled-components'; export const OuterContainer = styled.div` @@ -16,8 +16,8 @@ export const Container = styled.div((props) => ({ flexDirection: 'column', flex: 1, marginTop: props.theme.spacing(11), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, })); export const LoaderContainer = styled.div((props) => ({ @@ -25,26 +25,26 @@ export const LoaderContainer = styled.div((props) => ({ flex: 1, justifyContent: 'center', alignItems: 'center', - marginTop: props.theme.spacing(12), + marginTop: props.theme.space.l, })); export const ButtonContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(20), + padding: props.theme.space.m, + paddingTop: props.theme.space.l, + paddingBottom: props.theme.space.xxl, })); export const TransparentButtonContainer = styled.div((props) => ({ - marginRight: props.theme.spacing(6), + marginRight: props.theme.space.s, width: '100%', })); export const ReviewTransactionText = styled.h1((props) => ({ ...props.theme.headline_s, color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(12), + marginBottom: props.theme.space.l, textAlign: 'left', })); @@ -54,7 +54,7 @@ export const BundleLinkContainer = styled.button((props) => ({ backgroundColor: 'transparent', color: props.theme.colors.tangerine, transition: 'color 0.2s ease', - marginBottom: props.theme.spacing(6), + marginBottom: props.theme.space.s, ':hover': { color: props.theme.colors.tangerine_light, }, @@ -62,10 +62,10 @@ export const BundleLinkContainer = styled.button((props) => ({ export const BundleLinkText = styled.div((props) => ({ ...props.theme.typography.body_medium_m, - marginRight: props.theme.spacing(2), + marginRight: props.theme.space.xxs, })); -export const CustomizedModal = styled(BottomModal)` +export const StyledSheet = styled(Sheet)` display: flex; flex-direction: column; height: 100%; @@ -73,14 +73,13 @@ export const CustomizedModal = styled(BottomModal)` background-color: #181818 !important; `; -export const CustomizedModalContainer = styled(Container)` - margin-top: 0; +export const SheetContainer = styled.div` + margin-top: -40px; `; export const TxReviewModalControls = styled.div((props) => ({ display: 'flex', - columnGap: props.theme.spacing(6), - padding: props.theme.spacing(8), - paddingTop: props.theme.spacing(12), - paddingBottom: props.theme.spacing(20), + columnGap: props.theme.space.s, + paddingTop: props.theme.space.l, + paddingBottom: props.theme.space.xxl, })); diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index a6e8805b5..9be83b49b 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -2,7 +2,6 @@ import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types' import { delay } from '@common/utils/ledger'; import AccountHeaderComponent from '@components/accountHeader'; import AssetModal from '@components/assetModal'; -import ActionButton from '@components/button'; import BurnSection from '@components/confirmBtcTransaction/burnSection'; import DelegateSection from '@components/confirmBtcTransaction/delegateSection'; import MintSection from '@components/confirmBtcTransaction/mintSection'; @@ -29,6 +28,7 @@ import { btcTransaction, parseSummaryForRunes, } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; import Callout from '@ui-library/callout'; import Spinner from '@ui-library/spinner'; import { isLedgerAccount } from '@utils/helper'; @@ -42,11 +42,11 @@ import { BundleLinkText, ButtonContainer, Container, - CustomizedModal, - CustomizedModalContainer, LoaderContainer, OuterContainer, ReviewTransactionText, + SheetContainer, + StyledSheet, TransparentButtonContainer, TxReviewModalControls, } from './index.styled'; @@ -348,60 +348,56 @@ function SignBatchPsbtRequest() { - + )} {!hideAddress && ( - {getTruncatedAddress(address)} + {getTruncatedAddress(address, 6)} )} {!hideCopyButton && } diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 1c8e6a1b4..4ea31b307 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -57,6 +57,10 @@ const EditButton = styled.button` margin-left: ${(props) => props.theme.space.xs}; `; +const FeeRate = styled(StyledP)` + margin: ${(props) => props.theme.space.xxxs} 0; +`; + type Props = { feePerVByte?: BigNumber; fee: BigNumber; @@ -107,7 +111,7 @@ function TransferFeeView({ )} {subtitle && ( - + {subtitle} )} @@ -131,13 +135,13 @@ function TransferFeeView({ thousandSeparator suffix={` ${tUnits('SATS_PER_VB')}`} renderText={(value: string) => ( - + {value} - + )} /> )} - + 0; - const handleOnConfirmClick = (txHex: string) => { + const handleConfirmClick = (txHex: string) => { if (isLedgerAccount(selectedAccount)) { const txType: LedgerTransactionType = 'ORDINALS'; const state: ConfirmOrdinalsTransactionState = { @@ -160,7 +160,7 @@ function ConfirmOrdinalTransaction() { recipients={[{ address: recipientAddress, amountSats: new BigNumber(0) }]} loadingBroadcastedTx={isLoading} signedTxHex={signedTxHex} - onConfirmClick={handleOnConfirmClick} + onConfirmClick={handleConfirmClick} onCancelClick={handleBackButtonClick} ordinalTxUtxo={ordinalUtxo} assetDetail={selectedOrdinal ? selectedOrdinal.number.toString() : ''} From a2b9fa0dc0d644952dbc8d77d24440a51ed312c7 Mon Sep 17 00:00:00 2001 From: Denys Hriaznov Date: Wed, 10 Jul 2024 21:51:19 +0200 Subject: [PATCH 016/219] Make some ui tweaks --- src/app/components/confirmBtcTransactionComponent/index.tsx | 2 +- src/app/components/recipientComponent/index.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index bbf4c446d..75ee8dd4d 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -64,7 +64,7 @@ const ReviewTransactionText = styled.span<{ }>((props) => ({ ...props.theme.typography.headline_s, color: props.theme.colors.white_0, - marginBottom: props.theme.spacing(16), + marginBottom: props.theme.space.l, textAlign: props.centerAligned ? 'center' : 'left', })); diff --git a/src/app/components/recipientComponent/index.tsx b/src/app/components/recipientComponent/index.tsx index 90036fcf1..5f10f1226 100644 --- a/src/app/components/recipientComponent/index.tsx +++ b/src/app/components/recipientComponent/index.tsx @@ -217,11 +217,7 @@ function RecipientComponent({ )} )} - {recipientIndex && totalRecipient && totalRecipient !== 1 && ( - - {`${t('RECIPIENT')} ${recipientIndex}/${totalRecipient}`} - - )} + {heading && {heading}} {value && ( From 94aae0dfbdac66d89b8bc3da77f2ef8a795deead Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Thu, 11 Jul 2024 12:52:09 +0300 Subject: [PATCH 017/219] minor fixes --- package-lock.json | 1717 +++++++++++++++-- package.json | 4 +- src/app/screens/signMessageRequest/index.tsx | 52 +- .../useSignMessageRequest.ts | 2 +- .../screens/signMessageRequestInApp/index.tsx | 9 +- src/app/utils/ledger.ts | 14 +- 6 files changed, 1575 insertions(+), 223 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ffc52d25..214ab6445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "file:../sats-connect-core", + "@sats-connect/core": "0.0.15-be0d447", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "file:../xverse-core-private", + "@secretkeylabs/xverse-core": "17.1.1-5d1ad19", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -147,39 +147,6 @@ "node": "^18.18.2" } }, - "../sats-connect-core": { - "name": "@sats-connect/core", - "version": "0.0.15", - "license": "ISC", - "dependencies": { - "axios": "1.6.8", - "bitcoin-address-validation": "2.2.3", - "buffer": "6.0.3", - "jsontokens": "4.0.1", - "lodash.omit": "4.5.0" - }, - "devDependencies": { - "@types/jest": "^29.2.6", - "@types/lodash.omit": "4.5.9", - "husky": "^8.0.3", - "lint-staged": "^13.2.3", - "prettier": "^2.8.4", - "process": "^0.11.10", - "rimraf": "^3.0.2", - "stream-browserify": "^3.0.0", - "ts-jest": "^29.0.5", - "ts-loader": "^9.4.1", - "tsup": "^8.0.2", - "typescript": "5.4.5", - "util": "^0.12.4", - "vm-browserify": "^1.1.2", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" - }, - "peerDependencies": { - "valibot": "0.33.2" - } - }, "../xverse-core": { "name": "@secretkeylabs/xverse-core", "version": "12.0.1", @@ -259,88 +226,6 @@ "react-dom": ">18.0.0" } }, - "../xverse-core-private": { - "name": "@secretkeylabs/xverse-core", - "version": "17.1.1", - "license": "ISC", - "dependencies": { - "@bitcoinerlab/secp256k1": "^1.0.2", - "@noble/curves": "^1.2.0", - "@noble/secp256k1": "^1.7.1", - "@scure/base": "^1.1.1", - "@scure/bip32": "^1.4.0", - "@scure/bip39": "^1.3.0", - "@scure/btc-signer": "1.2.1", - "@stacks/auth": "6.13.1", - "@stacks/connect": "7.7.1", - "@stacks/encryption": "6.13.1", - "@stacks/network": "6.13.0", - "@stacks/stacking": "6.13.2", - "@stacks/storage": "6.13.1", - "@stacks/transactions": "6.13.1", - "@stacks/wallet-sdk": "6.13.1", - "@tanstack/react-query": "^4.29.3", - "@zondax/ledger-stacks": "^1.0.4", - "async-mutex": "^0.4.0", - "axios": "1.7.0", - "base64url": "^3.0.1", - "bip32": "^4.0.0", - "bip39": "3.0.3", - "bitcoin-address-validation": "^2.2.1", - "bitcoinjs-lib": "^6.1.3", - "bitcoinjs-message": "^2.2.0", - "bn.js": "^5.1.3", - "bs58": "^5.0.0", - "bs58check": "^3.0.1", - "buffer": "6.0.3", - "c32check": "^2.0.0", - "ecdsa-sig-formatter": "^1.0.11", - "ecpair": "^2.1.0", - "json-bigint": "^1.0.0", - "jsontokens": "^4.0.1", - "ledger-bitcoin": "^0.2.1", - "process": "^0.11.10", - "util": "^0.12.4", - "uuidv4": "^6.2.13", - "varuint-bitcoin": "^1.1.2" - }, - "devDependencies": { - "@types/json-bigint": "^1.0.4", - "@types/react": "^18.2.18", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "@vitest/coverage-v8": "^1.6.0", - "airbnb": "^0.0.2", - "axios-mock-adapter": "^1.22.0", - "bip322-js": "^1.1.0", - "eslint": "^8.38.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.3", - "lint-staged": "^13.2.3", - "mockdate": "^3.0.5", - "nock": "^13.5.4", - "prettier": "^2.8.7", - "rimraf": "^3.0.2", - "ts-loader": "^9.4.1", - "typescript": "^4.8.3", - "vitest": "^1.6.0", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" - }, - "engines": { - "node": "^18.18.2" - }, - "peerDependencies": { - "bignumber.js": "^9.0.0", - "micro-packed": ">0.5.0", - "react": ">18.0.0", - "react-dom": ">18.0.0" - } - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -700,6 +585,43 @@ "node": ">=6.9.0" } }, + "node_modules/@bitcoinerlab/descriptors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/descriptors/-/descriptors-1.1.1.tgz", + "integrity": "sha512-AMFkbBBg9T1iWtEmWB23oADk7zaOQix6wUPLXalhTyFDjhkFXDd6pCRfto/HAdaPg/ccM4GMTVgYLee9WdYFyQ==", + "dependencies": { + "@bitcoinerlab/miniscript": "^1.2.1", + "@bitcoinerlab/secp256k1": "^1.0.5", + "bip32": "^4.0.0", + "bitcoinjs-lib": "^6.1.3", + "ecpair": "^2.1.0" + }, + "peerDependencies": { + "ledger-bitcoin": "^0.2.2" + }, + "peerDependenciesMeta": { + "ledger-bitcoin": { + "optional": true + } + } + }, + "node_modules/@bitcoinerlab/miniscript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", + "dependencies": { + "bip68": "^1.0.4" + } + }, + "node_modules/@bitcoinerlab/secp256k1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", + "dependencies": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1402,8 +1324,29 @@ ] }, "node_modules/@sats-connect/core": { - "resolved": "../sats-connect-core", - "link": true + "version": "0.0.15-be0d447", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15-be0d447.tgz", + "integrity": "sha512-Eks6/BP0R1tPSnUn+nfEWGFvTod27tNWVZsF5Uk9DbIiFmCt0/Vc3VvTA25RMXsNtmsp5+TwLW1AzdXXMb3j3g==", + "dependencies": { + "axios": "1.6.8", + "bitcoin-address-validation": "2.2.3", + "buffer": "6.0.3", + "jsontokens": "4.0.1", + "lodash.omit": "4.5.0" + }, + "peerDependencies": { + "valibot": "0.33.2" + } + }, + "node_modules/@sats-connect/core/node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/@scure/base": { "version": "1.1.7", @@ -1413,6 +1356,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", @@ -1454,8 +1432,128 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "resolved": "../xverse-core-private", - "link": true + "version": "17.1.1-5d1ad19", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-5d1ad19/6aa125ec9eeaf38ebeca5ee11a998a7b821267aa", + "integrity": "sha512-n2A+DkgbQpCMGD7Pbx3x1SksSoGbuItNZNaGJAtHhZFdh8kErWCFZvROwBEC6UyiCbGc7yvMo5aN6RIzdv6VtA==", + "license": "ISC", + "dependencies": { + "@bitcoinerlab/secp256k1": "^1.0.2", + "@noble/curves": "^1.2.0", + "@noble/secp256k1": "^1.7.1", + "@scure/base": "^1.1.1", + "@scure/bip32": "^1.4.0", + "@scure/bip39": "^1.3.0", + "@scure/btc-signer": "1.2.1", + "@stacks/auth": "6.13.1", + "@stacks/connect": "7.7.1", + "@stacks/encryption": "6.13.1", + "@stacks/network": "6.13.0", + "@stacks/stacking": "6.13.2", + "@stacks/storage": "6.13.1", + "@stacks/transactions": "6.13.1", + "@stacks/wallet-sdk": "6.13.1", + "@tanstack/react-query": "^4.29.3", + "@zondax/ledger-stacks": "^1.0.4", + "async-mutex": "^0.4.0", + "axios": "1.7.0", + "base64url": "^3.0.1", + "bip32": "^4.0.0", + "bip39": "3.0.3", + "bitcoin-address-validation": "^2.2.1", + "bitcoinjs-lib": "^6.1.3", + "bitcoinjs-message": "^2.2.0", + "bn.js": "^5.1.3", + "bs58": "^5.0.0", + "bs58check": "^3.0.1", + "buffer": "6.0.3", + "c32check": "^2.0.0", + "ecdsa-sig-formatter": "^1.0.11", + "ecpair": "^2.1.0", + "json-bigint": "^1.0.0", + "jsontokens": "^4.0.1", + "ledger-bitcoin": "^0.2.1", + "process": "^0.11.10", + "util": "^0.12.4", + "uuidv4": "^6.2.13", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": "^18.18.2" + }, + "peerDependencies": { + "bignumber.js": "^9.0.0", + "micro-packed": ">0.5.0", + "react": ">18.0.0", + "react-dom": ">18.0.0" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/connect": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", + "integrity": "sha512-MwLRhgRLOGo0Y4IDC0qp9RUX2SZubgse1aI2TN/fz2abNIh1LgmOKUua3w17YiBEZxDD9nyQ4KW1c33bdnrOPw==", + "dependencies": { + "@stacks/auth": "^6.1.1", + "@stacks/connect-ui": "6.4.1", + "@stacks/network": "^6.1.1", + "@stacks/profile": "^6.1.1", + "@stacks/transactions": "^6.1.1", + "jsontokens": "^4.0.1" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@stacks/connect-ui": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@stacks/connect-ui/-/connect-ui-6.4.1.tgz", + "integrity": "sha512-Y6Fp26MUsMQq08zFSWus40rS7RNHrHj6VDJrFUqM9VsksV3wftpsRcy7psQusUvW1DS7fPza67IlM1dcN4rvSg==", + "dependencies": { + "@stencil/core": "^2.17.1" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@secretkeylabs/xverse-core/node_modules/bip39": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", + "integrity": "sha512-P0dKrz4g0V0BjXfx7d9QNkJ/Txcz/k+hM9TnjqjUaXtuOfAvxXSw2rJw8DX0e3ZPwnK/IgDxoRqf0bvoVCqbMg==", + "dependencies": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -1571,11 +1669,66 @@ "zone-file": "^2.0.0-beta.3" } }, + "node_modules/@stacks/stacking": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", + "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "dependencies": { + "@noble/hashes": "1.1.5", + "@scure/base": "1.1.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "@stacks/stacks-blockchain-api-types": "^0.61.0", + "@stacks/transactions": "^6.13.1", + "bs58": "^5.0.0" + } + }, + "node_modules/@stacks/stacking/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/stacking/node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/stacking/node_modules/@stacks/stacks-blockchain-api-types": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", + "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==" + }, "node_modules/@stacks/stacks-blockchain-api-types": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-6.1.1.tgz", "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, + "node_modules/@stacks/storage": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", + "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", + "dependencies": { + "@stacks/auth": "^6.13.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "base64-js": "^1.5.1", + "jsontokens": "^4.0.1" + } + }, "node_modules/@stacks/transactions": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", @@ -1600,6 +1753,54 @@ } ] }, + "node_modules/@stacks/wallet-sdk": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", + "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "dependencies": { + "@scure/bip32": "1.1.3", + "@scure/bip39": "1.1.0", + "@stacks/auth": "^6.13.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "@stacks/profile": "^6.13.1", + "@stacks/storage": "^6.13.1", + "@stacks/transactions": "^6.13.1", + "buffer": "^6.0.3", + "c32check": "^2.0.0", + "jsontokens": "^4.0.1", + "triplesec": "^4.0.3", + "zone-file": "^2.0.0-beta.3" + } + }, + "node_modules/@stacks/wallet-sdk/node_modules/@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/@stacks/wallet-sdk/node_modules/@scure/bip32": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.3.tgz", + "integrity": "sha512-dSH3+LCWONlSNQuF34xZrG6Xas7tp2jSSqHb/pMfXWM0vKE4JZOtK3uJfoWouUVW5IGlls75HkXmYLldZ8ySgQ==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "@noble/hashes": "~1.1.3", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + }, "node_modules/@stencil/core": { "version": "2.22.3", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", @@ -2412,6 +2613,14 @@ "@types/node": "*" } }, + "node_modules/@types/sha.js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", + "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -2440,6 +2649,11 @@ "@types/jest": "*" } }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "node_modules/@types/webextension-polyfill": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.9.2.tgz", @@ -3098,6 +3312,83 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zondax/ledger-stacks": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@zondax/ledger-stacks/-/ledger-stacks-1.0.4.tgz", + "integrity": "sha512-R8CB0CZ2poTzpcG0jhzzXZvXF7axIsmZFhp06aHCUjgz+1df63YbC4tUzyzmseekwqNWnaebWFejQKJ99WiHZA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@ledgerhq/hw-transport": "^6.28.1", + "@stacks/transactions": "^4.1.0", + "varuint-bitcoin": "^1.1.2" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/@stacks/common": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-4.3.5.tgz", + "integrity": "sha512-UuViiQ7fn3vdtTe3739aRzbl+wbukekeQuXgqt8d7nB2HC2HodD7GcHhpUga165cO35CD6lQUtj3vXxJb5Ga+A==", + "dependencies": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4", + "buffer": "^6.0.3" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/@stacks/network": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-4.3.5.tgz", + "integrity": "sha512-TC4+AkuT6qi3MoEGxTftA+4BNp99QvGnI+qtKQkoA1m0KDr8b9hSBUhugJHRhQbWuo7D6q0+JagYEGxLID29Kw==", + "dependencies": { + "@stacks/common": "^4.3.5", + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/@stacks/transactions": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-4.3.8.tgz", + "integrity": "sha512-5xYYv2TdXXM9PVixB79Pr99symQ8fhbVATjempGUxtL23/XUiRiLvJZohDxIE4VQ2EzbB4g4j8Y7oqPjj0h09Q==", + "dependencies": { + "@noble/hashes": "^1.0.0", + "@noble/secp256k1": "^1.5.5", + "@stacks/common": "^4.3.5", + "@stacks/network": "^4.3.5", + "@types/node": "^18.0.4", + "@types/sha.js": "^2.4.0", + "c32check": "^1.1.3", + "lodash.clonedeep": "^4.5.0", + "ripemd160-min": "^0.0.6", + "sha.js": "^2.4.11", + "smart-buffer": "^4.1.0" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@zondax/ledger-stacks/node_modules/c32check": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/c32check/-/c32check-1.1.3.tgz", + "integrity": "sha512-ADADE/PjAbJRlwpG3ShaOMbBUlJJZO7xaYSRD5Tub6PixQlgR4s36y9cvMf/YRGpkqX+QOxIdMw216iC320q9A==", + "dependencies": { + "base-x": "^3.0.8", + "buffer": "^5.6.0", + "cross-sha256": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -3634,6 +3925,14 @@ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" }, + "node_modules/base58-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", + "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3659,52 +3958,189 @@ "integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA==", "dev": true }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "engines": { - "node": ">=0.6" + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bip174": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", + "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "dependencies": { + "safe-buffer": "^5.0.1" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, + "node_modules/bip68": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", + "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==", "engines": { - "node": "*" + "node": ">=4.5.0" } }, - "node_modules/bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "node_modules/bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "dependencies": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.1", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, "engines": { - "node": "*" + "node": ">=8.0.0" } }, - "node_modules/binary-extensions": { + "node_modules/bitcoinjs-message": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", + "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", + "dependencies": { + "bech32": "^1.1.3", + "bs58check": "^2.1.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">=0.10" } }, - "node_modules/bip39": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", - "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "node_modules/bitcoinjs-message/node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", "dependencies": { - "@noble/hashes": "^1.2.0" + "safe-buffer": "^5.0.1" + } + }, + "node_modules/bitcoinjs-message/node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "node_modules/bitcoinjs-message/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bitcoinjs-message/node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" } }, "node_modules/bn.js": { @@ -3959,6 +4395,15 @@ "base-x": "^4.0.0" } }, + "node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3982,6 +4427,14 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equals": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", + "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4732,6 +5185,14 @@ "node-fetch": "^2.6.12" } }, + "node_modules/cross-sha256": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-sha256/-/cross-sha256-1.2.0.tgz", + "integrity": "sha512-KViLNMDZKV7jwFqjFx+rNhG26amnFYYQ0S+VaFlVvpk8tM+2XbFia/don/SjGHg9WQxnFVi6z64CGPuF3T+nNw==", + "dependencies": { + "buffer": "^5.6.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5361,12 +5822,46 @@ "webpack": "^4 || ^5" } }, + "node_modules/drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "dependencies": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ecpair": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", + "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", + "dependencies": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6530,6 +7025,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7554,6 +8054,24 @@ "@babel/runtime": "^7.17.2" } }, + "node_modules/iced-error": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/iced-error/-/iced-error-0.0.13.tgz", + "integrity": "sha512-yEEaG8QfyyRL0SsbNNDw3rVgTyqwHFMCuV6jDvD43f/2shmdaFXkqvFLGhDlsYNSolzYHwVLM/CrXt9GygYopA==" + }, + "node_modules/iced-lock": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/iced-lock/-/iced-lock-1.1.0.tgz", + "integrity": "sha512-J9UMVitgTMYrkUil5EB9/Q4BPWiMpFH156yjDlmMoMRKs3s3PnXj/6G0UlzIOGnNi5JVNk/zVYLXVnuo+1QnqQ==", + "dependencies": { + "iced-runtime": "^1.0.0" + } + }, + "node_modules/iced-runtime": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/iced-runtime/-/iced-runtime-1.0.4.tgz", + "integrity": "sha512-rgiJXNF6ZgF2Clh/TKUlBDW3q51YPDJUXmxGQXx1b8tbZpVpTn+1RX9q1sjNkujXIIaVxZByQzPHHORg7KV51g==" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7845,6 +8363,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8370,6 +8902,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -8496,6 +9036,21 @@ "shell-quote": "^1.7.3" } }, + "node_modules/ledger-bitcoin": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ledger-bitcoin/-/ledger-bitcoin-0.2.3.tgz", + "integrity": "sha512-sWdvMTR5CkebNlM0Mam9ROdpsD7Y4087kj4cbIaCCq8IXShCQ44vE3j0wTmt+sHp13eETgY63OWN1rkuIfMfuQ==", + "dependencies": { + "@bitcoinerlab/descriptors": "^1.0.2", + "@bitcoinerlab/secp256k1": "^1.0.5", + "@ledgerhq/hw-transport": "^6.20.0", + "bip32-path": "^0.4.2", + "bitcoinjs-lib": "^6.1.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8890,6 +9445,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "node_modules/lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", @@ -9307,6 +9867,14 @@ "node": "*" } }, + "node_modules/more-entropy": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", + "integrity": "sha512-e0TxQtU1F6/ZA8WnEA2JLQwwDqBTtZFLJSW7rWgUsQou35wx1IOL0g2O7q7oGoMgIJto+jHMnNGHLfSiylHRrw==", + "dependencies": { + "iced-runtime": ">=0.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9325,6 +9893,11 @@ "multicast-dns": "cli.js" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nano-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", @@ -12910,6 +13483,14 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "node_modules/progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13909,6 +14490,30 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secp256k1": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", + "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.2", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/secp256k1/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14116,6 +14721,11 @@ "sha.js": "bin.js" } }, + "node_modules/sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -14225,6 +14835,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -14808,6 +15427,19 @@ "node": ">=0.6" } }, + "node_modules/triplesec": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/triplesec/-/triplesec-4.0.3.tgz", + "integrity": "sha512-fug70e1nJoCMxsXQJlETisAALohm84vl++IiTTHEqM7Lgqwz62jrlwqOC/gJEAJjO/ByN127sEcioB56HW3wIw==", + "dependencies": { + "iced-error": ">=0.0.9", + "iced-lock": "^1.0.1", + "iced-runtime": "^1.0.2", + "more-entropy": ">=0.0.7", + "progress": "~1.1.2", + "uglify-js": "^3.1.9" + } + }, "node_modules/true-myth": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", @@ -15293,6 +15925,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -15320,6 +15957,17 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "node_modules/uglify-js": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -15426,6 +16074,18 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15450,11 +16110,19 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "dependencies": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } + }, "node_modules/valibot": { "version": "0.33.2", "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.33.2.tgz", @@ -16134,6 +16802,40 @@ "node": ">=8" } }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "dependencies": { + "bs58check": "<3.0.0" + } + }, + "node_modules/wif/node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/wif/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/wif/node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -16598,6 +17300,35 @@ "to-fast-properties": "^2.0.0" } }, + "@bitcoinerlab/descriptors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/descriptors/-/descriptors-1.1.1.tgz", + "integrity": "sha512-AMFkbBBg9T1iWtEmWB23oADk7zaOQix6wUPLXalhTyFDjhkFXDd6pCRfto/HAdaPg/ccM4GMTVgYLee9WdYFyQ==", + "requires": { + "@bitcoinerlab/miniscript": "^1.2.1", + "@bitcoinerlab/secp256k1": "^1.0.5", + "bip32": "^4.0.0", + "bitcoinjs-lib": "^6.1.3", + "ecpair": "^2.1.0" + } + }, + "@bitcoinerlab/miniscript": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", + "requires": { + "bip68": "^1.0.4" + } + }, + "@bitcoinerlab/secp256k1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", + "requires": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -17108,29 +17839,27 @@ "optional": true }, "@sats-connect/core": { - "version": "file:../sats-connect-core", + "version": "0.0.15-be0d447", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15-be0d447.tgz", + "integrity": "sha512-Eks6/BP0R1tPSnUn+nfEWGFvTod27tNWVZsF5Uk9DbIiFmCt0/Vc3VvTA25RMXsNtmsp5+TwLW1AzdXXMb3j3g==", "requires": { - "@types/jest": "^29.2.6", - "@types/lodash.omit": "4.5.9", "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", "buffer": "6.0.3", - "husky": "^8.0.3", "jsontokens": "4.0.1", - "lint-staged": "^13.2.3", - "lodash.omit": "4.5.0", - "prettier": "^2.8.4", - "process": "^0.11.10", - "rimraf": "^3.0.2", - "stream-browserify": "^3.0.0", - "ts-jest": "^29.0.5", - "ts-loader": "^9.4.1", - "tsup": "^8.0.2", - "typescript": "5.4.5", - "util": "^0.12.4", - "vm-browserify": "^1.1.2", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" + "lodash.omit": "4.5.0" + }, + "dependencies": { + "axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + } } }, "@scure/base": { @@ -17138,6 +17867,31 @@ "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==" }, + "@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "requires": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "dependencies": { + "@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + } + } + }, "@scure/bip39": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", @@ -17166,7 +17920,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "file:../xverse-core-private", + "version": "17.1.1-5d1ad19", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-5d1ad19/6aa125ec9eeaf38ebeca5ee11a998a7b821267aa", + "integrity": "sha512-n2A+DkgbQpCMGD7Pbx3x1SksSoGbuItNZNaGJAtHhZFdh8kErWCFZvROwBEC6UyiCbGc7yvMo5aN6RIzdv6VtA==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -17184,19 +17940,11 @@ "@stacks/transactions": "6.13.1", "@stacks/wallet-sdk": "6.13.1", "@tanstack/react-query": "^4.29.3", - "@types/json-bigint": "^1.0.4", - "@types/react": "^18.2.18", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "@vitest/coverage-v8": "^1.6.0", "@zondax/ledger-stacks": "^1.0.4", - "airbnb": "^0.0.2", "async-mutex": "^0.4.0", "axios": "1.7.0", - "axios-mock-adapter": "^1.22.0", "base64url": "^3.0.1", "bip32": "^4.0.0", - "bip322-js": "^1.1.0", "bip39": "3.0.3", "bitcoin-address-validation": "^2.2.1", "bitcoinjs-lib": "^6.1.3", @@ -17208,30 +17956,74 @@ "c32check": "^2.0.0", "ecdsa-sig-formatter": "^1.0.11", "ecpair": "^2.1.0", - "eslint": "^8.38.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.3", "json-bigint": "^1.0.0", "jsontokens": "^4.0.1", "ledger-bitcoin": "^0.2.1", - "lint-staged": "^13.2.3", - "mockdate": "^3.0.5", - "nock": "^13.5.4", - "prettier": "^2.8.7", "process": "^0.11.10", - "rimraf": "^3.0.2", - "ts-loader": "^9.4.1", - "typescript": "^4.8.3", "util": "^0.12.4", "uuidv4": "^6.2.13", - "varuint-bitcoin": "^1.1.2", - "vitest": "^1.6.0", - "webpack": "^5.74.0", - "webpack-cli": "^4.10.0" + "varuint-bitcoin": "^1.1.2" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "requires": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, + "@stacks/connect": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@stacks/connect/-/connect-7.7.1.tgz", + "integrity": "sha512-MwLRhgRLOGo0Y4IDC0qp9RUX2SZubgse1aI2TN/fz2abNIh1LgmOKUua3w17YiBEZxDD9nyQ4KW1c33bdnrOPw==", + "requires": { + "@stacks/auth": "^6.1.1", + "@stacks/connect-ui": "6.4.1", + "@stacks/network": "^6.1.1", + "@stacks/profile": "^6.1.1", + "@stacks/transactions": "^6.1.1", + "jsontokens": "^4.0.1" + } + }, + "@stacks/connect-ui": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@stacks/connect-ui/-/connect-ui-6.4.1.tgz", + "integrity": "sha512-Y6Fp26MUsMQq08zFSWus40rS7RNHrHj6VDJrFUqM9VsksV3wftpsRcy7psQusUvW1DS7fPza67IlM1dcN4rvSg==", + "requires": { + "@stencil/core": "^2.17.1" + } + }, + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + }, + "async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "requires": { + "tslib": "^2.4.0" + } + }, + "bip39": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.3.tgz", + "integrity": "sha512-P0dKrz4g0V0BjXfx7d9QNkJ/Txcz/k+hM9TnjqjUaXtuOfAvxXSw2rJw8DX0e3ZPwnK/IgDxoRqf0bvoVCqbMg==", + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + } } }, "@sinclair/typebox": { @@ -17346,11 +18138,56 @@ "zone-file": "^2.0.0-beta.3" } }, + "@stacks/stacking": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@stacks/stacking/-/stacking-6.13.2.tgz", + "integrity": "sha512-4h1UQuL2+Xdra9zMqzUElvKG9X9fenuNE7hD9sIqyxyLFxeQ7gRqczmTYPsmaj4wY5004JNj+efzGJ0VmpOcAA==", + "requires": { + "@noble/hashes": "1.1.5", + "@scure/base": "1.1.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "@stacks/stacks-blockchain-api-types": "^0.61.0", + "@stacks/transactions": "^6.13.1", + "bs58": "^5.0.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, + "@stacks/stacks-blockchain-api-types": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-0.61.0.tgz", + "integrity": "sha512-yPOfTUboo5eA9BZL/hqMcM71GstrFs9YWzOrJFPeP4cOO1wgYvAcckgBRbgiE3NqeX0A7SLZLDAXLZbATuRq9w==" + } + } + }, "@stacks/stacks-blockchain-api-types": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@stacks/stacks-blockchain-api-types/-/stacks-blockchain-api-types-6.1.1.tgz", "integrity": "sha512-Mw5dBPx3DySPupwaq0iBdm1WdEVXIfhjUVaTjI2iSyzWz4Fgs3U7JCaAezLbgNu7Q69c/ZN4JUDWuo9FVjy7oA==" }, + "@stacks/storage": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@stacks/storage/-/storage-6.13.1.tgz", + "integrity": "sha512-XnzRAETDKW7Ij3cuUNllrrnLOCwctV/XrIHgVWnD04LNL5R9apkwp9IkzaFbyJZ+XrkUBKwtdCgVYype2XgT6w==", + "requires": { + "@stacks/auth": "^6.13.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "base64-js": "^1.5.1", + "jsontokens": "^4.0.1" + } + }, "@stacks/transactions": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-6.13.1.tgz", @@ -17371,6 +18208,44 @@ } } }, + "@stacks/wallet-sdk": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@stacks/wallet-sdk/-/wallet-sdk-6.13.1.tgz", + "integrity": "sha512-262CYKAm1j8oVxfGUIJrHp867j9gm5NrqPM85s0TfCv2QhfLDkvme6nKgmvtL2TecAZkBa5tu8M5DQ7z92WvAQ==", + "requires": { + "@scure/bip32": "1.1.3", + "@scure/bip39": "1.1.0", + "@stacks/auth": "^6.13.1", + "@stacks/common": "^6.13.0", + "@stacks/encryption": "^6.13.1", + "@stacks/network": "^6.13.0", + "@stacks/profile": "^6.13.1", + "@stacks/storage": "^6.13.1", + "@stacks/transactions": "^6.13.1", + "buffer": "6.0.3", + "c32check": "^2.0.0", + "jsontokens": "^4.0.1", + "triplesec": "^4.0.3", + "zone-file": "^2.0.0-beta.3" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", + "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" + }, + "@scure/bip32": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.3.tgz", + "integrity": "sha512-dSH3+LCWONlSNQuF34xZrG6Xas7tp2jSSqHb/pMfXWM0vKE4JZOtK3uJfoWouUVW5IGlls75HkXmYLldZ8ySgQ==", + "requires": { + "@noble/hashes": "~1.1.3", + "@noble/secp256k1": "~1.7.0", + "@scure/base": "~1.1.0" + } + } + } + }, "@stencil/core": { "version": "2.22.3", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", @@ -18040,6 +18915,14 @@ "@types/node": "*" } }, + "@types/sha.js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", + "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "requires": { + "@types/node": "*" + } + }, "@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -18068,6 +18951,11 @@ "@types/jest": "*" } }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "@types/webextension-polyfill": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.9.2.tgz", @@ -18561,6 +19449,82 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@zondax/ledger-stacks": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@zondax/ledger-stacks/-/ledger-stacks-1.0.4.tgz", + "integrity": "sha512-R8CB0CZ2poTzpcG0jhzzXZvXF7axIsmZFhp06aHCUjgz+1df63YbC4tUzyzmseekwqNWnaebWFejQKJ99WiHZA==", + "requires": { + "@babel/runtime": "^7.12.5", + "@ledgerhq/hw-transport": "^6.28.1", + "@stacks/transactions": "^4.1.0", + "varuint-bitcoin": "^1.1.2" + }, + "dependencies": { + "@stacks/common": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@stacks/common/-/common-4.3.5.tgz", + "integrity": "sha512-UuViiQ7fn3vdtTe3739aRzbl+wbukekeQuXgqt8d7nB2HC2HodD7GcHhpUga165cO35CD6lQUtj3vXxJb5Ga+A==", + "requires": { + "@types/bn.js": "^5.1.0", + "@types/node": "^18.0.4", + "buffer": "6.0.3" + } + }, + "@stacks/network": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@stacks/network/-/network-4.3.5.tgz", + "integrity": "sha512-TC4+AkuT6qi3MoEGxTftA+4BNp99QvGnI+qtKQkoA1m0KDr8b9hSBUhugJHRhQbWuo7D6q0+JagYEGxLID29Kw==", + "requires": { + "@stacks/common": "^4.3.5", + "cross-fetch": "^3.1.5" + } + }, + "@stacks/transactions": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@stacks/transactions/-/transactions-4.3.8.tgz", + "integrity": "sha512-5xYYv2TdXXM9PVixB79Pr99symQ8fhbVATjempGUxtL23/XUiRiLvJZohDxIE4VQ2EzbB4g4j8Y7oqPjj0h09Q==", + "requires": { + "@noble/hashes": "^1.0.0", + "@noble/secp256k1": "^1.5.5", + "@stacks/common": "^4.3.5", + "@stacks/network": "^4.3.5", + "@types/node": "^18.0.4", + "@types/sha.js": "^2.4.0", + "c32check": "^1.1.3", + "lodash.clonedeep": "^4.5.0", + "ripemd160-min": "^0.0.6", + "sha.js": "^2.4.11", + "smart-buffer": "^4.1.0" + } + }, + "@types/node": { + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "requires": { + "undici-types": "~5.26.4" + } + }, + "base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "c32check": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/c32check/-/c32check-1.1.3.tgz", + "integrity": "sha512-ADADE/PjAbJRlwpG3ShaOMbBUlJJZO7xaYSRD5Tub6PixQlgR4s36y9cvMf/YRGpkqX+QOxIdMw216iC320q9A==", + "requires": { + "base-x": "^3.0.8", + "buffer": "6.0.3", + "cross-sha256": "^1.2.0" + } + } + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -18973,6 +19937,11 @@ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" }, + "base58-js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/base58-js/-/base58-js-1.0.5.tgz", + "integrity": "sha512-LkkAPP8Zu+c0SVNRTRVDyMfKVORThX+rCViget00xdgLRrKkClCTz1T7cIrpr69ShwV5XJuuoZvMvJ43yURwkA==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -18984,12 +19953,22 @@ "integrity": "sha512-p32+F8dg+ANGx7s8QsZS74ZPHfIycmC2yZcoerzFgbersIYWitPbbF39G6SBx3gyvzyLH5nt1ooocxr0IHuWKA==", "dev": true }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -19012,6 +19991,35 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip174": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", + "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==" + }, + "bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + } + }, + "bip32-path": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/bip32-path/-/bip32-path-0.4.2.tgz", + "integrity": "sha512-ZBMCELjJfcNMkz5bDuJ1WrYvjlhEF5k6mQ8vUr4N7MbVRsXei7ZOg8VhhwMfNiW68NWmLkgkc6WvTickrLGprQ==" + }, "bip39": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", @@ -19020,6 +20028,88 @@ "@noble/hashes": "^1.2.0" } }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bip68": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bip68/-/bip68-1.0.4.tgz", + "integrity": "sha512-O1htyufFTYy3EO0JkHg2CLykdXEtV2ssqw47Gq9A0WByp662xpJnMEB9m43LZjsSDjIAOozWRExlFQk2hlV1XQ==" + }, + "bitcoin-address-validation": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz", + "integrity": "sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==", + "requires": { + "base58-js": "^1.0.0", + "bech32": "^2.0.0", + "sha256-uint8array": "^0.10.3" + } + }, + "bitcoinjs-lib": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.6.tgz", + "integrity": "sha512-Fk8+Vc+e2rMoDU5gXkW9tD+313rhkm5h6N9HfZxXvYU9LedttVvmXKTgd9k5rsQJjkSfsv6XRM8uhJv94SrvcA==", + "requires": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.1", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + } + }, + "bitcoinjs-message": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", + "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", + "requires": { + "bech32": "^1.1.3", + "bs58check": "^2.1.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, + "dependencies": { + "base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + } + } + }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", @@ -19231,6 +20321,15 @@ "base-x": "^4.0.0" } }, + "bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -19240,6 +20339,11 @@ "ieee754": "^1.2.1" } }, + "buffer-equals": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", + "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -19821,6 +20925,14 @@ "node-fetch": "^2.6.12" } }, + "cross-sha256": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-sha256/-/cross-sha256-1.2.0.tgz", + "integrity": "sha512-KViLNMDZKV7jwFqjFx+rNhG26amnFYYQ0S+VaFlVvpk8tM+2XbFia/don/SjGHg9WQxnFVi6z64CGPuF3T+nNw==", + "requires": { + "buffer": "6.0.3" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -20301,12 +21413,40 @@ "dotenv-defaults": "^2.0.2" } }, + "drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "requires": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ecpair": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.1.0.tgz", + "integrity": "sha512-cL/mh3MtJutFOvFc27GPZE2pWL3a3k4YvzUWEOvilnfZVlH3Jwgx/7d6tlD7/75tNk8TG2m+7Kgtz0SI1tWcqw==", + "requires": { + "randombytes": "^2.1.0", + "typeforce": "^1.18.0", + "wif": "^2.0.6" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -21202,6 +22342,11 @@ "flat-cache": "^3.0.4" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -21931,6 +23076,24 @@ "@babel/runtime": "^7.17.2" } }, + "iced-error": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/iced-error/-/iced-error-0.0.13.tgz", + "integrity": "sha512-yEEaG8QfyyRL0SsbNNDw3rVgTyqwHFMCuV6jDvD43f/2shmdaFXkqvFLGhDlsYNSolzYHwVLM/CrXt9GygYopA==" + }, + "iced-lock": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/iced-lock/-/iced-lock-1.1.0.tgz", + "integrity": "sha512-J9UMVitgTMYrkUil5EB9/Q4BPWiMpFH156yjDlmMoMRKs3s3PnXj/6G0UlzIOGnNi5JVNk/zVYLXVnuo+1QnqQ==", + "requires": { + "iced-runtime": "^1.0.0" + } + }, + "iced-runtime": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/iced-runtime/-/iced-runtime-1.0.4.tgz", + "integrity": "sha512-rgiJXNF6ZgF2Clh/TKUlBDW3q51YPDJUXmxGQXx1b8tbZpVpTn+1RX9q1sjNkujXIIaVxZByQzPHHORg7KV51g==" + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -22113,6 +23276,14 @@ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -22472,6 +23643,14 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -22586,6 +23765,18 @@ "shell-quote": "^1.7.3" } }, + "ledger-bitcoin": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/ledger-bitcoin/-/ledger-bitcoin-0.2.3.tgz", + "integrity": "sha512-sWdvMTR5CkebNlM0Mam9ROdpsD7Y4087kj4cbIaCCq8IXShCQ44vE3j0wTmt+sHp13eETgY63OWN1rkuIfMfuQ==", + "requires": { + "@bitcoinerlab/descriptors": "^1.0.2", + "@bitcoinerlab/secp256k1": "^1.0.5", + "@ledgerhq/hw-transport": "^6.20.0", + "bip32-path": "^0.4.2", + "bitcoinjs-lib": "^6.1.3" + } + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -22859,6 +24050,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", @@ -23187,6 +24383,14 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, + "more-entropy": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/more-entropy/-/more-entropy-0.0.7.tgz", + "integrity": "sha512-e0TxQtU1F6/ZA8WnEA2JLQwwDqBTtZFLJSW7rWgUsQou35wx1IOL0g2O7q7oGoMgIJto+jHMnNGHLfSiylHRrw==", + "requires": { + "iced-runtime": ">=0.0.1" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -23202,6 +24406,11 @@ "thunky": "^1.0.2" } }, + "nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "nano-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", @@ -25574,6 +26783,11 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==" + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -26304,6 +27518,28 @@ "ajv-keywords": "^3.5.2" } }, + "secp256k1": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", + "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.2", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -26487,6 +27723,11 @@ "safe-buffer": "^5.0.1" } }, + "sha256-uint8array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz", + "integrity": "sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==" + }, "shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -26568,6 +27809,11 @@ } } }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, "sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -26984,6 +28230,19 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "triplesec": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/triplesec/-/triplesec-4.0.3.tgz", + "integrity": "sha512-fug70e1nJoCMxsXQJlETisAALohm84vl++IiTTHEqM7Lgqwz62jrlwqOC/gJEAJjO/ByN127sEcioB56HW3wIw==", + "requires": { + "iced-error": ">=0.0.9", + "iced-lock": "^1.0.1", + "iced-runtime": "^1.0.2", + "more-entropy": ">=0.0.7", + "progress": "~1.1.2", + "uglify-js": "^3.1.9" + } + }, "true-myth": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", @@ -27338,6 +28597,11 @@ "is-typed-array": "^1.1.9" } }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -27356,6 +28620,11 @@ "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", "dev": true }, + "uglify-js": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", + "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==" + }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -27424,6 +28693,18 @@ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", "requires": {} }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -27444,8 +28725,16 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "requires": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } }, "valibot": { "version": "0.33.2", @@ -27864,6 +29153,42 @@ "stackback": "0.0.2" } }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "requires": { + "bs58check": "<3.0.0" + }, + "dependencies": { + "base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + } + } + }, "wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", diff --git a/package.json b/package.json index a76637fd4..0549eded5 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "file:../sats-connect-core", + "@sats-connect/core": "0.0.15-be0d447", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "file:../xverse-core-private", + "@secretkeylabs/xverse-core": "17.1.1-5d1ad19", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", diff --git a/src/app/screens/signMessageRequest/index.tsx b/src/app/screens/signMessageRequest/index.tsx index d19ab1545..82c861a78 100644 --- a/src/app/screens/signMessageRequest/index.tsx +++ b/src/app/screens/signMessageRequest/index.tsx @@ -16,10 +16,10 @@ import { MessageSigningProtocols, Return, RpcErrorCode } from '@sats-connect/cor import CollapsableContainer from '@screens/signatureRequest/collapsableContainer'; import SignatureRequestMessage from '@screens/signatureRequest/signatureRequestMessage'; import { finalizeMessageSignature } from '@screens/signatureRequest/utils'; -import { bip0322Hash } from '@secretkeylabs/xverse-core'; +import { bip0322Hash, legacyHash } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; -import { handleBip322LedgerMessageSigning } from '@utils/ledger'; +import { handleLedgerMessageSigning } from '@utils/ledger'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -41,7 +41,7 @@ function SignMessageRequest() { const selectedAccount = useSelectedAccount(); const { accountsList, network } = useWalletSelector(); const { payload, tabId, requestToken, confirmSignMessage, requestId } = useSignMessageRequest(); - const { validationError } = useSignMessageValidation(payload); + const { validationError, setValidationError } = useSignMessageValidation(payload); const [addressType, setAddressType] = useState(''); const [isSigning, setIsSigning] = useState(false); @@ -119,22 +119,36 @@ function SignMessageRequest() { setCurrentStepIndex(1); try { - const signature = await handleBip322LedgerMessageSigning({ + const signature = await handleLedgerMessageSigning({ transport, addressIndex: selectedAccount.deviceAccountIndex, address: payload.address, networkType: network.type, message: payload.message, + protocol: (payload as any).protocol as MessageSigningProtocols, }); - const signingMessage = { - source: MESSAGE_SOURCE, - method: SatsConnectMethods.signMessageResponse, - payload: { - signMessageRequest: requestToken, - signMessageResponse: signature, - }, - }; - chrome.tabs.sendMessage(+tabId, signingMessage); + if (requestToken) { + const signingMessage = { + source: MESSAGE_SOURCE, + method: SatsConnectMethods.signMessageResponse, + payload: { + signMessageRequest: requestToken, + signMessageResponse: signature, + }, + }; + chrome.tabs.sendMessage(+tabId, signingMessage); + window.close(); + } + const response = makeRpcSuccessResponse(requestId, { + address: payload.address, + messageHash: + (payload as any).protocol === MessageSigningProtocols.BIP322 + ? bip0322Hash(payload.message) + : legacyHash(payload.message).toString('base64'), + signature, + protocol: (payload as any).protocol, + }); + sendRpcResponse(+tabId, response); window.close(); } catch (e: any) { console.error(e); @@ -178,22 +192,25 @@ function SignMessageRequest() { setIsModalVisible(true); return; } - const bip322signature = await confirmSignMessage(); + const signedMessage = await confirmSignMessage(); if (requestToken) { const signingMessage = { source: MESSAGE_SOURCE, method: SatsConnectMethods.signMessageResponse, payload: { signMessageRequest: requestToken, - signMessageResponse: bip322signature, + signMessageResponse: signedMessage.signature, }, }; chrome.tabs.sendMessage(+tabId, signingMessage); } else { const signMessageResult: Return<'signMessage'> = { address: payload.address, - messageHash: bip0322Hash(payload.message), - signature: bip322signature ?? '', + messageHash: + (payload as any).protocol === MessageSigningProtocols.BIP322 + ? bip0322Hash(payload.message) + : legacyHash(payload.message).toString('base64'), + signature: signedMessage.signature, protocol: (payload as any).protocol, }; const response = makeRpcSuccessResponse(requestId, signMessageResult); @@ -201,6 +218,7 @@ function SignMessageRequest() { } window.close(); } catch (err) { + setValidationError({ error: (err as any).message }); console.log(err); } finally { setIsSigning(false); diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index 4206c8bb8..18056ba6b 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -92,7 +92,7 @@ export const useSignMessageValidation = ( }; }, [requestPayload]); - return { validationError, validateSignMessage }; + return { validationError, validateSignMessage, setValidationError }; }; export const useSignMessageRequest = () => { diff --git a/src/app/screens/signMessageRequestInApp/index.tsx b/src/app/screens/signMessageRequestInApp/index.tsx index 7066d58c4..06d2842b5 100644 --- a/src/app/screens/signMessageRequestInApp/index.tsx +++ b/src/app/screens/signMessageRequestInApp/index.tsx @@ -16,7 +16,7 @@ import { bip0322Hash, MessageSigningProtocols, signMessage } from '@secretkeylab import Button from '@ui-library/button'; import Sheet from '@ui-library/sheet'; import { getTruncatedAddress, isHardwareAccount } from '@utils/helper'; -import { handleBip322LedgerMessageSigning } from '@utils/ledger'; +import { handleLedgerMessageSigning } from '@utils/ledger'; import { useEffect, useState } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -132,12 +132,13 @@ function SignMessageRequestInApp() { setCurrentStepIndex(1); try { - const bip322signature = await handleBip322LedgerMessageSigning({ + const bip322signature = await handleLedgerMessageSigning({ transport, addressIndex: selectedAccount.deviceAccountIndex, address: payload.address, networkType: network.type, message: payload.message, + protocol: MessageSigningProtocols.BIP322, }); await runesApi.submitCancelRunesSellOrder({ @@ -189,14 +190,14 @@ function SignMessageRequestInApp() { setIsModalVisible(true); return; } - const bip322signature = await confirmSignMessage(); + const signedMessage = await confirmSignMessage(); await runesApi.submitCancelRunesSellOrder({ orderIds: payload.orderIds, makerPublicKey: selectedAccount?.ordinalsPublicKey!, makerAddress: selectedAccount?.ordinalsAddress!, token: payload.token, - signature: bip322signature, + signature: signedMessage.signature, }); handleGoBack(); diff --git a/src/app/utils/ledger.ts b/src/app/utils/ledger.ts index 89b0f43c3..c0d9febf8 100644 --- a/src/app/utils/ledger.ts +++ b/src/app/utils/ledger.ts @@ -1,28 +1,36 @@ -import { NetworkType, signSimpleBip322Message, Transport } from '@secretkeylabs/xverse-core'; +import { + MessageSigningProtocols, + NetworkType, + signMessageLedger, + Transport, +} from '@secretkeylabs/xverse-core'; -export const handleBip322LedgerMessageSigning = async ({ +export const handleLedgerMessageSigning = async ({ transport, addressIndex, address, networkType, message, + protocol, }: { transport: Transport; addressIndex?: number; address: string; networkType: NetworkType; message: string; + protocol?: MessageSigningProtocols; }) => { if (addressIndex === undefined) { throw new Error('Account not found'); } - const signature = await signSimpleBip322Message({ + const signature = await signMessageLedger({ transport, networkType, addressIndex, address, message, + protocol, }); return signature; From 17f366ed1af0acfc7c47f225b31f517b31ab3a19 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Thu, 11 Jul 2024 19:25:15 +0800 Subject: [PATCH 018/219] duplicate new component, revert previous changes --- .../confirmBtcTransaction/index.tsx | 3 - .../confirmBtcTransaction/receiveSection.tsx | 33 +- .../transactionSummary.tsx | 3 - .../confirmBtcTransaction/transferSection.tsx | 14 +- .../ContentLabel/common.ts | 11 + .../ContentLabel/index.tsx | 262 +++++++++++++++ .../ContentLabel/preview.tsx | 173 ++++++++++ .../ContentLabel/utils.ts | 54 ++++ .../burnSection.tsx | 56 ++++ .../delegateSection.tsx | 110 +++++++ .../etchSection.tsx | 236 ++++++++++++++ .../index.tsx | 274 ++++++++++++++++ .../inscribeSection/index.tsx | 78 +++++ .../inscribeSection/styles.tsx | 59 ++++ .../itemRow/amount.tsx | 81 +++++ .../amountWithInscriptionSatribute.tsx | 128 ++++++++ .../itemRow/bundleTxView.tsx | 97 ++++++ .../itemRow/inscription.tsx | 117 +++++++ .../itemRow/inscriptionSatributeRow.tsx | 80 +++++ .../itemRow/rareSats.tsx | 125 ++++++++ .../itemRow/runeAmount.tsx | 93 ++++++ .../ledgerStepView.tsx | 87 +++++ .../mintSection.tsx | 98 ++++++ .../receiveSection.tsx | 204 ++++++++++++ .../runes.tsx | 74 +++++ .../transactionSummary.tsx | 205 ++++++++++++ .../transferSection.tsx | 164 ++++++++++ .../txInOutput/transactionInput.tsx | 83 +++++ .../txInOutput/transactionOutput.tsx | 108 +++++++ .../txInOutput/txInOutput.tsx | 103 ++++++ .../confirmBtcTransactionSingleParty/utils.ts | 300 ++++++++++++++++++ src/app/screens/etchRune/index.tsx | 2 +- src/app/screens/mintRune/index.tsx | 2 +- .../restoreFunds/recoverRunes/index.tsx | 2 +- src/app/screens/sendBtc/stepDisplay.tsx | 5 +- src/app/screens/sendRune/stepDisplay.tsx | 4 +- src/app/screens/signPsbtRequest/index.tsx | 2 +- 37 files changed, 3482 insertions(+), 48 deletions(-) create mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts create mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts create mode 100644 src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/index.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/inscribeSection/index.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/inscribeSection/styles.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/amount.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/amountWithInscriptionSatribute.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/bundleTxView.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/inscription.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/inscriptionSatributeRow.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/rareSats.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/runeAmount.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/ledgerStepView.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/mintSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/runes.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionInput.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionOutput.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/txInOutput.tsx create mode 100644 src/app/components/confirmBtcTransactionSingleParty/utils.ts diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index 00f2ca543..e5678829b 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -52,7 +52,6 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - recipientAddress?: string; runeSummary?: RuneSummaryActions | RuneSummary; showCenotaphCallout: boolean; isLoading: boolean; @@ -85,7 +84,6 @@ function ConfirmBtcTransaction({ inputs, outputs, feeOutput, - recipientAddress, // TODO - ensure this logic works for PSBT runeSummary, showCenotaphCallout, isLoading, @@ -215,7 +213,6 @@ function ConfirmBtcTransaction({ inputs={inputs} outputs={outputs} feeOutput={feeOutput} - recipientAddress={recipientAddress} transactionIsFinal={isFinal} showCenotaphCallout={showCenotaphCallout} getFeeForFeeRate={getFeeForFeeRate} diff --git a/src/app/components/confirmBtcTransaction/receiveSection.tsx b/src/app/components/confirmBtcTransaction/receiveSection.tsx index bb4b14b62..af8cd6027 100644 --- a/src/app/components/confirmBtcTransaction/receiveSection.tsx +++ b/src/app/components/confirmBtcTransaction/receiveSection.tsx @@ -11,19 +11,12 @@ import Amount from './itemRow/amount'; import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; import { getOutputsWithAssetsToUserAddress } from './utils'; -const Title = styled.p` - ${(props) => props.theme.typography.body_medium_m}; - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.s}; - margin-bottom: ${(props) => props.theme.space.xs}; -`; - const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', background: props.theme.colors.elevation1, - borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -55,14 +48,13 @@ type Props = { onShowInscription: (inscription: btcTransaction.IOInscription) => void; runeReceipts?: RuneSummary['receipts']; }; - function ReceiveSection({ - outputs, - netAmount, - onShowInscription, - runeReceipts, - transactionIsFinal, -}: Props) { + outputs, + netAmount, + onShowInscription, + runeReceipts, + transactionIsFinal, + }: Props) { const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { hasActivatedRareSatsKey } = useWalletSelector(); const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); @@ -112,12 +104,11 @@ function ReceiveSection({ return ( <> - {(showOrdinalSection || showPaymentSection) && {t('YOU_WILL_RECEIVE')}} {showOrdinalSection && (
- - {t('TO')} + + {t('YOU_WILL_RECEIVE')} @@ -153,8 +144,8 @@ function ReceiveSection({ {showPaymentSection && (
- - {t('TO')} + + {t('YOU_WILL_RECEIVE')} diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 3ce7017dd..ada9cbd09 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -53,7 +53,6 @@ type Props = { inputs: btcTransaction.EnhancedInput[]; outputs: btcTransaction.EnhancedOutput[]; feeOutput?: btcTransaction.TransactionFeeOutput; - recipientAddress?: string; runeSummary?: RuneSummaryActions | RuneSummary; getFeeForFeeRate?: ( feeRate: number, @@ -70,7 +69,6 @@ function TransactionSummary({ inputs, outputs, feeOutput, - recipientAddress, runeSummary, isSubmitting, getFeeForFeeRate, @@ -138,7 +136,6 @@ function TransactionSummary({ ({ type Props = { outputs: btcTransaction.EnhancedOutput[]; inputs: btcTransaction.EnhancedInput[]; - recipientAddress?: string; transactionIsFinal: boolean; runeTransfers?: RuneSummary['transfers']; netAmount: number; @@ -57,7 +56,6 @@ type Props = { function TransferSection({ outputs, inputs, - recipientAddress, transactionIsFinal, runeTransfers, netAmount, @@ -96,18 +94,8 @@ function TransferSection({ return ( <> - {t('YOU_WILL_SEND')} + {t('YOU_WILL_TRANSFER')} -
- - {t('TO')} - - {recipientAddress && ( - - {getTruncatedAddress(recipientAddress, 6)} - - )} -
{ // if transaction is not final, then runes will be delegated and will show up in the delegation section transactionIsFinal && diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts new file mode 100644 index 000000000..db8754581 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts @@ -0,0 +1,11 @@ +export enum ContentType { + SVG = 'SVG', + IMAGE = 'IMAGE', + HTML = 'HTML', + TEXT = 'TEXT', + MARKDOWN = 'MARKDOWN', + JSON = 'JSON', + VIDEO = 'VIDEO', + AUDIO = 'AUDIO', + OTHER = 'OTHER', +} diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx new file mode 100644 index 000000000..2cd79c82d --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx @@ -0,0 +1,262 @@ +import useWalletSelector from '@hooks/useWalletSelector'; +import { DotsThreeVertical, Eye, Share } from '@phosphor-icons/react'; +import { getBrc20Details } from '@secretkeylabs/xverse-core'; +import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; +import { formatNumber } from '@utils/helper'; +import axios from 'axios'; +import BigNumber from 'bignumber.js'; +import { MouseEvent as ReactMouseEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import { ContentType } from './common'; +import Preview from './preview'; +import getSatsDetails from './utils'; + +const previewableContentTypes = new Set([ + ContentType.IMAGE, + ContentType.TEXT, + ContentType.HTML, + ContentType.JSON, + ContentType.VIDEO, + ContentType.AUDIO, + ContentType.MARKDOWN, +]); + +const ordiViewTypes = new Set([ContentType.HTML, ContentType.SVG, ContentType.MARKDOWN]); + +const getContentType = (inputContentType: string) => { + const contentType = inputContentType.toLowerCase(); + if (contentType.includes('svg')) return ContentType.SVG; + if (contentType.includes('image')) return ContentType.IMAGE; + if (contentType.includes('html')) return ContentType.HTML; + if (contentType.includes('markdown')) return ContentType.MARKDOWN; + if (contentType.includes('text')) return ContentType.TEXT; + if (contentType.includes('json')) return ContentType.JSON; + if (contentType.includes('video')) return ContentType.VIDEO; + if (contentType.includes('audio')) return ContentType.AUDIO; + + return ContentType.OTHER; +}; + +const isPreviewable = (contentType: ContentType) => previewableContentTypes.has(contentType); +const isOrdiPreviewable = (contentType: ContentType) => ordiViewTypes.has(contentType); + +const SuffixContainer = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', +})); + +const Container = styled.div({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + cursor: 'pointer', +}); + +const ButtonIcon = styled.div((props) => ({ + width: 20, + height: 20, + marginLeft: props.theme.spacing(4), + position: 'relative', +})); + +const Suffix = styled.div((props) => ({ + ...props.theme.typography.body_medium_s, + color: props.theme.colors.white_400, +})); + +const MenuContainer = styled.div({ + position: 'relative', + display: 'inline-flex', + alignItems: 'center', +}); + +const Menu = styled.div((props) => ({ + position: 'absolute', + top: 0, + right: 0, + + display: 'inline-flex', + padding: props.theme.spacing(6), + flexDirection: 'column', + alignItems: 'flex-start', + + borderRadius: 12, + background: props.theme.colors.background.elevation3, + boxShadow: '0px 4px 16px 0px rgba(0, 0, 0, 0.25)', +})); + +const MenuItem = styled.div((props) => ({ + whiteSpace: 'nowrap', + cursor: 'pointer', + width: '100%', + padding: props.theme.spacing(6), +})); + +type Props = { + type: 'BASE_64' | 'PLAIN_TEXT'; + contentType: string; + content: string; + repeat?: number; + inscriptionId?: string; +}; + +function ContentIcon({ + type, + content, + contentType: inputContentType, + repeat = 1, + inscriptionId, +}: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'INSCRIPTION_REQUEST.PREVIEW' }); + const [showPreview, setShowPreview] = useState(false); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + const { network } = useWalletSelector(); + + useEffect(() => { + // Close menu when clicking outside + if (!showMenu || !menuRef.current) return; + + function handleClickOutside(event) { + if (menuRef.current && !menuRef.current.contains(event.target)) { + setShowMenu(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [menuRef, showMenu]); + + const satsDetails = useMemo( + () => getSatsDetails(content, inputContentType), + [content, inputContentType], + ); + + const brc20Details = useMemo( + () => getBrc20Details(content, inputContentType), + [content, inputContentType], + ); + + if (satsDetails) { + return ( + +
+ {t('SATS.TITLE')} {t(`SATS.${satsDetails.op.toUpperCase()}`)} +
+ {satsDetails.value} +
+ ); + } + + if (brc20Details) { + return ( + +
+ {t('BRC20.TITLE')} {t(`BRC20.${brc20Details.op.toUpperCase()}`)} +
+ + {formatNumber(new BigNumber(brc20Details.value).multipliedBy(repeat).toString())}{' '} + {brc20Details.tick} + +
+ ); + } + + const contentType = getContentType(inputContentType); + + const canShowPreview = isPreviewable(contentType); + const canPreviewInOrd = isOrdiPreviewable(contentType); + + const showPreviewButton = canShowPreview && !canPreviewInOrd; + const showOrdButton = !canShowPreview && canPreviewInOrd; + const showMenuButton = canShowPreview && canPreviewInOrd; + + const onTogglePreview = async (e?: ReactMouseEvent) => { + setShowMenu(false); + + // prevent click from going to menu parent + if (e) e.stopPropagation(); + + if (canShowPreview) setShowPreview((current) => !current); + }; + + const onOrdClick = async (e?: ReactMouseEvent) => { + setShowMenu(false); + + // prevent click from going to menu parent + if (e) e.stopPropagation(); + + const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); + + if (canPreviewInOrd) { + const { data: previewId } = await axios.post( + `${XVERSE_ORDIVIEW_URL(network.type)}/previewHtml`, + { + html: displayContent, + contentType: inputContentType, + }, + ); + + window.open(`${XVERSE_ORDIVIEW_URL(network.type)}/previewHtml/${previewId}`, '_blank'); + } + }; + + const onMenuClick = () => { + setShowMenu((current) => !current); + }; + + const clickAction = () => { + if (showPreviewButton) onTogglePreview(); + if (showOrdButton) onOrdClick(); + if (showMenuButton) onMenuClick(); + }; + + return ( + <> + +
{inputContentType}
+ {showPreviewButton && ( + + + + )} + {showOrdButton && ( + + + + )} + {showMenuButton && ( + + + + + {showMenu && ( + + {t('SHOW')} + {t('PREVIEW')} + + )} + + )} +
+ + setShowPreview(false)} + /> + + ); +} + +export default ContentIcon; diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx new file mode 100644 index 000000000..ca6847dd0 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx @@ -0,0 +1,173 @@ +import { X } from '@phosphor-icons/react'; +import { useLayoutEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import useWalletSelector from '@hooks/useWalletSelector'; +import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; +import { ContentType } from './common'; + +const MIN_FONT_SIZE = 9; + +const Container = styled.div` + width: 100vw; + height: 100vh; + position: fixed; + top: 0px; + left: 0px; + display: flex; + justify-content: center; + align-items: center; + background: rgba(18, 21, 30, 0.85); + backdrop-filter: blur(8px); + z-index: 1; +`; + +const PreviewImg = styled.img` + width: 240px; + height: 240px; + max-width: 80vw; + max-height: 80vh; +`; + +const PreviewVideo = styled.video` + width: 240px; + height: 240px; + max-width: 80vw; + max-height: 80vh; +`; + +const PreviewAudio = styled.audio` + width: 240px; + height: 240px; + max-width: 80vw; + max-height: 80vh; +`; + +const PreviewTextContainer = styled.div` + max-width: 100vw; + max-height: 100vh; + overflow-y: auto; +`; + +type PreviewTextProps = { + fontSize: number; +}; +const PreviewText = styled.pre((props) => ({ + fontSize: props.fontSize || 24, + width: '80vw', + whiteSpace: 'pre-wrap', + wordWrap: 'break-word', +})); + +const CloseTick = styled.div((props) => ({ + width: props.theme.spacing(12), + height: props.theme.spacing(12), + position: 'absolute', + top: props.theme.spacing(12), + right: props.theme.spacing(8), + cursor: 'pointer', +})); + +type Props = { + onClick: () => void; + type: 'BASE_64' | 'PLAIN_TEXT'; + content: string; + contentType: ContentType; + contentTypeRaw: string; + visible: boolean; + inscriptionId?: string; +}; + +function Preview({ + onClick, + type, + content, + contentType, + contentTypeRaw, + visible, + inscriptionId, +}: Props) { + const containerRef = useRef(null); + const textRef = useRef(null); + const [fontSize, setFontSize] = useState(24); + const { network } = useWalletSelector(); + + useLayoutEffect(() => { + // this decreases the font size until the preview text fits in the container + if (!visible || !containerRef.current || !textRef.current || fontSize < MIN_FONT_SIZE) return; + + const { height: parentHeight } = containerRef.current.getBoundingClientRect(); + const { height: preHeight } = textRef.current.getBoundingClientRect(); + + if (preHeight <= parentHeight * 0.9) return; + + setFontSize(fontSize - 0.5); + }, [visible, fontSize]); + + if (!visible) return null; + + let preview: React.ReactElement | null = null; + + if ( + contentType === ContentType.TEXT || + contentType === ContentType.JSON || + contentType === ContentType.MARKDOWN + ) { + const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); + + preview = ( + + + {displayContent} + + + ); + } else if (contentType === ContentType.HTML) { + const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); + + preview = ( + + +
{displayContent}
+
+
+ ); + } else if (contentType === ContentType.IMAGE) { + // workaround to show the inscription preview for an already inscribed inscription + if (inscriptionId) { + preview = ( + + ); + } else { + preview = ; + } + } else if (contentType === ContentType.VIDEO) { + preview = ( + + + + + ); + } else if (contentType === ContentType.AUDIO) { + preview = ( + + + + + ); + } + + return ( + + + + + {preview} + + ); +} + +export default Preview; diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts new file mode 100644 index 000000000..934461416 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts @@ -0,0 +1,54 @@ +import { isValidContentType, isValidFields } from '@secretkeylabs/xverse-core'; + +type SatsDefinition = { + op: 'ns' | 'reg'; + value: string; +}; + +const getSatsDetails = (content: string, contentType: string): undefined | SatsDefinition => { + if (!isValidContentType(contentType)) { + return undefined; + } + + try { + const parsedContent = JSON.parse(content); + + if (parsedContent.p !== 'sns' || !['ns', 'reg'].includes(parsedContent.op)) { + return undefined; + } + + const parsedFields = Object.keys(parsedContent); + const parsedValues = Object.values(parsedContent); + + if (parsedValues.some((v) => typeof v !== 'string')) { + return undefined; + } + + const namespaceRequiredFields = new Set(['p', 'op', 'ns']); + const namespaceOptionalFields = new Set(['about', 'avatar']); + + const nameRequiredFields = new Set(['p', 'op', 'name']); + const nameOptionalFields = new Set(['avatar', 'rev', 'relay']); + + const isValidNs = + parsedContent.op === 'ns' && + isValidFields(parsedFields, namespaceRequiredFields, namespaceOptionalFields); + + const isValidReg = + parsedContent.op === 'reg' && + isValidFields(parsedFields, nameRequiredFields, nameOptionalFields); + + if (!isValidNs && !isValidReg) { + return undefined; + } + + return { + op: parsedContent.op, + value: parsedContent.ns || parsedContent.name, + }; + } catch (e) { + return undefined; + } +}; + +export default getSatsDetails; diff --git a/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx new file mode 100644 index 000000000..a56f46c4c --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx @@ -0,0 +1,56 @@ +import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; +import { RuneSummary } from '@secretkeylabs/xverse-core'; +import InputFeedback from '@ui-library/inputFeedback'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, + marginTop: `${props.theme.space.m}`, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +type Props = { + burns?: RuneSummary['burns']; +}; + +function BurnSection({ burns }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + if (!burns?.length) return null; + + return ( + +
+ +
+ {burns.map((burn) => ( + + + + ))} +
+ ); +} + +export default BurnSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx new file mode 100644 index 000000000..c2b29e9f3 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx @@ -0,0 +1,110 @@ +import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; +import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; +import { WarningOctagon } from '@phosphor-icons/react'; +import { animated, config, useSpring } from '@react-spring/web'; +import { RuneSummary } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import Theme from 'theme'; + +const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + background: props.theme.colors.elevation1, + borderRadius: 12, + padding: `${props.theme.space.m} 0`, + justifyContent: 'center', + marginBottom: props.theme.space.s, +})); + +const RowContainer = styled.div((props) => ({ + padding: `0 ${props.theme.space.m}`, + marginTop: `${props.theme.space.m}`, +})); + +const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: props.spaceBetween ? 'space-between' : 'initial', +})); + +const Header = styled(RowCenter)((props) => ({ + padding: `0 ${props.theme.space.m}`, +})); + +const WarningButton = styled.button` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + background-color: ${(props) => props.theme.colors.elevation1}; + padding: ${(props) => props.theme.space.m}; + padding-bottom: 0; +`; + +const DelegationDescription = styled(StyledP)` + padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; + padding-bottom: ${(props) => props.theme.space.xs}; +`; + +const Title = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xxs}; +`; + +type Props = { + delegations?: RuneSummary['transfers']; +}; + +function DelegateSection({ delegations }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + + const [showDelegationInfo, setShowDelegationInfo] = useState(false); + + const slideInStyles = useSpring({ + config: { ...config.gentle, duration: 200 }, + from: { opacity: 0, maxHeight: 0 }, + to: { opacity: 1, maxHeight: 100 }, + reverse: !showDelegationInfo, + }); + + const arrowRotation = useSpring({ + transform: showDelegationInfo ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); + + if (!delegations?.length) return null; + + return ( + +
+ + {t('YOU_WILL_DELEGATE')} + +
+ {delegations.map((delegation) => ( + + + + ))} + setShowDelegationInfo((prevState) => !prevState)}> + + + + {t('UNKNOWN_RUNE_RECIPIENTS')} + + + + + + + {t('RUNE_DELEGATION_DESCRIPTION')} + + +
+ ); +} + +export default DelegateSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx new file mode 100644 index 000000000..0b1f25230 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx @@ -0,0 +1,236 @@ +import useOrdinalsApi from '@hooks/apiClients/useOrdinalsApi'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import { ArrowRight } from '@phosphor-icons/react'; +import { EtchActionDetails, Inscription, RUNE_DISPLAY_DEFAULTS } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; +import Theme from '../../../theme'; +import InscribeSection from './inscribeSection'; +import { + AddressLabel, + Container, + Header, + RowCenter, + RuneAmount, + RuneData, + RuneImage, + RuneSymbol, + RuneValue, +} from './runes'; + +type Props = { + etch?: EtchActionDetails; +}; + +/** + * only used for ordinals service etches + */ +function EtchSection({ etch }: Props) { + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { ordinalsAddress } = useSelectedAccount(); + const [delegateInscriptionDetails, setDelegateInscriptionDetails] = useState( + null, + ); + const ordinalsApi = useOrdinalsApi(); + + const fetchInscriptionDetails = useCallback(async (inscriptionId: string) => { + const inscriptionDetails = await ordinalsApi.getInscription(inscriptionId); + if (inscriptionDetails) { + setDelegateInscriptionDetails(inscriptionDetails); + } + }, []); + + useEffect(() => { + if (etch?.delegateInscriptionId) { + fetchInscriptionDetails(etch.delegateInscriptionId); + } + }, [fetchInscriptionDetails, etch?.delegateInscriptionId]); + + if (!etch) return null; + + return ( + <> + +
+ + {t('YOU_WILL_ISSUE')} + +
+
+ + {t('NAME')} + + + {etch.runeName} + +
+
+ + {t('SYMBOL')} + + + {etch.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol} + +
+
+ + {t('DIVISIBILITY')} + + + {etch.divisibility || RUNE_DISPLAY_DEFAULTS.divisibility} + +
+
+ + {t('MINTABLE')} + + + {etch.isMintable ? t('YES') : t('NO')} + +
+
+ + {t('MINT_AMOUNT')} + + + ( + + {value} + + )} + /> + +
+
+ + {t('MINTING_LIMIT')} + + ( + + {value} + + )} + /> +
+ {etch.terms?.heightStart || + (etch.terms?.heightEnd && ( +
+ + {t('RUNE_BLOCK_HEIGHT_TERM')} + + + {`${etch.terms.heightStart}/${etch.terms.heightEnd}`} + +
+ ))} + {etch.terms?.offsetStart || + (etch.terms?.offsetEnd && ( +
+ + {t('RUNE_BLOCK_OFFSET_TERM')} + + + {`${etch.terms.offsetStart ? etch.terms.offsetStart : '-'}/${etch.terms.offsetEnd}`} + +
+ ))} +
+ {etch.premine && ( + +
+ + {t('YOU_WILL_RECEIVE')} + + + + + {' '} + {etch.destinationAddress && etch.destinationAddress !== ordinalsAddress + ? getShortTruncatedAddress(etch.destinationAddress) + : t('YOUR_ORDINAL_ADDRESS')} + + +
+
+ + {etch.runeName} + +
+
+ + + + {etch.symbol || '¤'} + + + + + {t('AMOUNT')} + + {etch && ( + + {t('RUNE_SIZE')}: {RUNE_DISPLAY_DEFAULTS.size} Sats + + )} + + + ( + + + {value} + + + {etch?.symbol ?? '¤'} + + + )} + /> +
+
+ )} + {etch.inscriptionDetails && ( + + )} + {etch.delegateInscriptionId && delegateInscriptionDetails && ( + + )} + + ); +} + +export default EtchSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/index.tsx b/src/app/components/confirmBtcTransactionSingleParty/index.tsx new file mode 100644 index 000000000..5482d2e85 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/index.tsx @@ -0,0 +1,274 @@ +import { delay } from '@common/utils/ledger'; +import { Tab } from '@components/tabBar'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import TransportFactory from '@ledgerhq/hw-transport-webusb'; +import { + RuneSummary, + RuneSummaryActions, + Transport, + btcTransaction, +} from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; +import Callout from '@ui-library/callout'; +import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; +import Sheet from '@ui-library/sheet'; +import Spinner from '@ui-library/spinner'; +import { isLedgerAccount } from '@utils/helper'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; +import SendLayout from '../../layouts/sendLayout'; +import LedgerStepView, { Steps } from './ledgerStepView'; +import TransactionSummary from './transactionSummary'; + +const LoaderContainer = styled.div(() => ({ + display: 'flex', + flex: 1, + justifyContent: 'center', + alignItems: 'center', +})); + +const ReviewTransactionText = styled(StyledP)` + text-align: left; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +const SpacedCallout = styled(Callout)` + margin-bottom: ${(props) => props.theme.space.m}; +`; + +const SuccessActionsContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: props.theme.space.s, + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, + marginBottom: props.theme.space.xxl, + marginTop: props.theme.space.xxl, +})); + +type Props = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + feeOutput?: btcTransaction.TransactionFeeOutput; + recipientAddress?: string; + runeSummary?: RuneSummaryActions | RuneSummary; + showCenotaphCallout: boolean; + isLoading: boolean; + isSubmitting: boolean; + isBroadcast?: boolean; + isError?: boolean; + showAccountHeader?: boolean; + hideBottomBar?: boolean; + cancelText: string; + confirmText: string; + onConfirm: (ledgerTransport?: Transport) => void; + onCancel: () => void; + onBackClick?: () => void; + confirmDisabled?: boolean; + getFeeForFeeRate?: ( + feeRate: number, + useEffectiveFeeRate?: boolean, + ) => Promise; + onFeeRateSet?: (feeRate: number) => void; + feeRate?: number; + isFinal?: boolean; + // TODO: add sighash single warning + hasSigHashSingle?: boolean; + hasSigHashNone?: boolean; + title?: string; + selectedBottomTab?: Tab; +}; + +function ConfirmBtcTransactionSingleParty({ + inputs, + outputs, + feeOutput, + recipientAddress, + runeSummary, + showCenotaphCallout, + isLoading, + isSubmitting, + isBroadcast, + isError = false, + cancelText, + confirmText, + onConfirm, + onCancel, + onBackClick, + showAccountHeader, + hideBottomBar, + confirmDisabled = false, + getFeeForFeeRate, + onFeeRateSet, + feeRate, + hasSigHashNone = false, + isFinal = true, + hasSigHashSingle = false, + title, + selectedBottomTab, +}: Props) { + const [isModalVisible, setIsModalVisible] = useState(false); + const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger); + const [isButtonDisabled, setIsButtonDisabled] = useState(false); + const [isConnectSuccess, setIsConnectSuccess] = useState(false); + const [isConnectFailed, setIsConnectFailed] = useState(false); + const [isTxRejected, setIsTxRejected] = useState(false); + + const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); + const { t: signatureRequestTranslate } = useTranslation('translation', { + keyPrefix: 'SIGNATURE_REQUEST', + }); + const selectedAccount = useSelectedAccount(); + + const hideBackButton = !onBackClick; + const hasInsufficientRunes = + runeSummary?.transfers?.some((transfer) => !transfer.hasSufficientBalance) ?? false; + const validMintingRune = + !runeSummary?.mint || + (runeSummary?.mint && runeSummary.mint.runeIsOpen && runeSummary.mint.runeIsMintable); + + const onConfirmPress = async () => { + if (!isLedgerAccount(selectedAccount)) { + return onConfirm(); + } + + // show ledger connection screens + setIsModalVisible(true); + }; + + const handleConnectAndConfirm = async () => { + if (!selectedAccount) { + console.error('No account selected'); + return; + } + setIsButtonDisabled(true); + + const transport = await TransportFactory.create(); + + if (!transport) { + setIsConnectSuccess(false); + setIsConnectFailed(true); + setIsButtonDisabled(false); + return; + } + + setIsConnectSuccess(true); + await delay(1500); + + if (currentStep !== Steps.ExternalInputs && currentStep !== Steps.ConfirmTransaction) { + setCurrentStep(Steps.ExternalInputs); + return; + } + + if (currentStep !== Steps.ConfirmTransaction) { + setCurrentStep(Steps.ConfirmTransaction); + } + + try { + onConfirm(transport); + } catch (err) { + console.error(err); + setIsTxRejected(true); + } + }; + + const goToConfirmationStep = () => { + setCurrentStep(Steps.ConfirmTransaction); + + handleConnectAndConfirm(); + }; + + const handleRetry = async () => { + setIsTxRejected(false); + setIsConnectSuccess(false); + setCurrentStep(Steps.ConnectLedger); + }; + + return isLoading ? ( + + + + ) : ( + <> + + + {title || t('REVIEW_TRANSACTION')} + + {hasSigHashNone && ( + + )} + {!isBroadcast && } + + {!isLoading && ( + + + {isExpanded && ( + + {inputs.map((input) => ( + + ))} + + {t('OUTPUT')} + + {outputs.map((output, index) => ( + + ))} + + )} +
+ ); +} + +export default TxInOutput; diff --git a/src/app/components/confirmBtcTransactionSingleParty/utils.ts b/src/app/components/confirmBtcTransactionSingleParty/utils.ts new file mode 100644 index 000000000..1e8b89ca3 --- /dev/null +++ b/src/app/components/confirmBtcTransactionSingleParty/utils.ts @@ -0,0 +1,300 @@ +import { btcTransaction, BundleSatRange, FungibleToken } from '@secretkeylabs/xverse-core'; + +export type SatRangeTx = { + totalSats: number; + offset: number; + fromAddress: string; + inscriptions: (Omit & { + content_type: string; + inscription_number: number; + })[]; + satributes: btcTransaction.IOSatribute['types']; +}; + +const DUMMY_OFFSET = -1; + +export const isScriptOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionScriptOutput => + (output as btcTransaction.TransactionScriptOutput).script !== undefined; + +export const isPubKeyOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionPubKeyOutput => + !!(output as btcTransaction.TransactionPubKeyOutput).pubKeys?.length; + +export const isAddressOutput = ( + output: btcTransaction.EnhancedOutput, +): output is btcTransaction.TransactionOutput => + (output as btcTransaction.TransactionOutput).address !== undefined; + +type CommonInputOutputUtilProps = { + inputs: btcTransaction.EnhancedInput[]; + outputs: btcTransaction.EnhancedOutput[]; + btcAddress: string; + ordinalsAddress: string; +}; + +export const getNetAmount = ({ + inputs, + outputs, + btcAddress, + ordinalsAddress, +}: CommonInputOutputUtilProps) => { + const initialValue = 0; + + const totalUserSpend = inputs.reduce((accumulator: number, input) => { + const isFromUserAddress = [btcAddress, ordinalsAddress].includes(input.extendedUtxo.address); + if (isFromUserAddress) { + return accumulator + input.extendedUtxo.utxo.value; + } + return accumulator; + }, initialValue); + + const totalUserReceive = outputs.reduce((accumulator: number, output) => { + const isToUserAddress = + isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); + if (isToUserAddress) { + return accumulator + output.amount; + } + return accumulator; + }, initialValue); + + return totalUserReceive - totalUserSpend; +}; + +export const getOutputsWithAssetsFromUserAddress = ({ + btcAddress, + ordinalsAddress, + outputs, +}: Omit) => { + // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes + const outputsFromPayment: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; + const outputsFromOrdinal: ( + | btcTransaction.TransactionOutput + | btcTransaction.TransactionPubKeyOutput + )[] = []; + outputs.forEach((output) => { + if (isScriptOutput(output)) { + return; + } + + const itemsFromPayment: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; + const itemsFromOrdinal: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; + [...output.inscriptions, ...output.satributes].forEach((item) => { + if (item.fromAddress === btcAddress) { + return itemsFromPayment.push(item); + } + if (item.fromAddress === ordinalsAddress) { + itemsFromOrdinal.push(item); + } + }); + + if (itemsFromOrdinal.length > 0) { + outputsFromOrdinal.push(output); + } + if (itemsFromPayment.length > 0) { + outputsFromPayment.push(output); + } + }); + + return { outputsFromPayment, outputsFromOrdinal }; +}; + +export const getInputsWitAssetsFromUserAddress = ({ + btcAddress, + ordinalsAddress, + inputs, +}: Omit) => { + // we want to discard inputs that are not from user address and do not have inscriptions or satributes + const inputFromPayment: btcTransaction.EnhancedInput[] = []; + const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; + inputs.forEach((input) => { + if (!input.inscriptions.length && !input.satributes.length) { + return; + } + + if (input.extendedUtxo.address === btcAddress) { + return inputFromPayment.push(input); + } + if (input.extendedUtxo.address === ordinalsAddress) { + inputFromOrdinal.push(input); + } + }); + + return { inputFromPayment, inputFromOrdinal }; +}; + +export const getOutputsWithAssetsToUserAddress = ({ + btcAddress, + ordinalsAddress, + outputs, +}: Omit) => { + const outputsToPayment: btcTransaction.TransactionOutput[] = []; + const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; + outputs.forEach((output) => { + // we want to discard outputs that are not spendable or are not to user address + if ( + isScriptOutput(output) || + isPubKeyOutput(output) || + ![btcAddress, ordinalsAddress].includes(output.address) + ) { + return; + } + + if (output.address === btcAddress) { + return outputsToPayment.push(output); + } + + // we don't want to show amount to ordinals address, because it's not spendable + if ( + output.address === ordinalsAddress && + (output.inscriptions.length > 0 || output.satributes.length > 0) + ) { + outputsToOrdinal.push(output); + } + }); + + return { outputsToPayment, outputsToOrdinal }; +}; + +export const mapTxSatributeInfoToBundleInfo = (item: btcTransaction.IOSatribute | SatRangeTx) => { + const commonProps = { + offset: item.offset, + block: 0, + range: { + start: '0', + end: '0', + }, + yearMined: 0, + }; + + // SatRangeTx + if ('totalSats' in item) { + return { + ...commonProps, + totalSats: item.totalSats, + inscriptions: item.inscriptions, + satributes: item.satributes, + } as BundleSatRange; + } + + // btcTransaction.IOSatribute + return { + ...commonProps, + totalSats: item.amount, + inscriptions: [], + satributes: item.types, + } as BundleSatRange; +}; + +export const getSatRangesWithInscriptions = ({ + satributes, + inscriptions, + amount, +}: { + inscriptions: btcTransaction.IOInscription[]; + satributes: btcTransaction.IOSatribute[]; + amount: number; +}) => { + const satRanges: { + [offset: number]: SatRangeTx; + } = {}; + + satributes.forEach((satribute) => { + const { types, amount: totalSats, ...rest } = satribute; + satRanges[rest.offset] = { ...rest, satributes: types, totalSats, inscriptions: [] }; + }); + + inscriptions.forEach((inscription) => { + const { contentType, number, ...inscriptionRest } = inscription; + const mappedInscription = { + ...inscriptionRest, + content_type: contentType, + inscription_number: number, + }; + if (satRanges[inscription.offset]) { + satRanges[inscription.offset] = { + ...satRanges[inscription.offset], + inscriptions: [...satRanges[inscription.offset].inscriptions, mappedInscription], + }; + return; + } + + satRanges[inscription.offset] = { + totalSats: 1, + offset: inscription.offset, + fromAddress: inscription.fromAddress, + inscriptions: [mappedInscription], + satributes: ['COMMON'], + }; + }); + + const { amountOfExoticsOrInscribedSats, totalExoticSats } = Object.values(satRanges).reduce( + (acc, range) => ({ + amountOfExoticsOrInscribedSats: acc.amountOfExoticsOrInscribedSats + range.totalSats, + totalExoticSats: + acc.totalExoticSats + (!range.satributes.includes('COMMON') ? range.totalSats : 0), + }), + { + amountOfExoticsOrInscribedSats: 0, + totalExoticSats: 0, + }, + ); + + if (amountOfExoticsOrInscribedSats < amount) { + satRanges[DUMMY_OFFSET] = { + totalSats: amount - amountOfExoticsOrInscribedSats, + offset: DUMMY_OFFSET, + fromAddress: '', + inscriptions: [], + satributes: ['COMMON'], + }; + } + + // sort should be: inscribed rare, rare, inscribed common, common + const satRangesArray = Object.values(satRanges).sort((a, b) => { + // Check conditions for each category + const aHasInscriptions = a.inscriptions.length > 0; + const bHasInscriptions = b.inscriptions.length > 0; + const aHasRareSatributes = a.satributes.some((s) => s !== 'COMMON'); + const bHasRareSatributes = b.satributes.some((s) => s !== 'COMMON'); + + // sats not rare and not inscribed at bottom + if (!aHasInscriptions && !aHasRareSatributes) return 1; + + // sats inscribed and rare at top + if (aHasInscriptions && aHasRareSatributes) return -1; + + // sats not inscribed and rare below inscribed and rare + if (bHasInscriptions && bHasRareSatributes) return 1; + + // sats inscribed and not rare above sats not inscribed and not rare + if (aHasRareSatributes) return -1; + if (bHasRareSatributes) return 1; + + // equal ranges + return 0; + }); + + return { satRanges: satRangesArray, totalExoticSats }; +}; + +export const mapRuneNameToPlaceholder = ( + runeName: string, + symbol: string, + inscriptionId: string, +): FungibleToken => ({ + protocol: 'runes', + name: runeName, + assetName: '', + balance: '', + principal: '', + total_received: '', + total_sent: '', + runeSymbol: symbol, + runeInscriptionId: inscriptionId, +}); diff --git a/src/app/screens/etchRune/index.tsx b/src/app/screens/etchRune/index.tsx index 28fa75708..47e340b1b 100644 --- a/src/app/screens/etchRune/index.tsx +++ b/src/app/screens/etchRune/index.tsx @@ -1,4 +1,4 @@ -import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; +import ConfirmBtcTransaction from 'app/components/confirmBtcTransaction'; import RequestError from '@components/requests/requestError'; import { Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; diff --git a/src/app/screens/mintRune/index.tsx b/src/app/screens/mintRune/index.tsx index 1ca9aee17..641fb05fc 100644 --- a/src/app/screens/mintRune/index.tsx +++ b/src/app/screens/mintRune/index.tsx @@ -1,4 +1,4 @@ -import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; +import ConfirmBtcTransaction from 'app/components/confirmBtcTransaction'; import RequestError from '@components/requests/requestError'; import { RUNE_DISPLAY_DEFAULTS, Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; diff --git a/src/app/screens/restoreFunds/recoverRunes/index.tsx b/src/app/screens/restoreFunds/recoverRunes/index.tsx index b0fbabb04..a30206e58 100644 --- a/src/app/screens/restoreFunds/recoverRunes/index.tsx +++ b/src/app/screens/restoreFunds/recoverRunes/index.tsx @@ -1,4 +1,4 @@ -import ConfirmBitcoinTransaction from '@components/confirmBtcTransaction'; +import ConfirmBitcoinTransaction from 'app/components/confirmBtcTransaction'; import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; diff --git a/src/app/screens/sendBtc/stepDisplay.tsx b/src/app/screens/sendBtc/stepDisplay.tsx index ff64687a9..8392ba7c1 100644 --- a/src/app/screens/sendBtc/stepDisplay.tsx +++ b/src/app/screens/sendBtc/stepDisplay.tsx @@ -1,4 +1,4 @@ -import ConfirmBitcoinTransaction from '@components/confirmBtcTransaction'; +import ConfirmBitcoinTransaction from 'app/components/confirmBtcTransaction'; import TokenImage from '@components/tokenImage'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -7,6 +7,7 @@ import AmountSelector from './amountSelector'; import BtcRecipientSelector from './btcRecipientSelector'; import { TransactionSummary } from './helpers'; import { Step, getNextStep } from './steps'; +import ConfirmBtcTransactionSingleParty from '@components/confirmBtcTransactionSingleParty'; const TitleContainer = styled.div` display: flex; @@ -120,7 +121,7 @@ function StepDisplay({ return null; } return ( - Date: Thu, 11 Jul 2024 19:34:14 +0800 Subject: [PATCH 019/219] remove duplicated components --- .../ContentLabel/common.ts | 11 - .../ContentLabel/index.tsx | 262 ------------------ .../ContentLabel/preview.tsx | 173 ------------ .../ContentLabel/utils.ts | 54 ---- .../etchSection.tsx | 2 +- .../inscribeSection/index.tsx | 78 ------ .../inscribeSection/styles.tsx | 59 ---- .../itemRow/amount.tsx | 81 ------ .../amountWithInscriptionSatribute.tsx | 128 --------- .../itemRow/bundleTxView.tsx | 97 ------- .../itemRow/inscription.tsx | 117 -------- .../itemRow/inscriptionSatributeRow.tsx | 80 ------ .../itemRow/rareSats.tsx | 125 --------- .../itemRow/runeAmount.tsx | 93 ------- .../receiveSection.tsx | 4 +- .../transactionSummary.tsx | 4 +- .../transferSection.tsx | 6 +- .../txInOutput/transactionInput.tsx | 83 ------ .../txInOutput/transactionOutput.tsx | 108 -------- .../txInOutput/txInOutput.tsx | 103 ------- 20 files changed, 8 insertions(+), 1660 deletions(-) delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/inscribeSection/index.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/inscribeSection/styles.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/amount.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/amountWithInscriptionSatribute.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/bundleTxView.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/inscription.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/inscriptionSatributeRow.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/rareSats.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/itemRow/runeAmount.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionInput.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionOutput.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/txInOutput/txInOutput.tsx diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts deleted file mode 100644 index db8754581..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/common.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum ContentType { - SVG = 'SVG', - IMAGE = 'IMAGE', - HTML = 'HTML', - TEXT = 'TEXT', - MARKDOWN = 'MARKDOWN', - JSON = 'JSON', - VIDEO = 'VIDEO', - AUDIO = 'AUDIO', - OTHER = 'OTHER', -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx deleted file mode 100644 index 2cd79c82d..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/index.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import useWalletSelector from '@hooks/useWalletSelector'; -import { DotsThreeVertical, Eye, Share } from '@phosphor-icons/react'; -import { getBrc20Details } from '@secretkeylabs/xverse-core'; -import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; -import { formatNumber } from '@utils/helper'; -import axios from 'axios'; -import BigNumber from 'bignumber.js'; -import { MouseEvent as ReactMouseEvent, useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import { ContentType } from './common'; -import Preview from './preview'; -import getSatsDetails from './utils'; - -const previewableContentTypes = new Set([ - ContentType.IMAGE, - ContentType.TEXT, - ContentType.HTML, - ContentType.JSON, - ContentType.VIDEO, - ContentType.AUDIO, - ContentType.MARKDOWN, -]); - -const ordiViewTypes = new Set([ContentType.HTML, ContentType.SVG, ContentType.MARKDOWN]); - -const getContentType = (inputContentType: string) => { - const contentType = inputContentType.toLowerCase(); - if (contentType.includes('svg')) return ContentType.SVG; - if (contentType.includes('image')) return ContentType.IMAGE; - if (contentType.includes('html')) return ContentType.HTML; - if (contentType.includes('markdown')) return ContentType.MARKDOWN; - if (contentType.includes('text')) return ContentType.TEXT; - if (contentType.includes('json')) return ContentType.JSON; - if (contentType.includes('video')) return ContentType.VIDEO; - if (contentType.includes('audio')) return ContentType.AUDIO; - - return ContentType.OTHER; -}; - -const isPreviewable = (contentType: ContentType) => previewableContentTypes.has(contentType); -const isOrdiPreviewable = (contentType: ContentType) => ordiViewTypes.has(contentType); - -const SuffixContainer = styled.div((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_0, - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-end', -})); - -const Container = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - cursor: 'pointer', -}); - -const ButtonIcon = styled.div((props) => ({ - width: 20, - height: 20, - marginLeft: props.theme.spacing(4), - position: 'relative', -})); - -const Suffix = styled.div((props) => ({ - ...props.theme.typography.body_medium_s, - color: props.theme.colors.white_400, -})); - -const MenuContainer = styled.div({ - position: 'relative', - display: 'inline-flex', - alignItems: 'center', -}); - -const Menu = styled.div((props) => ({ - position: 'absolute', - top: 0, - right: 0, - - display: 'inline-flex', - padding: props.theme.spacing(6), - flexDirection: 'column', - alignItems: 'flex-start', - - borderRadius: 12, - background: props.theme.colors.background.elevation3, - boxShadow: '0px 4px 16px 0px rgba(0, 0, 0, 0.25)', -})); - -const MenuItem = styled.div((props) => ({ - whiteSpace: 'nowrap', - cursor: 'pointer', - width: '100%', - padding: props.theme.spacing(6), -})); - -type Props = { - type: 'BASE_64' | 'PLAIN_TEXT'; - contentType: string; - content: string; - repeat?: number; - inscriptionId?: string; -}; - -function ContentIcon({ - type, - content, - contentType: inputContentType, - repeat = 1, - inscriptionId, -}: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'INSCRIPTION_REQUEST.PREVIEW' }); - const [showPreview, setShowPreview] = useState(false); - const [showMenu, setShowMenu] = useState(false); - const menuRef = useRef(null); - const { network } = useWalletSelector(); - - useEffect(() => { - // Close menu when clicking outside - if (!showMenu || !menuRef.current) return; - - function handleClickOutside(event) { - if (menuRef.current && !menuRef.current.contains(event.target)) { - setShowMenu(false); - } - } - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [menuRef, showMenu]); - - const satsDetails = useMemo( - () => getSatsDetails(content, inputContentType), - [content, inputContentType], - ); - - const brc20Details = useMemo( - () => getBrc20Details(content, inputContentType), - [content, inputContentType], - ); - - if (satsDetails) { - return ( - -
- {t('SATS.TITLE')} {t(`SATS.${satsDetails.op.toUpperCase()}`)} -
- {satsDetails.value} -
- ); - } - - if (brc20Details) { - return ( - -
- {t('BRC20.TITLE')} {t(`BRC20.${brc20Details.op.toUpperCase()}`)} -
- - {formatNumber(new BigNumber(brc20Details.value).multipliedBy(repeat).toString())}{' '} - {brc20Details.tick} - -
- ); - } - - const contentType = getContentType(inputContentType); - - const canShowPreview = isPreviewable(contentType); - const canPreviewInOrd = isOrdiPreviewable(contentType); - - const showPreviewButton = canShowPreview && !canPreviewInOrd; - const showOrdButton = !canShowPreview && canPreviewInOrd; - const showMenuButton = canShowPreview && canPreviewInOrd; - - const onTogglePreview = async (e?: ReactMouseEvent) => { - setShowMenu(false); - - // prevent click from going to menu parent - if (e) e.stopPropagation(); - - if (canShowPreview) setShowPreview((current) => !current); - }; - - const onOrdClick = async (e?: ReactMouseEvent) => { - setShowMenu(false); - - // prevent click from going to menu parent - if (e) e.stopPropagation(); - - const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); - - if (canPreviewInOrd) { - const { data: previewId } = await axios.post( - `${XVERSE_ORDIVIEW_URL(network.type)}/previewHtml`, - { - html: displayContent, - contentType: inputContentType, - }, - ); - - window.open(`${XVERSE_ORDIVIEW_URL(network.type)}/previewHtml/${previewId}`, '_blank'); - } - }; - - const onMenuClick = () => { - setShowMenu((current) => !current); - }; - - const clickAction = () => { - if (showPreviewButton) onTogglePreview(); - if (showOrdButton) onOrdClick(); - if (showMenuButton) onMenuClick(); - }; - - return ( - <> - -
{inputContentType}
- {showPreviewButton && ( - - - - )} - {showOrdButton && ( - - - - )} - {showMenuButton && ( - - - - - {showMenu && ( - - {t('SHOW')} - {t('PREVIEW')} - - )} - - )} -
- - setShowPreview(false)} - /> - - ); -} - -export default ContentIcon; diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx deleted file mode 100644 index ca6847dd0..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/preview.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { X } from '@phosphor-icons/react'; -import { useLayoutEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; - -import useWalletSelector from '@hooks/useWalletSelector'; -import { XVERSE_ORDIVIEW_URL } from '@utils/constants'; -import { ContentType } from './common'; - -const MIN_FONT_SIZE = 9; - -const Container = styled.div` - width: 100vw; - height: 100vh; - position: fixed; - top: 0px; - left: 0px; - display: flex; - justify-content: center; - align-items: center; - background: rgba(18, 21, 30, 0.85); - backdrop-filter: blur(8px); - z-index: 1; -`; - -const PreviewImg = styled.img` - width: 240px; - height: 240px; - max-width: 80vw; - max-height: 80vh; -`; - -const PreviewVideo = styled.video` - width: 240px; - height: 240px; - max-width: 80vw; - max-height: 80vh; -`; - -const PreviewAudio = styled.audio` - width: 240px; - height: 240px; - max-width: 80vw; - max-height: 80vh; -`; - -const PreviewTextContainer = styled.div` - max-width: 100vw; - max-height: 100vh; - overflow-y: auto; -`; - -type PreviewTextProps = { - fontSize: number; -}; -const PreviewText = styled.pre((props) => ({ - fontSize: props.fontSize || 24, - width: '80vw', - whiteSpace: 'pre-wrap', - wordWrap: 'break-word', -})); - -const CloseTick = styled.div((props) => ({ - width: props.theme.spacing(12), - height: props.theme.spacing(12), - position: 'absolute', - top: props.theme.spacing(12), - right: props.theme.spacing(8), - cursor: 'pointer', -})); - -type Props = { - onClick: () => void; - type: 'BASE_64' | 'PLAIN_TEXT'; - content: string; - contentType: ContentType; - contentTypeRaw: string; - visible: boolean; - inscriptionId?: string; -}; - -function Preview({ - onClick, - type, - content, - contentType, - contentTypeRaw, - visible, - inscriptionId, -}: Props) { - const containerRef = useRef(null); - const textRef = useRef(null); - const [fontSize, setFontSize] = useState(24); - const { network } = useWalletSelector(); - - useLayoutEffect(() => { - // this decreases the font size until the preview text fits in the container - if (!visible || !containerRef.current || !textRef.current || fontSize < MIN_FONT_SIZE) return; - - const { height: parentHeight } = containerRef.current.getBoundingClientRect(); - const { height: preHeight } = textRef.current.getBoundingClientRect(); - - if (preHeight <= parentHeight * 0.9) return; - - setFontSize(fontSize - 0.5); - }, [visible, fontSize]); - - if (!visible) return null; - - let preview: React.ReactElement | null = null; - - if ( - contentType === ContentType.TEXT || - contentType === ContentType.JSON || - contentType === ContentType.MARKDOWN - ) { - const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); - - preview = ( - - - {displayContent} - - - ); - } else if (contentType === ContentType.HTML) { - const displayContent = type === 'PLAIN_TEXT' ? content : atob(content); - - preview = ( - - -
{displayContent}
-
-
- ); - } else if (contentType === ContentType.IMAGE) { - // workaround to show the inscription preview for an already inscribed inscription - if (inscriptionId) { - preview = ( - - ); - } else { - preview = ; - } - } else if (contentType === ContentType.VIDEO) { - preview = ( - - - - - ); - } else if (contentType === ContentType.AUDIO) { - preview = ( - - - - - ); - } - - return ( - - - - - {preview} - - ); -} - -export default Preview; diff --git a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts b/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts deleted file mode 100644 index 934461416..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/ContentLabel/utils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isValidContentType, isValidFields } from '@secretkeylabs/xverse-core'; - -type SatsDefinition = { - op: 'ns' | 'reg'; - value: string; -}; - -const getSatsDetails = (content: string, contentType: string): undefined | SatsDefinition => { - if (!isValidContentType(contentType)) { - return undefined; - } - - try { - const parsedContent = JSON.parse(content); - - if (parsedContent.p !== 'sns' || !['ns', 'reg'].includes(parsedContent.op)) { - return undefined; - } - - const parsedFields = Object.keys(parsedContent); - const parsedValues = Object.values(parsedContent); - - if (parsedValues.some((v) => typeof v !== 'string')) { - return undefined; - } - - const namespaceRequiredFields = new Set(['p', 'op', 'ns']); - const namespaceOptionalFields = new Set(['about', 'avatar']); - - const nameRequiredFields = new Set(['p', 'op', 'name']); - const nameOptionalFields = new Set(['avatar', 'rev', 'relay']); - - const isValidNs = - parsedContent.op === 'ns' && - isValidFields(parsedFields, namespaceRequiredFields, namespaceOptionalFields); - - const isValidReg = - parsedContent.op === 'reg' && - isValidFields(parsedFields, nameRequiredFields, nameOptionalFields); - - if (!isValidNs && !isValidReg) { - return undefined; - } - - return { - op: parsedContent.op, - value: parsedContent.ns || parsedContent.name, - }; - } catch (e) { - return undefined; - } -}; - -export default getSatsDetails; diff --git a/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx index 0b1f25230..6644287fd 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx @@ -8,7 +8,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import Theme from '../../../theme'; -import InscribeSection from './inscribeSection'; +import InscribeSection from '../confirmBtcTransaction/inscribeSection'; import { AddressLabel, Container, diff --git a/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/index.tsx b/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/index.tsx deleted file mode 100644 index 3a5c3586e..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; -import { ArrowDown } from '@phosphor-icons/react'; -import { StyledP } from '@ui-library/common.styled'; -import { getShortTruncatedAddress } from '@utils/helper'; -import { useTranslation } from 'react-i18next'; -import ContentLabel from '../ContentLabel'; -import { Pill, StyledPillLabel } from '../runes'; -import { - ButtonIcon, - CardContainer, - CardRow, - IconLabel, - InfoIconContainer, - YourAddress, -} from './styles'; - -type InscribeSectionProps = { - contentType: string; - content: string; - payloadType: 'BASE_64' | 'PLAIN_TEXT'; - ordinalsAddress: string; - repeat?: number; - inscriptionId?: string; -}; - -function InscribeSection({ - repeat, - content, - contentType, - ordinalsAddress, - payloadType, - inscriptionId, -}: InscribeSectionProps) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - return ( - - - - {t('INSCRIBE.TITLE')} - {repeat && {`x${repeat}`}} - - - - -
- -
-
{t('INSCRIBE.ORDINAL')}
-
- -
- - - - - - {t('INSCRIBE.TO')} - - - - {getShortTruncatedAddress(ordinalsAddress)} - - - {t('INSCRIBE.YOUR_ADDRESS')} - - - -
- ); -} - -export default InscribeSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/styles.tsx b/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/styles.tsx deleted file mode 100644 index b4475f295..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/inscribeSection/styles.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import styled from 'styled-components'; - -export const CardContainer = styled.div<{ bottomPadding?: boolean }>((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, - display: 'flex', - flexDirection: 'column', - gap: props.theme.space.m, - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: props.theme.spacing(8), - paddingBottom: props.bottomPadding ? props.theme.spacing(12) : props.theme.spacing(8), - justifyContent: 'center', - marginBottom: props.theme.spacing(6), - fontSize: 14, -})); - -type CardRowProps = { - topMargin?: boolean; - center?: boolean; -}; -export const CardRow = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: props.center ? 'center' : 'flex-start', - justifyContent: 'space-between', - marginTop: props.topMargin ? props.theme.spacing(8) : 0, -})); - -export const IconLabel = styled.div((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', -})); - -export const ButtonIcon = styled.img((props) => ({ - width: 32, - height: 32, - marginRight: props.theme.spacing(4), -})); - -export const InfoIconContainer = styled.div((props) => ({ - background: props.theme.colors.white_0, - color: props.theme.colors.elevation0, - width: 32, - height: 32, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - borderRadius: '50%', - marginRight: props.theme.spacing(5), -})); - -export const YourAddress = styled.div` - text-align: right; -`; diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/amount.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/amount.tsx deleted file mode 100644 index 87406135d..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/amount.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import FiatAmountText from '@components/fiatAmountText'; -import TokenImage from '@components/tokenImage'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent, satsToBtc } from '@secretkeylabs/xverse-core'; -import Avatar from '@ui-library/avatar'; -import { StyledP } from '@ui-library/common.styled'; -import BigNumber from 'bignumber.js'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; - -const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ - display: 'flex', - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', - columnGap: props.theme.space.xs, -})); - -const NumberTypeContainer = styled.div` - text-align: right; -`; - -const StyledFiatAmountText = styled(FiatAmountText)` - display: block; - ${(props) => props.theme.typography.body_medium_s} - color: ${(props) => props.theme.colors.white_400}; - margin-top: ${(props) => props.theme.space.xxxs}; -`; - -const StyledBtcTitle = styled(StyledP)` - margin-top: ${(props) => props.theme.space.xxxs}; -`; - -type Props = { - amount: number; -}; - -export default function Amount({ amount }: Props) { - const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { t } = useTranslation('translation'); - - return ( - -
- } /> -
- -
- - {t('CONFIRM_TRANSACTION.AMOUNT')} - - - Bitcoin - -
- - ( - - {value} - - )} - /> - - -
-
- ); -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/amountWithInscriptionSatribute.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/amountWithInscriptionSatribute.tsx deleted file mode 100644 index b7f790d4c..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/amountWithInscriptionSatribute.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { WarningOctagon } from '@phosphor-icons/react'; -import { animated, config, useSpring } from '@react-spring/web'; -import { btcTransaction } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import Theme from 'theme'; -import { mapTxSatributeInfoToBundleInfo } from '../utils'; -import Inscription from './inscription'; - -const WarningContainer = styled.div` - display: flex; - flex-direction: column; - border-radius: ${(props) => props.theme.space.s}; -`; - -const WarningButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; - padding-top: ${(props) => props.theme.space.m}; -`; - -const ItemsContainer = styled(animated.div)((props) => ({ - borderRadius: props.theme.space.s, - backgroundColor: props.theme.colors.elevation2, - padding: `${props.theme.space.s} 0`, - marginTop: props.theme.space.m, -})); - -const Range = styled.div` - padding: 0 ${(props) => props.theme.space.s}; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const Title = styled(StyledP)` - margin-left: ${(props) => props.theme.space.xxs}; -`; - -export default function AmountWithInscriptionSatribute({ - satributes, - inscriptions, - onShowInscription, -}: { - satributes: btcTransaction.IOSatribute[]; - inscriptions: btcTransaction.IOInscription[]; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; -}) { - const [showBundleDetail, setShowBundleDetail] = useState(false); - - const { t } = useTranslation('translation'); - const { hasActivatedRareSatsKey } = useWalletSelector(); - - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 400 }, - from: { opacity: 0, height: 0 }, - to: { - opacity: showBundleDetail ? 1 : 0, - height: showBundleDetail ? 'auto' : 0, - }, - }); - - const arrowRotation = useSpring({ - transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - // we only show the satributes if the user has activated the rare sats key - const satributesArray = hasActivatedRareSatsKey ? satributes : []; - const amountOfAssets = satributesArray.length + inscriptions.length; - - return amountOfAssets > 0 ? ( - - setShowBundleDetail((prevState) => !prevState)}> - - - - {t( - `CONFIRM_TRANSACTION.${ - satributesArray.length ? 'INSCRIBED_RARE_SATS' : 'INSCRIBED_SATS' - }`, - )} - - - - - - {showBundleDetail && ( - - {inscriptions.map((item: btcTransaction.IOInscription, index: number) => ( -
- - - - {(satributesArray.length || inscriptions.length > index + 1) && ( - - )} -
- ))} - {satributesArray.map((item: btcTransaction.IOSatribute, index: number) => ( -
- - - - {satributesArray.length > index + 1 && } -
- ))} -
- )} -
- ) : null; -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/bundleTxView.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/bundleTxView.tsx deleted file mode 100644 index da3b22b90..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/bundleTxView.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import Link from '@assets/img/rareSats/link.svg'; -import { CubeTransparent } from '@phosphor-icons/react'; -import { btcTransaction } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import Theme from 'theme'; -import Avatar from '../../../ui-library/avatar'; -import { SatRangeTx } from '../utils'; -import Inscription from './inscription'; -import RareSats from './rareSats'; - -type Props = { - inscriptions: btcTransaction.IOInscription[]; - satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; - bundleSize: number; - isRareSatsEnabled?: boolean; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; -}; - -const Header = styled.div((props) => ({ - display: 'flex', - flex: 1, - flexDirection: 'row', - alignItems: 'center', - marginBottom: props.theme.space.m, -})); - -const AvatarContainer = styled.div` - margin-right: ${(props) => props.theme.space.xs}; -`; - -const RowsContainer = styled.div` - padding-left: ${(props) => props.theme.space.m}; -`; -const LinkContainer = styled.div` - display: flex; - width: 32px; - justify-content: center; - margin: ${(props) => props.theme.space.xxxs} 0; -`; - -export default function BundleTxView({ - inscriptions, - satributesInfo, - bundleSize, - isRareSatsEnabled, - onShowInscription, -}: Props) { - const { t } = useTranslation('translation'); - - // we only show rare sats if there are any and the user has enabled the feature - const showRareSats = satributesInfo.totalExoticSats > 0 && isRareSatsEnabled; - - return ( - <> -
- - } /> - -
- - {t('COMMON.BUNDLE')} - - {bundleSize && ( - ( - - {value} - - )} - /> - )} -
-
- - {inscriptions.map((inscription, index) => ( -
- - {!!(inscriptions.length > index + 1 || showRareSats) && ( - - link - - )} -
- ))} - {showRareSats && } -
- - ); -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscription.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscription.tsx deleted file mode 100644 index 400575026..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscription.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small_over_card.svg'; -import { Eye } from '@phosphor-icons/react'; -import OrdinalImage from '@screens/ordinals/ordinalImage'; -import { btcTransaction } from '@secretkeylabs/xverse-core'; -import Avatar from '@ui-library/avatar'; -import { StyledP } from '@ui-library/common.styled'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import Theme from 'theme'; - -type Props = { - inscription: btcTransaction.IOInscription; - bundleSize?: number; - hideTypeSizeInfo?: boolean; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; -}; - -const RowCenter = styled.div<{ spaceBetween?: boolean; gap?: boolean }>((props) => ({ - display: 'flex', - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', - gap: props.gap ? props.theme.space.xs : 0, -})); - -const InscriptionNumberContainer = styled.button` - background-color: transparent; -`; - -const NumberTypeContainer = styled.div` - text-align: right; -`; - -const AvatarContainer = styled.div` - margin-right: ${(props) => props.theme.space.xs}; -`; - -const InscriptionNumber = styled(StyledP)` - margin-right: ${(props) => props.theme.space.xs}; -`; -const ContentType = styled(StyledP)` - word-break: break-word; -`; - -export default function Inscription({ - inscription, - bundleSize, - hideTypeSizeInfo = false, - onShowInscription, -}: Props) { - const { t } = useTranslation('translation'); - - return ( - - - - } - /> - - -
- {!hideTypeSizeInfo && ( - <> - - {t('COMMON.INSCRIPTION')} - - {bundleSize && ( - ( - - {value} - - )} - /> - )} - - )} -
- - { - onShowInscription(inscription); - }} - > - - - {inscription.number} - - - - - - {inscription.contentType} - - -
-
- ); -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscriptionSatributeRow.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscriptionSatributeRow.tsx deleted file mode 100644 index beca09ac1..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/inscriptionSatributeRow.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import useWalletSelector from '@hooks/useWalletSelector'; -import { btcTransaction } from '@secretkeylabs/xverse-core'; -import Divider from '@ui-library/divider'; -import styled from 'styled-components'; -import { getSatRangesWithInscriptions } from '../utils'; -import Amount from './amount'; -import BundleTxView from './bundleTxView'; -import Inscription from './inscription'; -import RareSats from './rareSats'; - -const RowContainer = styled.div((props) => ({ - padding: `0 ${props.theme.space.m}`, -})); - -type Props = { - inscriptions: btcTransaction.IOInscription[]; - satributes: btcTransaction.IOSatribute[]; - amount: number; - showBottomDivider?: boolean; - showTopDivider?: boolean; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; -}; - -function InscriptionSatributeRow({ - inscriptions, - satributes, - amount, - showBottomDivider, - showTopDivider, - onShowInscription, -}: Props) { - const { hasActivatedRareSatsKey } = useWalletSelector(); - - const satributesInfo = getSatRangesWithInscriptions({ - satributes, - inscriptions, - amount, - }); - - const getRow = () => { - if (inscriptions.length > 0 && inscriptions.length + satributes.length > 1) { - return ( - - ); - } - - if (inscriptions.length) { - return ( - - ); - } - - // if rare sats is disabled we show the amount of btc - if (!hasActivatedRareSatsKey) { - return ; - } - - return ; - }; - - return ( - <> - {showTopDivider && } - {getRow()} - {showBottomDivider && } - - ); -} - -export default InscriptionSatributeRow; diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/rareSats.tsx deleted file mode 100644 index ee362347c..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/rareSats.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; -import { Butterfly } from '@phosphor-icons/react'; -import { animated, config, useSpring } from '@react-spring/web'; -import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import Theme from 'theme'; -import Avatar from '../../../ui-library/avatar'; -import { SatRangeTx, mapTxSatributeInfoToBundleInfo } from '../utils'; - -const SatsBundleContainer = styled.div` - display: flex; - flex-direction: column; - border-radius: ${(props) => props.theme.space.s}; -`; - -const SatsBundleButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const RangesContainer = styled(animated.div)((props) => ({ - borderRadius: props.theme.space.s, - backgroundColor: props.theme.colors.elevation2, - padding: `${props.theme.space.s} 0`, - marginTop: props.theme.space.m, -})); - -const Range = styled.div` - padding: 0 ${(props) => props.theme.space.s}; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const BundleInfo = styled.div` - display: flex; - flex-direction: column; - align-items: flex-start; - margin-left: ${(props) => props.theme.space.s}; -`; - -function RareSats({ - satributesInfo, - bundleSize, -}: { - satributesInfo: { satRanges: SatRangeTx[]; totalExoticSats: number }; - bundleSize?: number; -}) { - const [showBundleDetail, setShowBundleDetail] = useState(false); - - const { t } = useTranslation('translation'); - - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 400 }, - from: { opacity: 0, height: 0 }, - to: { - opacity: showBundleDetail ? 1 : 0, - height: showBundleDetail ? 'auto' : 0, - }, - }); - - const arrowRotation = useSpring({ - transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - return ( - - setShowBundleDetail((prevState) => !prevState)} - > - - } /> - - {`${ - satributesInfo.totalExoticSats - } ${t('NFT_DASHBOARD_SCREEN.RARE_SATS')}`} - {bundleSize && ( - ( - - {value} - - )} - /> - )} - - - - - - {showBundleDetail && ( - - {satributesInfo.satRanges.map((item: SatRangeTx, index: number) => ( -
- - - - {satributesInfo.satRanges.length > index + 1 && } -
- ))} -
- )} -
- ); -} - -export default RareSats; diff --git a/src/app/components/confirmBtcTransactionSingleParty/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransactionSingleParty/itemRow/runeAmount.tsx deleted file mode 100644 index d55fe9ad3..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/itemRow/runeAmount.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { mapRuneNameToPlaceholder } from '@components/confirmBtcTransaction/utils'; -import TokenImage from '@components/tokenImage'; -import { RuneBase } from '@secretkeylabs/xverse-core'; -import Avatar from '@ui-library/avatar'; -import { StyledP } from '@ui-library/common.styled'; -import { ftDecimals } from '@utils/helper'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; - -const Container = styled.div({ - width: '100%', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const AvatarContainer = styled.div` - margin-right: ${(props) => props.theme.space.xs}; -`; - -const Row = styled.div({ - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - gap: '24px', -}); - -const Column = styled.div` - width: 100%; - display: flex; - flex-direction: column; - gap: 2px; - overflow: hidden; -`; - -const StyledPRight = styled(StyledP)` - word-break: break-all; - text-align: end; -`; - -type Props = { - rune: RuneBase; - hasSufficientBalance?: boolean; -}; - -export default function RuneAmount({ rune, hasSufficientBalance = true }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { runeName, amount, divisibility, symbol, inscriptionId } = rune; - const amountWithDecimals = ftDecimals(String(amount), divisibility); - return ( - - - - } - /> - - - - - {t('AMOUNT')} - - ( - - {value} - - )} - /> - - - {runeName} - - - - ); -} diff --git a/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx index bb4b14b62..926465785 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx @@ -7,8 +7,8 @@ import { StyledP } from '@ui-library/common.styled'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Theme from 'theme'; -import Amount from './itemRow/amount'; -import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import Amount from '../confirmBtcTransaction/itemRow/amount'; +import InscriptionSatributeRow from '../confirmBtcTransaction/itemRow/inscriptionSatributeRow'; import { getOutputsWithAssetsToUserAddress } from './utils'; const Title = styled.p` diff --git a/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx b/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx index 3ce7017dd..1550a9546 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx @@ -20,12 +20,12 @@ import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import AmountWithInscriptionSatribute from '../confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; +import TxInOutput from '../confirmBtcTransaction/txInOutput/txInOutput'; import DelegateSection from './delegateSection'; import EtchSection from './etchSection'; -import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; import ReceiveSection from './receiveSection'; import TransferSection from './transferSection'; -import TxInOutput from './txInOutput/txInOutput'; import { getNetAmount, isScriptOutput } from './utils'; const Container = styled.div((props) => ({ diff --git a/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx index 2482dfe88..3cddaa2f9 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx @@ -5,9 +5,9 @@ import { StyledP } from '@ui-library/common.styled'; import { getTruncatedAddress } from '@utils/helper'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import Amount from './itemRow/amount'; -import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; -import InscriptionSatributeRow from './itemRow/inscriptionSatributeRow'; +import Amount from '../confirmBtcTransaction/itemRow/amount'; +import AmountWithInscriptionSatribute from '../confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; +import InscriptionSatributeRow from '../confirmBtcTransaction/itemRow/inscriptionSatributeRow'; import { getInputsWitAssetsFromUserAddress, getOutputsWithAssetsFromUserAddress } from './utils'; const Title = styled.p` diff --git a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionInput.tsx b/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionInput.tsx deleted file mode 100644 index 438843d94..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionInput.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import InputIcon from '@assets/img/transactions/arrowUpRed.svg'; -import TransferDetailView from '@components/transferDetailView'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { getTruncatedAddress } from '@utils/helper'; -import BigNumber from 'bignumber.js'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const TransferDetailContainer = styled.div((props) => ({ - paddingBottom: props.theme.space.m, -})); - -const SubValueText = styled(StyledP)((props) => ({ - color: props.theme.colors.white_400, -})); - -const TxIdContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - gap: props.theme.space.xxs, - marginTop: props.theme.space.xxxs, -})); - -type Props = { - input: btcTransaction.EnhancedInput; -}; - -function TransactionInput({ input }: Props) { - const { btcAddress, ordinalsAddress } = useSelectedAccount(); - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - - const isPaymentsAddress = input.extendedUtxo.address === btcAddress; - const isOrdinalsAddress = input.extendedUtxo.address === ordinalsAddress; - const isExternalInput = !isPaymentsAddress && !isOrdinalsAddress; - - // TODO: show this in the UI? - // const insecureInput = - // input.sigHash === btc.SigHash.NONE || input.sigHash === btc.SigHash.NONE_ANYONECANPAY; - - const renderAddress = (addressToBeDisplayed: string) => - addressToBeDisplayed === btcAddress || addressToBeDisplayed === ordinalsAddress ? ( - - - {getTruncatedAddress(addressToBeDisplayed)} - - ({t('YOUR_ADDRESS')}) - - ) : ( - - {getTruncatedAddress(addressToBeDisplayed)} - - ); - - return ( - - - {isExternalInput ? ( - - - {getTruncatedAddress(input.extendedUtxo.utxo.txid)} - - (txid) - - ) : ( - renderAddress(input.extendedUtxo.address) - )} - - - ); -} - -export default TransactionInput; diff --git a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionOutput.tsx b/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionOutput.tsx deleted file mode 100644 index c684fb8e6..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/transactionOutput.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import ScriptIcon from '@assets/img/transactions/ScriptIcon.svg'; -import OutputIcon from '@assets/img/transactions/received.svg'; -import TransferDetailView from '@components/transferDetailView'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { btcTransaction, satsToBtc } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { getTruncatedAddress } from '@utils/helper'; -import BigNumber from 'bignumber.js'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import { isAddressOutput, isPubKeyOutput, isScriptOutput } from '../utils'; - -const TransferDetailContainer = styled.div((props) => ({ - paddingBottom: props.theme.space.m, -})); - -const SubValueText = styled(StyledP)((props) => ({ - color: props.theme.colors.white_400, -})); - -const HighlightText = styled(StyledP)((props) => ({ - color: props.theme.colors.white_0, -})); - -const TxIdContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - gap: props.theme.space.xxs, - marginTop: props.theme.space.xxxs, -})); - -type Props = { - output: btcTransaction.EnhancedOutput; - scriptOutputCount?: number; -}; - -function TransactionOutput({ output, scriptOutputCount }: Props) { - const { btcAddress, ordinalsAddress, btcPublicKey, ordinalsPublicKey } = useSelectedAccount(); - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const isOutputWithScript = isScriptOutput(output); - const isOutputWithPubKey = isPubKeyOutput(output); - - const detailViewIcon = isOutputWithScript ? ScriptIcon : OutputIcon; - const detailViewHideCopyButton = - isOutputWithScript || isOutputWithPubKey - ? true - : btcAddress === output.address || ordinalsAddress === output.address; - - const detailView = () => { - if (isOutputWithScript) { - return ( - {`${t( - 'SCRIPT_OUTPUT', - )} #${scriptOutputCount}`} - ); - } - if (isOutputWithPubKey) { - const outputType = output.type === 'pk' ? t('PUBLIC_KEY') : t('MULTISIG'); - const toOwnKey = - output.pubKeys?.includes(btcPublicKey) || output.pubKeys?.includes(ordinalsPublicKey); - const toOwnString = toOwnKey ? ` (${t('YOUR_PUBLIC_KEY')})` : ''; - return ( - - {outputType} - {toOwnString} - - ); - } - - if (output.address === btcAddress || output.address === ordinalsAddress) { - return ( - - - {getTruncatedAddress(output.address)} - - ({t('YOUR_ADDRESS')}) - - ); - } - - return ( - - {getTruncatedAddress(output.address)} - - ); - }; - - return ( - - - {detailView()} - - - ); -} - -export default TransactionOutput; diff --git a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/txInOutput.tsx b/src/app/components/confirmBtcTransactionSingleParty/txInOutput/txInOutput.tsx deleted file mode 100644 index 7a9e11e9f..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/txInOutput/txInOutput.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import { animated, config, useSpring } from '@react-spring/web'; -import { btcTransaction } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import { isScriptOutput } from '../utils'; -import TransactionInput from './transactionInput'; -import TransactionOutput from './transactionOutput'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - borderRadius: props.theme.space.s, - background: props.theme.colors.elevation1, - padding: props.theme.space.m, - marginBottom: props.theme.space.s, -})); - -const Button = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const OutputTitleText = styled(StyledP)((props) => ({ - marginBottom: props.theme.space.m, -})); - -const ExpandedContainer = styled(animated.div)((props) => ({ - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.space.m, -})); - -type Props = { - inputs: btcTransaction.EnhancedInput[]; - outputs: btcTransaction.EnhancedOutput[]; -}; - -function TxInOutput({ inputs, outputs }: Props) { - const [isExpanded, setIsExpanded] = useState(false); - - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 400 }, - from: { opacity: 0, height: 0 }, - to: { - opacity: isExpanded ? 1 : 0, - height: isExpanded ? 'auto' : 0, - }, - }); - - const arrowRotation = useSpring({ - transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - let scriptOutputCount = 1; - - return ( - - - {isExpanded && ( - - {inputs.map((input) => ( - - ))} - - {t('OUTPUT')} - - {outputs.map((output, index) => ( - - ))} - - )} - - ); -} - -export default TxInOutput; From df3f9660d6bb99ced445fb98f6bf2ce59b8b9d36 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Thu, 11 Jul 2024 19:39:00 +0800 Subject: [PATCH 020/219] remove duplicated components --- .../burnSection.tsx | 56 ---- .../delegateSection.tsx | 110 ------- .../etchSection.tsx | 236 -------------- .../index.tsx | 2 +- .../ledgerStepView.tsx | 87 ----- .../mintSection.tsx | 98 ------ .../receiveSection.tsx | 204 ------------ .../runes.tsx | 74 ----- .../transactionSummary.tsx | 8 +- .../transferSection.tsx | 5 +- .../confirmBtcTransactionSingleParty/utils.ts | 300 ------------------ 11 files changed, 9 insertions(+), 1171 deletions(-) delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/ledgerStepView.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/mintSection.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/runes.tsx delete mode 100644 src/app/components/confirmBtcTransactionSingleParty/utils.ts diff --git a/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx deleted file mode 100644 index a56f46c4c..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/burnSection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; -import { RuneSummary } from '@secretkeylabs/xverse-core'; -import InputFeedback from '@ui-library/inputFeedback'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: `${props.theme.space.m} 0`, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -const RowContainer = styled.div((props) => ({ - padding: `0 ${props.theme.space.m}`, - marginTop: `${props.theme.space.m}`, -})); - -const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', -})); - -const Header = styled(RowCenter)((props) => ({ - padding: `0 ${props.theme.space.m}`, -})); - -type Props = { - burns?: RuneSummary['burns']; -}; - -function BurnSection({ burns }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - - if (!burns?.length) return null; - - return ( - -
- -
- {burns.map((burn) => ( - - - - ))} -
- ); -} - -export default BurnSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx deleted file mode 100644 index c2b29e9f3..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/delegateSection.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; -import { WarningOctagon } from '@phosphor-icons/react'; -import { animated, config, useSpring } from '@react-spring/web'; -import { RuneSummary } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import Theme from 'theme'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - padding: `${props.theme.space.m} 0`, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -const RowContainer = styled.div((props) => ({ - padding: `0 ${props.theme.space.m}`, - marginTop: `${props.theme.space.m}`, -})); - -const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', -})); - -const Header = styled(RowCenter)((props) => ({ - padding: `0 ${props.theme.space.m}`, -})); - -const WarningButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; - padding: ${(props) => props.theme.space.m}; - padding-bottom: 0; -`; - -const DelegationDescription = styled(StyledP)` - padding: ${(props) => props.theme.space.s} ${(props) => props.theme.space.m}; - padding-bottom: ${(props) => props.theme.space.xs}; -`; - -const Title = styled(StyledP)` - margin-left: ${(props) => props.theme.space.xxs}; -`; - -type Props = { - delegations?: RuneSummary['transfers']; -}; - -function DelegateSection({ delegations }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - - const [showDelegationInfo, setShowDelegationInfo] = useState(false); - - const slideInStyles = useSpring({ - config: { ...config.gentle, duration: 200 }, - from: { opacity: 0, maxHeight: 0 }, - to: { opacity: 1, maxHeight: 100 }, - reverse: !showDelegationInfo, - }); - - const arrowRotation = useSpring({ - transform: showDelegationInfo ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - if (!delegations?.length) return null; - - return ( - -
- - {t('YOU_WILL_DELEGATE')} - -
- {delegations.map((delegation) => ( - - - - ))} - setShowDelegationInfo((prevState) => !prevState)}> - - - - {t('UNKNOWN_RUNE_RECIPIENTS')} - - - - - - - {t('RUNE_DELEGATION_DESCRIPTION')} - - -
- ); -} - -export default DelegateSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx deleted file mode 100644 index 6644287fd..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/etchSection.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import useOrdinalsApi from '@hooks/apiClients/useOrdinalsApi'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { ArrowRight } from '@phosphor-icons/react'; -import { EtchActionDetails, Inscription, RUNE_DISPLAY_DEFAULTS } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import Theme from '../../../theme'; -import InscribeSection from '../confirmBtcTransaction/inscribeSection'; -import { - AddressLabel, - Container, - Header, - RowCenter, - RuneAmount, - RuneData, - RuneImage, - RuneSymbol, - RuneValue, -} from './runes'; - -type Props = { - etch?: EtchActionDetails; -}; - -/** - * only used for ordinals service etches - */ -function EtchSection({ etch }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { ordinalsAddress } = useSelectedAccount(); - const [delegateInscriptionDetails, setDelegateInscriptionDetails] = useState( - null, - ); - const ordinalsApi = useOrdinalsApi(); - - const fetchInscriptionDetails = useCallback(async (inscriptionId: string) => { - const inscriptionDetails = await ordinalsApi.getInscription(inscriptionId); - if (inscriptionDetails) { - setDelegateInscriptionDetails(inscriptionDetails); - } - }, []); - - useEffect(() => { - if (etch?.delegateInscriptionId) { - fetchInscriptionDetails(etch.delegateInscriptionId); - } - }, [fetchInscriptionDetails, etch?.delegateInscriptionId]); - - if (!etch) return null; - - return ( - <> - -
- - {t('YOU_WILL_ISSUE')} - -
-
- - {t('NAME')} - - - {etch.runeName} - -
-
- - {t('SYMBOL')} - - - {etch.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol} - -
-
- - {t('DIVISIBILITY')} - - - {etch.divisibility || RUNE_DISPLAY_DEFAULTS.divisibility} - -
-
- - {t('MINTABLE')} - - - {etch.isMintable ? t('YES') : t('NO')} - -
-
- - {t('MINT_AMOUNT')} - - - ( - - {value} - - )} - /> - -
-
- - {t('MINTING_LIMIT')} - - ( - - {value} - - )} - /> -
- {etch.terms?.heightStart || - (etch.terms?.heightEnd && ( -
- - {t('RUNE_BLOCK_HEIGHT_TERM')} - - - {`${etch.terms.heightStart}/${etch.terms.heightEnd}`} - -
- ))} - {etch.terms?.offsetStart || - (etch.terms?.offsetEnd && ( -
- - {t('RUNE_BLOCK_OFFSET_TERM')} - - - {`${etch.terms.offsetStart ? etch.terms.offsetStart : '-'}/${etch.terms.offsetEnd}`} - -
- ))} -
- {etch.premine && ( - -
- - {t('YOU_WILL_RECEIVE')} - - - - - {' '} - {etch.destinationAddress && etch.destinationAddress !== ordinalsAddress - ? getShortTruncatedAddress(etch.destinationAddress) - : t('YOUR_ORDINAL_ADDRESS')} - - -
-
- - {etch.runeName} - -
-
- - - - {etch.symbol || '¤'} - - - - - {t('AMOUNT')} - - {etch && ( - - {t('RUNE_SIZE')}: {RUNE_DISPLAY_DEFAULTS.size} Sats - - )} - - - ( - - - {value} - - - {etch?.symbol ?? '¤'} - - - )} - /> -
-
- )} - {etch.inscriptionDetails && ( - - )} - {etch.delegateInscriptionId && delegateInscriptionDetails && ( - - )} - - ); -} - -export default EtchSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/index.tsx b/src/app/components/confirmBtcTransactionSingleParty/index.tsx index 5482d2e85..45dc78294 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/index.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/index.tsx @@ -18,7 +18,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import SendLayout from '../../layouts/sendLayout'; -import LedgerStepView, { Steps } from './ledgerStepView'; +import LedgerStepView, { Steps } from '../confirmBtcTransaction/ledgerStepView'; import TransactionSummary from './transactionSummary'; const LoaderContainer = styled.div(() => ({ diff --git a/src/app/components/confirmBtcTransactionSingleParty/ledgerStepView.tsx b/src/app/components/confirmBtcTransactionSingleParty/ledgerStepView.tsx deleted file mode 100644 index 93791ef5c..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/ledgerStepView.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import InfoIcon from '@assets/img/info.svg'; -import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default.svg'; -import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; -import LedgerConnectionView, { - ConnectLedgerContainer, - ConnectLedgerText, -} from '@components/ledger/connectLedgerView'; -import LedgerFailView from '@components/ledger/failLedgerView'; -import { - ConnectLedgerTitle, - InfoImage, -} from '@screens/ledger/confirmLedgerTransaction/index.styled'; -import { TFunction } from 'react-i18next'; - -export enum Steps { - ConnectLedger = 0, - ExternalInputs = 1, - ConfirmTransaction = 2, -} - -type Props = { - currentStep: Steps; - isConnectSuccess: boolean; - isConnectFailed: boolean; - isTxRejected: boolean; - t: TFunction<'translation', 'CONFIRM_TRANSACTION'>; - signatureRequestTranslate: TFunction<'translation', 'SIGNATURE_REQUEST'>; -}; - -function LedgerStepView({ - currentStep, - isConnectSuccess, - isConnectFailed, - isTxRejected, - t, - signatureRequestTranslate, -}: Props) { - switch (currentStep) { - case Steps.ConnectLedger: - return ( - - ); - case Steps.ExternalInputs: - if (isTxRejected || isConnectFailed) { - return ( - - ); - } - - return ( -
- - - - {t('LEDGER.INPUTS_WARNING.EXTERNAL_INPUTS')} /
- {t('LEDGER.INPUTS_WARNING.NON_DEFAULT_SIGHASH')} -
- {t('LEDGER.INPUTS_WARNING.SUBTITLE')} -
-
- ); - case Steps.ConfirmTransaction: - return ( - - ); - default: - return null; - } -} - -export default LedgerStepView; diff --git a/src/app/components/confirmBtcTransactionSingleParty/mintSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/mintSection.tsx deleted file mode 100644 index cd3c9b7aa..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/mintSection.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { ArrowRight } from '@phosphor-icons/react'; -import { MintActionDetails, RuneSummary } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { ftDecimals, getShortTruncatedAddress } from '@utils/helper'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import Theme from '../../../theme'; -import { - AddressLabel, - Container, - Header, - Pill, - RowCenter, - RuneAmount, - RuneData, - RuneImage, - RuneSymbol, - RuneValue, - StyledPillLabel, -} from './runes'; - -type Props = { - mints?: RuneSummary['mint'][] | MintActionDetails[]; -}; - -function MintSection({ mints }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const { ordinalsAddress } = useSelectedAccount(); - if (!mints) return null; - - return ( - <> - {mints.map( - (mint) => - mint && ( - -
- - {t('YOU_WILL_MINT')} - {mint.repeats && {`x${mint.repeats}`}} - - - - - {mint.destinationAddress && mint.destinationAddress !== ordinalsAddress - ? getShortTruncatedAddress(mint.destinationAddress) - : t('YOUR_ORDINAL_ADDRESS')} - - -
-
- - {mint?.runeName} - -
-
- - - - {mint?.symbol} - - - - - {t('AMOUNT')} - - {mint.runeSize && ( // This is the only place where runeSize is used - - {t('RUNE_SIZE')}: {mint.runeSize} Sats - - )} - - - ( - - - {value} - - - {mint?.symbol} - - - )} - /> -
-
- ), - )} - - ); -} - -export default MintSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx deleted file mode 100644 index 926465785..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/receiveSection.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { ArrowRight } from '@phosphor-icons/react'; -import { btcTransaction, RuneSummary } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import Theme from 'theme'; -import Amount from '../confirmBtcTransaction/itemRow/amount'; -import InscriptionSatributeRow from '../confirmBtcTransaction/itemRow/inscriptionSatributeRow'; -import { getOutputsWithAssetsToUserAddress } from './utils'; - -const Title = styled.p` - ${(props) => props.theme.typography.body_medium_m}; - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.s}; - margin-bottom: ${(props) => props.theme.space.xs}; -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -const RowCenter = styled.div<{ spaceBetween?: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: props.spaceBetween ? 'space-between' : 'initial', -})); - -const Header = styled(RowCenter)((props) => ({ - padding: `0 ${props.theme.space.m}`, -})); - -const RowContainer = styled.div<{ noPadding?: boolean; noMargin?: boolean }>((props) => ({ - padding: props.noPadding ? 0 : `0 ${props.theme.space.m}`, - marginTop: props.noMargin ? 0 : `${props.theme.space.m}`, -})); - -const AddressLabel = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -type Props = { - outputs: btcTransaction.EnhancedOutput[]; - netAmount: number; - transactionIsFinal: boolean; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; - runeReceipts?: RuneSummary['receipts']; -}; - -function ReceiveSection({ - outputs, - netAmount, - onShowInscription, - runeReceipts, - transactionIsFinal, -}: Props) { - const { btcAddress, ordinalsAddress } = useSelectedAccount(); - const { hasActivatedRareSatsKey } = useWalletSelector(); - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - - const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ - outputs, - btcAddress, - ordinalsAddress, - }); - - // if receiving runes from own addresses, hide it because it is change, unless it swap addresses (recover runes) - const filteredRuneReceipts = - runeReceipts?.filter( - (receipt) => - !receipt.sourceAddresses.some( - (address) => - (address === ordinalsAddress && receipt.destinationAddress === ordinalsAddress) || - (address === btcAddress && receipt.destinationAddress === btcAddress), - ), - ) ?? []; - const ordinalRuneReceipts = filteredRuneReceipts.filter( - (receipt) => receipt.destinationAddress === ordinalsAddress, - ); - const paymentRuneReceipts = filteredRuneReceipts.filter( - (receipt) => receipt.destinationAddress === btcAddress, - ); - - const inscriptionsRareSatsInPayment = outputsToPayment.filter( - (output) => - output.inscriptions.length > 0 || (hasActivatedRareSatsKey && output.satributes.length > 0), - ); - const areInscriptionsRareSatsInPayment = inscriptionsRareSatsInPayment.length > 0; - const areInscriptionRareSatsInOrdinal = outputsToOrdinal.length > 0; - const amountIsBiggerThanZero = netAmount > 0; - - // if transaction is not final, then runes will be delegated and will show up in the delegation section - const showOrdinalRunes = !!(transactionIsFinal && ordinalRuneReceipts.length); - const showOrdinalSection = !!(showOrdinalRunes || areInscriptionRareSatsInOrdinal); - - // if transaction is not final, then runes will be delegated and will show up in the delegation section - const showPaymentRunes = !!(transactionIsFinal && paymentRuneReceipts.length); - const showPaymentSection = !!( - amountIsBiggerThanZero || - showPaymentRunes || - areInscriptionsRareSatsInPayment - ); - - return ( - <> - {(showOrdinalSection || showPaymentSection) && {t('YOU_WILL_RECEIVE')}} - {showOrdinalSection && ( - -
- - {t('TO')} - - - - {t('YOUR_ORDINAL_ADDRESS')} - -
- {showOrdinalRunes && - ordinalRuneReceipts.map((receipt) => ( - - - - ))} - {areInscriptionRareSatsInOrdinal && ( - - {outputsToOrdinal - .sort((a, b) => b.inscriptions.length - a.inscriptions.length) - .map((output, index) => ( - index + 1} - /> - ))} - - )} -
- )} - {showPaymentSection && ( - -
- - {t('TO')} - - - - {t('YOUR_PAYMENT_ADDRESS')} - -
- {showPaymentRunes && - paymentRuneReceipts.map((receipt) => ( - - - - ))} - {amountIsBiggerThanZero && ( - - - - )} - {areInscriptionsRareSatsInPayment && ( - - {inscriptionsRareSatsInPayment - .sort((a, b) => b.inscriptions.length - a.inscriptions.length) - .map((output, index) => ( - index + 1} - /> - ))} - - )} -
- )} - - ); -} - -export default ReceiveSection; diff --git a/src/app/components/confirmBtcTransactionSingleParty/runes.tsx b/src/app/components/confirmBtcTransactionSingleParty/runes.tsx deleted file mode 100644 index 13fbcb731..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/runes.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { StyledP } from '@ui-library/common.styled'; -import styled from 'styled-components'; - -export const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - background: props.theme.colors.elevation1, - borderRadius: 12, - paddingTop: props.theme.space.m, - justifyContent: 'center', - marginBottom: props.theme.space.s, -})); - -export const RowCenter = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', -}); - -export const AddressLabel = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -export const Header = styled(RowCenter)((props) => ({ - marginBottom: props.theme.space.m, - padding: `0 ${props.theme.space.m}`, -})); - -export const StyledPillLabel = styled.p` - display: flex; - align-items: center; - gap: ${(props) => props.theme.space.s}; -`; - -export const Pill = styled.span` - ${(props) => props.theme.typography.body_bold_s} - color: ${(props) => props.theme.colors.elevation0}; - background-color: ${(props) => props.theme.colors.white_0}; - padding: 3px 6px; - border-radius: 40px; -`; - -export const RuneValue = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -export const RuneSymbol = styled(StyledP)((props) => ({ - marginLeft: props.theme.space.xxs, -})); - -export const RuneData = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -export const RuneImage = styled.div((props) => ({ - height: 32, - width: 32, - borderRadius: '50%', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: props.theme.colors.white_850, -})); - -export const RuneAmount = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginLeft: props.theme.space.xs, -})); diff --git a/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx b/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx index 1550a9546..ace25b479 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/transactionSummary.tsx @@ -20,13 +20,13 @@ import BigNumber from 'bignumber.js'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; +import DelegateSection from '../confirmBtcTransaction/delegateSection'; +import EtchSection from '../confirmBtcTransaction/etchSection'; import AmountWithInscriptionSatribute from '../confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; +import ReceiveSection from '../confirmBtcTransaction/receiveSection'; import TxInOutput from '../confirmBtcTransaction/txInOutput/txInOutput'; -import DelegateSection from './delegateSection'; -import EtchSection from './etchSection'; -import ReceiveSection from './receiveSection'; +import { getNetAmount, isScriptOutput } from '../confirmBtcTransaction/utils'; import TransferSection from './transferSection'; -import { getNetAmount, isScriptOutput } from './utils'; const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, diff --git a/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx b/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx index 3cddaa2f9..c98a56fb7 100644 --- a/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx +++ b/src/app/components/confirmBtcTransactionSingleParty/transferSection.tsx @@ -8,7 +8,10 @@ import styled from 'styled-components'; import Amount from '../confirmBtcTransaction/itemRow/amount'; import AmountWithInscriptionSatribute from '../confirmBtcTransaction/itemRow/amountWithInscriptionSatribute'; import InscriptionSatributeRow from '../confirmBtcTransaction/itemRow/inscriptionSatributeRow'; -import { getInputsWitAssetsFromUserAddress, getOutputsWithAssetsFromUserAddress } from './utils'; +import { + getInputsWitAssetsFromUserAddress, + getOutputsWithAssetsFromUserAddress, +} from '../confirmBtcTransaction/utils'; const Title = styled.p` ${(props) => props.theme.typography.body_medium_m}; diff --git a/src/app/components/confirmBtcTransactionSingleParty/utils.ts b/src/app/components/confirmBtcTransactionSingleParty/utils.ts deleted file mode 100644 index 1e8b89ca3..000000000 --- a/src/app/components/confirmBtcTransactionSingleParty/utils.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { btcTransaction, BundleSatRange, FungibleToken } from '@secretkeylabs/xverse-core'; - -export type SatRangeTx = { - totalSats: number; - offset: number; - fromAddress: string; - inscriptions: (Omit & { - content_type: string; - inscription_number: number; - })[]; - satributes: btcTransaction.IOSatribute['types']; -}; - -const DUMMY_OFFSET = -1; - -export const isScriptOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionScriptOutput => - (output as btcTransaction.TransactionScriptOutput).script !== undefined; - -export const isPubKeyOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionPubKeyOutput => - !!(output as btcTransaction.TransactionPubKeyOutput).pubKeys?.length; - -export const isAddressOutput = ( - output: btcTransaction.EnhancedOutput, -): output is btcTransaction.TransactionOutput => - (output as btcTransaction.TransactionOutput).address !== undefined; - -type CommonInputOutputUtilProps = { - inputs: btcTransaction.EnhancedInput[]; - outputs: btcTransaction.EnhancedOutput[]; - btcAddress: string; - ordinalsAddress: string; -}; - -export const getNetAmount = ({ - inputs, - outputs, - btcAddress, - ordinalsAddress, -}: CommonInputOutputUtilProps) => { - const initialValue = 0; - - const totalUserSpend = inputs.reduce((accumulator: number, input) => { - const isFromUserAddress = [btcAddress, ordinalsAddress].includes(input.extendedUtxo.address); - if (isFromUserAddress) { - return accumulator + input.extendedUtxo.utxo.value; - } - return accumulator; - }, initialValue); - - const totalUserReceive = outputs.reduce((accumulator: number, output) => { - const isToUserAddress = - isAddressOutput(output) && [btcAddress, ordinalsAddress].includes(output.address); - if (isToUserAddress) { - return accumulator + output.amount; - } - return accumulator; - }, initialValue); - - return totalUserReceive - totalUserSpend; -}; - -export const getOutputsWithAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit) => { - // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes - const outputsFromPayment: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - const outputsFromOrdinal: ( - | btcTransaction.TransactionOutput - | btcTransaction.TransactionPubKeyOutput - )[] = []; - outputs.forEach((output) => { - if (isScriptOutput(output)) { - return; - } - - const itemsFromPayment: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - const itemsFromOrdinal: (btcTransaction.IOInscription | btcTransaction.IOSatribute)[] = []; - [...output.inscriptions, ...output.satributes].forEach((item) => { - if (item.fromAddress === btcAddress) { - return itemsFromPayment.push(item); - } - if (item.fromAddress === ordinalsAddress) { - itemsFromOrdinal.push(item); - } - }); - - if (itemsFromOrdinal.length > 0) { - outputsFromOrdinal.push(output); - } - if (itemsFromPayment.length > 0) { - outputsFromPayment.push(output); - } - }); - - return { outputsFromPayment, outputsFromOrdinal }; -}; - -export const getInputsWitAssetsFromUserAddress = ({ - btcAddress, - ordinalsAddress, - inputs, -}: Omit) => { - // we want to discard inputs that are not from user address and do not have inscriptions or satributes - const inputFromPayment: btcTransaction.EnhancedInput[] = []; - const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; - inputs.forEach((input) => { - if (!input.inscriptions.length && !input.satributes.length) { - return; - } - - if (input.extendedUtxo.address === btcAddress) { - return inputFromPayment.push(input); - } - if (input.extendedUtxo.address === ordinalsAddress) { - inputFromOrdinal.push(input); - } - }); - - return { inputFromPayment, inputFromOrdinal }; -}; - -export const getOutputsWithAssetsToUserAddress = ({ - btcAddress, - ordinalsAddress, - outputs, -}: Omit) => { - const outputsToPayment: btcTransaction.TransactionOutput[] = []; - const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; - outputs.forEach((output) => { - // we want to discard outputs that are not spendable or are not to user address - if ( - isScriptOutput(output) || - isPubKeyOutput(output) || - ![btcAddress, ordinalsAddress].includes(output.address) - ) { - return; - } - - if (output.address === btcAddress) { - return outputsToPayment.push(output); - } - - // we don't want to show amount to ordinals address, because it's not spendable - if ( - output.address === ordinalsAddress && - (output.inscriptions.length > 0 || output.satributes.length > 0) - ) { - outputsToOrdinal.push(output); - } - }); - - return { outputsToPayment, outputsToOrdinal }; -}; - -export const mapTxSatributeInfoToBundleInfo = (item: btcTransaction.IOSatribute | SatRangeTx) => { - const commonProps = { - offset: item.offset, - block: 0, - range: { - start: '0', - end: '0', - }, - yearMined: 0, - }; - - // SatRangeTx - if ('totalSats' in item) { - return { - ...commonProps, - totalSats: item.totalSats, - inscriptions: item.inscriptions, - satributes: item.satributes, - } as BundleSatRange; - } - - // btcTransaction.IOSatribute - return { - ...commonProps, - totalSats: item.amount, - inscriptions: [], - satributes: item.types, - } as BundleSatRange; -}; - -export const getSatRangesWithInscriptions = ({ - satributes, - inscriptions, - amount, -}: { - inscriptions: btcTransaction.IOInscription[]; - satributes: btcTransaction.IOSatribute[]; - amount: number; -}) => { - const satRanges: { - [offset: number]: SatRangeTx; - } = {}; - - satributes.forEach((satribute) => { - const { types, amount: totalSats, ...rest } = satribute; - satRanges[rest.offset] = { ...rest, satributes: types, totalSats, inscriptions: [] }; - }); - - inscriptions.forEach((inscription) => { - const { contentType, number, ...inscriptionRest } = inscription; - const mappedInscription = { - ...inscriptionRest, - content_type: contentType, - inscription_number: number, - }; - if (satRanges[inscription.offset]) { - satRanges[inscription.offset] = { - ...satRanges[inscription.offset], - inscriptions: [...satRanges[inscription.offset].inscriptions, mappedInscription], - }; - return; - } - - satRanges[inscription.offset] = { - totalSats: 1, - offset: inscription.offset, - fromAddress: inscription.fromAddress, - inscriptions: [mappedInscription], - satributes: ['COMMON'], - }; - }); - - const { amountOfExoticsOrInscribedSats, totalExoticSats } = Object.values(satRanges).reduce( - (acc, range) => ({ - amountOfExoticsOrInscribedSats: acc.amountOfExoticsOrInscribedSats + range.totalSats, - totalExoticSats: - acc.totalExoticSats + (!range.satributes.includes('COMMON') ? range.totalSats : 0), - }), - { - amountOfExoticsOrInscribedSats: 0, - totalExoticSats: 0, - }, - ); - - if (amountOfExoticsOrInscribedSats < amount) { - satRanges[DUMMY_OFFSET] = { - totalSats: amount - amountOfExoticsOrInscribedSats, - offset: DUMMY_OFFSET, - fromAddress: '', - inscriptions: [], - satributes: ['COMMON'], - }; - } - - // sort should be: inscribed rare, rare, inscribed common, common - const satRangesArray = Object.values(satRanges).sort((a, b) => { - // Check conditions for each category - const aHasInscriptions = a.inscriptions.length > 0; - const bHasInscriptions = b.inscriptions.length > 0; - const aHasRareSatributes = a.satributes.some((s) => s !== 'COMMON'); - const bHasRareSatributes = b.satributes.some((s) => s !== 'COMMON'); - - // sats not rare and not inscribed at bottom - if (!aHasInscriptions && !aHasRareSatributes) return 1; - - // sats inscribed and rare at top - if (aHasInscriptions && aHasRareSatributes) return -1; - - // sats not inscribed and rare below inscribed and rare - if (bHasInscriptions && bHasRareSatributes) return 1; - - // sats inscribed and not rare above sats not inscribed and not rare - if (aHasRareSatributes) return -1; - if (bHasRareSatributes) return 1; - - // equal ranges - return 0; - }); - - return { satRanges: satRangesArray, totalExoticSats }; -}; - -export const mapRuneNameToPlaceholder = ( - runeName: string, - symbol: string, - inscriptionId: string, -): FungibleToken => ({ - protocol: 'runes', - name: runeName, - assetName: '', - balance: '', - principal: '', - total_received: '', - total_sent: '', - runeSymbol: symbol, - runeInscriptionId: inscriptionId, -}); From e2da920f946e78b2851831bd763096d960920e43 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 12 Jul 2024 03:05:24 +0300 Subject: [PATCH 021/219] minor fixes --- package-lock.json | 28 +++++------ package.json | 4 +- src/app/screens/signMessageRequest/index.tsx | 48 +++++++++++-------- .../useSignMessageRequest.ts | 27 +++++++---- .../screens/signMessageRequestInApp/index.tsx | 4 +- src/app/utils/ledger.ts | 5 +- .../utils/rpc/responseMessages/bitcoin.ts | 11 +++++ 7 files changed, 78 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 214ab6445..b928619e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.0.15-be0d447", + "@sats-connect/core": "0.1.0-0984c63", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "17.1.1-5d1ad19", + "@secretkeylabs/xverse-core": "17.1.1-06fb55e", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -1324,9 +1324,9 @@ ] }, "node_modules/@sats-connect/core": { - "version": "0.0.15-be0d447", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15-be0d447.tgz", - "integrity": "sha512-Eks6/BP0R1tPSnUn+nfEWGFvTod27tNWVZsF5Uk9DbIiFmCt0/Vc3VvTA25RMXsNtmsp5+TwLW1AzdXXMb3j3g==", + "version": "0.1.0-0984c63", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.0-0984c63.tgz", + "integrity": "sha512-Qw4WgFRDXCwD5juCb0MHXemMow1QqzKta8ThP1aK5vm5CNawc3tV76sZGlzV/6GX399Lcea+5nHkAyeRq3LBvA==", "dependencies": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", @@ -1432,9 +1432,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "17.1.1-5d1ad19", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-5d1ad19/6aa125ec9eeaf38ebeca5ee11a998a7b821267aa", - "integrity": "sha512-n2A+DkgbQpCMGD7Pbx3x1SksSoGbuItNZNaGJAtHhZFdh8kErWCFZvROwBEC6UyiCbGc7yvMo5aN6RIzdv6VtA==", + "version": "17.1.1-06fb55e", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-06fb55e/abf3b2c01388a864c8f3ac1f92547de0dea6f9ea", + "integrity": "sha512-/d/aHXhHGrpaRa0bL0O+td7tIhN4fMTZc7ptwZkjLK+hOYTuAs9bvYr1wSggJlopajmS4C11ZGepbg4izYming==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -17839,9 +17839,9 @@ "optional": true }, "@sats-connect/core": { - "version": "0.0.15-be0d447", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.0.15-be0d447.tgz", - "integrity": "sha512-Eks6/BP0R1tPSnUn+nfEWGFvTod27tNWVZsF5Uk9DbIiFmCt0/Vc3VvTA25RMXsNtmsp5+TwLW1AzdXXMb3j3g==", + "version": "0.1.0-0984c63", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.0-0984c63.tgz", + "integrity": "sha512-Qw4WgFRDXCwD5juCb0MHXemMow1QqzKta8ThP1aK5vm5CNawc3tV76sZGlzV/6GX399Lcea+5nHkAyeRq3LBvA==", "requires": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", @@ -17920,9 +17920,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "17.1.1-5d1ad19", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-5d1ad19/6aa125ec9eeaf38ebeca5ee11a998a7b821267aa", - "integrity": "sha512-n2A+DkgbQpCMGD7Pbx3x1SksSoGbuItNZNaGJAtHhZFdh8kErWCFZvROwBEC6UyiCbGc7yvMo5aN6RIzdv6VtA==", + "version": "17.1.1-06fb55e", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/17.1.1-06fb55e/abf3b2c01388a864c8f3ac1f92547de0dea6f9ea", + "integrity": "sha512-/d/aHXhHGrpaRa0bL0O+td7tIhN4fMTZc7ptwZkjLK+hOYTuAs9bvYr1wSggJlopajmS4C11ZGepbg4izYming==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", diff --git a/package.json b/package.json index 0549eded5..9c0072f32 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,9 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "^1.43.1", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "0.0.15-be0d447", + "@sats-connect/core": "0.1.0-0984c63", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "17.1.1-5d1ad19", + "@secretkeylabs/xverse-core": "17.1.1-06fb55e", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", diff --git a/src/app/screens/signMessageRequest/index.tsx b/src/app/screens/signMessageRequest/index.tsx index 82c861a78..f3002e98c 100644 --- a/src/app/screens/signMessageRequest/index.tsx +++ b/src/app/screens/signMessageRequest/index.tsx @@ -2,7 +2,8 @@ import ledgerConnectDefaultIcon from '@assets/img/ledger/ledger_connect_default. import ledgerConnectBtcIcon from '@assets/img/ledger/ledger_import_connect_btc.svg'; import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types'; import { delay } from '@common/utils/ledger'; -import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; +import { sendSignMessageSuccessResponseMessage } from '@common/utils/rpc/responseMessages/bitcoin'; +import { sendUserRejectionMessage } from '@common/utils/rpc/responseMessages/errors'; import AccountHeaderComponent from '@components/accountHeader'; import BottomModal from '@components/bottomModal'; import ConfirmScreen from '@components/confirmScreen'; @@ -119,13 +120,13 @@ function SignMessageRequest() { setCurrentStepIndex(1); try { - const signature = await handleLedgerMessageSigning({ + const signedMessage = await handleLedgerMessageSigning({ transport, addressIndex: selectedAccount.deviceAccountIndex, address: payload.address, networkType: network.type, message: payload.message, - protocol: (payload as any).protocol as MessageSigningProtocols, + protocol: payload.protocol, }); if (requestToken) { const signingMessage = { @@ -133,26 +134,30 @@ function SignMessageRequest() { method: SatsConnectMethods.signMessageResponse, payload: { signMessageRequest: requestToken, - signMessageResponse: signature, + signMessageResponse: signedMessage.signature, }, }; chrome.tabs.sendMessage(+tabId, signingMessage); window.close(); } - const response = makeRpcSuccessResponse(requestId, { + const signMessageResult: Return<'signMessage'> = { address: payload.address, messageHash: - (payload as any).protocol === MessageSigningProtocols.BIP322 + payload.protocol === MessageSigningProtocols.BIP322 ? bip0322Hash(payload.message) : legacyHash(payload.message).toString('base64'), - signature, - protocol: (payload as any).protocol, + signature: signedMessage.signature, + protocol: signedMessage.protocol as MessageSigningProtocols, + }; + + sendSignMessageSuccessResponseMessage({ + tabId: +tabId, + messageId: requestId, + result: signMessageResult, }); - sendRpcResponse(+tabId, response); window.close(); } catch (e: any) { - console.error(e); - + setValidationError({ error: e.message }); if (e.name === 'LockedDeviceError') { setCurrentStepIndex(0); setIsConnectSuccess(false); @@ -175,11 +180,10 @@ function SignMessageRequest() { if (requestToken) { finalizeMessageSignature({ requestPayload: requestToken, tabId: +tabId, data: 'cancel' }); } else { - const cancelError = makeRPCError(requestId, { - code: RpcErrorCode.USER_REJECTION, - message: 'User rejected the request.', + sendUserRejectionMessage({ + tabId: +tabId, + messageId: requestId, }); - sendRpcResponse(+tabId, cancelError); } window.close(); }; @@ -207,19 +211,21 @@ function SignMessageRequest() { const signMessageResult: Return<'signMessage'> = { address: payload.address, messageHash: - (payload as any).protocol === MessageSigningProtocols.BIP322 + payload.protocol === MessageSigningProtocols.BIP322 ? bip0322Hash(payload.message) : legacyHash(payload.message).toString('base64'), signature: signedMessage.signature, - protocol: (payload as any).protocol, + protocol: signedMessage.protocol, }; - const response = makeRpcSuccessResponse(requestId, signMessageResult); - sendRpcResponse(+tabId, response); + sendSignMessageSuccessResponseMessage({ + tabId: +tabId, + messageId: requestId, + result: signMessageResult, + }); } window.close(); } catch (err) { setValidationError({ error: (err as any).message }); - console.log(err); } finally { setIsSigning(false); } @@ -234,7 +240,7 @@ function SignMessageRequest() { }; if (validationError) { - return ; + return ; } return ( diff --git a/src/app/screens/signMessageRequest/useSignMessageRequest.ts b/src/app/screens/signMessageRequest/useSignMessageRequest.ts index 18056ba6b..ff65a7d11 100644 --- a/src/app/screens/signMessageRequest/useSignMessageRequest.ts +++ b/src/app/screens/signMessageRequest/useSignMessageRequest.ts @@ -2,8 +2,13 @@ import useSeedVault from '@hooks/useSeedVault'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; -import { Params, SignMessageOptions, SignMessagePayload } from '@sats-connect/core'; -import { MessageSigningProtocols, signMessage } from '@secretkeylabs/xverse-core'; +import { + BitcoinNetworkType, + Params, + SignMessageOptions, + SignMessagePayload, +} from '@sats-connect/core'; +import { signMessage } from '@secretkeylabs/xverse-core'; import { isHardwareAccount } from '@utils/helper'; import { decodeToken } from 'jsontokens'; import { useEffect, useMemo, useState } from 'react'; @@ -13,6 +18,7 @@ import SuperJSON from 'superjson'; const useSignMessageRequestParams = () => { const { search } = useLocation(); + const { network } = useWalletSelector(); const params = useMemo(() => new URLSearchParams(search), [search]); const tabId = params.get('tabId') ?? '0'; const requestId = params.get('requestId') ?? ''; @@ -27,13 +33,18 @@ const useSignMessageRequestParams = () => { requestToken: token, }; } - const rpcPayload = SuperJSON.parse>(payloadToken); + const rpcPayload: SignMessagePayload = { + ...SuperJSON.parse>(payloadToken), + network: { + type: network.type as BitcoinNetworkType, + }, + }; return { payload: rpcPayload, requestToken: null, }; - }, [params, payloadToken]); + }, [params, payloadToken, network.type]); return { tabId, payload, requestToken, requestId }; }; @@ -43,9 +54,7 @@ type ValidationError = { errorTitle?: string; }; -export const useSignMessageValidation = ( - requestPayload: SignMessagePayload | Params<'signMessage'> | undefined, -) => { +export const useSignMessageValidation = (requestPayload: SignMessagePayload | undefined) => { const [validationError, setValidationError] = useState(null); const { t } = useTranslation('translation', { keyPrefix: 'REQUEST_ERRORS' }); const selectedAccount = useSelectedAccount(); @@ -67,7 +76,7 @@ export const useSignMessageValidation = ( const validateSignMessage = () => { if (!requestPayload) return; - if ((requestPayload as any).network && (requestPayload as any).network.type !== network.type) { + if (requestPayload.network.type !== network.type) { setValidationError({ error: t('NETWORK_MISMATCH'), }); @@ -107,7 +116,7 @@ export const useSignMessageRequest = () => { accounts: accountsList, message, address, - protocol: (payload as any).protocol, + protocol: payload.protocol, seedPhrase, network: network.type, }); diff --git a/src/app/screens/signMessageRequestInApp/index.tsx b/src/app/screens/signMessageRequestInApp/index.tsx index 06d2842b5..c2c1dd276 100644 --- a/src/app/screens/signMessageRequestInApp/index.tsx +++ b/src/app/screens/signMessageRequestInApp/index.tsx @@ -132,7 +132,7 @@ function SignMessageRequestInApp() { setCurrentStepIndex(1); try { - const bip322signature = await handleLedgerMessageSigning({ + const signedMessage = await handleLedgerMessageSigning({ transport, addressIndex: selectedAccount.deviceAccountIndex, address: payload.address, @@ -146,7 +146,7 @@ function SignMessageRequestInApp() { makerPublicKey: selectedAccount?.ordinalsPublicKey!, makerAddress: selectedAccount?.ordinalsAddress!, token: payload.token, - signature: bip322signature, + signature: signedMessage.signature, }); handleGoBack(); diff --git a/src/app/utils/ledger.ts b/src/app/utils/ledger.ts index c0d9febf8..913cbd909 100644 --- a/src/app/utils/ledger.ts +++ b/src/app/utils/ledger.ts @@ -33,7 +33,10 @@ export const handleLedgerMessageSigning = async ({ protocol, }); - return signature; + return { + signature, + protocol, + }; }; export const signatureVrsToRsv = (sig: string): string => sig.slice(2) + sig.slice(0, 2); diff --git a/src/common/utils/rpc/responseMessages/bitcoin.ts b/src/common/utils/rpc/responseMessages/bitcoin.ts index 233204309..3157d4ec3 100644 --- a/src/common/utils/rpc/responseMessages/bitcoin.ts +++ b/src/common/utils/rpc/responseMessages/bitcoin.ts @@ -24,3 +24,14 @@ export function sendGetAddressesSuccessResponseMessage({ }: GetAddressesSuccess) { sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result)); } + +type SignMessageSuccess = BaseArgs & { + result: Return<'signMessage'>; +}; +export function sendSignMessageSuccessResponseMessage({ + tabId, + messageId, + result, +}: SignMessageSuccess) { + sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result)); +} From 0e60d5fe9671a757ffd16c390ee110c3a2cfef04 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Fri, 12 Jul 2024 11:27:50 +0800 Subject: [PATCH 022/219] Runes listing enhancement (#394) * handle scenario where a rune has no ME floor price * fixes * add snackbar * Improve the custom rune price input * fix side effect --------- Co-authored-by: Denys Hriaznov --- src/app/App.tsx | 2 +- .../queries/runes/useRuneFloorPriceQuery.ts | 11 +- .../hooks/queries/runes/useRuneSellPsbt.ts | 2 +- .../screens/listRune/floorComparisonLabel.tsx | 3 +- src/app/screens/listRune/index.tsx | 32 +++-- .../screens/listRune/setCustomPriceModal.tsx | 136 ++++++++---------- src/locales/en.json | 5 +- 7 files changed, 99 insertions(+), 92 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index a4b9336c3..365944a72 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -45,7 +45,7 @@ function App(): React.ReactNode { position="bottom-center" containerStyle={{ bottom: 32 }} toastOptions={{ - duration: 1500, + duration: 2000, success: { icon: ( diff --git a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts index 0423f1941..460a9d3a2 100644 --- a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts +++ b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts @@ -9,10 +9,13 @@ export default function useRuneFloorPriceQuery(runeName: string, backgroundRefet throw new Error('Only available on Mainnet'); } const runesApi = useRunesApi(); - const queryFn = useCallback(async () => { - const res = await runesApi.getRuneMarketData(runeName); - return Number(res.floorUnitPrice.formatted); - }, [runeName, runesApi]); + const queryFn = useCallback( + async () => + runesApi + .getRuneMarketData(runeName) + .then((res) => Number(res.floorUnitPrice?.formatted ?? 0)), + [runeName, runesApi], + ); return useQuery({ refetchOnWindowFocus: backgroundRefetch, refetchOnReconnect: backgroundRefetch, diff --git a/src/app/hooks/queries/runes/useRuneSellPsbt.ts b/src/app/hooks/queries/runes/useRuneSellPsbt.ts index 1ab6a95e8..0b9133cdd 100644 --- a/src/app/hooks/queries/runes/useRuneSellPsbt.ts +++ b/src/app/hooks/queries/runes/useRuneSellPsbt.ts @@ -58,7 +58,7 @@ const useRuneSellPsbt = (runeName: string, listingUtxos: Record {lowError && ( @@ -46,7 +47,7 @@ function FloorComparisonLabel({
)} - {!lowError && !highError && ( + {!lowError && !highError && !noFloorPrice && ( <> {priceSats === floorPriceSats && ( diff --git a/src/app/screens/listRune/index.tsx b/src/app/screens/listRune/index.tsx index c957e5229..a22cf4cce 100644 --- a/src/app/screens/listRune/index.tsx +++ b/src/app/screens/listRune/index.tsx @@ -26,6 +26,7 @@ import { formatToXDecimalPlaces, ftDecimals } from '@utils/helper'; import { getFullTxId, getTxIdFromFullTxId, getVoutFromFullTxId } from '@utils/runes'; import BigNumber from 'bignumber.js'; import { useEffect, useReducer } from 'react'; +import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -86,6 +87,8 @@ export default function ListRuneScreen() { isRefetching: floorPriceRefetching, } = useRuneFloorPriceQuery(selectedRune?.name ?? '', false); + const noFloorPrice = runeFloorPrice === 0; + const isLoading = listItemsLoading || listItemsRefetching || floorPriceLoading || floorPriceRefetching; @@ -107,6 +110,7 @@ export default function ListRuneScreen() { getRuneSellPsbt, signPsbtPayload, loading: psbtLoading, + error: psbtError, } = useRuneSellPsbt(selectedRune?.name ?? '', listItemsMap); const selectedListItems = Object.values(listItemsMap).filter((item) => item.selected); @@ -207,14 +211,14 @@ export default function ListRuneScreen() { type: 'RESTORE_STATE_FROM_PSBT', payload: location.state, }); - } else if (listItemsResponse && runeFloorPrice) { + } else if (listItemsResponse && runeFloorPrice !== undefined && selectedRune) { dispatch({ type: 'INITIATE_LIST_ITEMS', payload: listItemsResponse.reduce( (map, item) => ({ ...map, [getFullTxId(item)]: { - selected: false, + selected: listItemsMap[getFullTxId(item)]?.selected ?? false, useIndividualCustomPrice: false, satAmount: BigNumber(item.value).toNumber(), amount: Number( @@ -241,6 +245,12 @@ export default function ListRuneScreen() { } }, [listRunesState, signPsbtPayload, navigate, selectedRune, runeId]); + useEffect(() => { + if (!psbtLoading && psbtError) { + toast.error(psbtError); + } + }, [psbtError, psbtLoading]); + if (isLoading) { return ( dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 1 })} variant={ - runePriceOption === 1 && !individualCustomPriceUsed + runePriceOption === 1 && !noFloorPrice && !individualCustomPriceUsed ? 'primary' : 'secondary' } /> dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 1.05 })} variant={ runePriceOption === 1.05 && !individualCustomPriceUsed @@ -379,6 +391,7 @@ export default function ListRuneScreen() { /> dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 1.1 })} variant={ runePriceOption === 1.1 && !individualCustomPriceUsed @@ -388,6 +401,7 @@ export default function ListRuneScreen() { /> dispatch({ type: 'SET_RUNE_PRICE_OPTION', payload: 1.2 })} variant={ runePriceOption === 1.2 && !individualCustomPriceUsed @@ -408,10 +422,12 @@ export default function ListRuneScreen() { /> - {`${t('MAGIC_EDEN_FLOOR_PRICE', { - sats: formatToXDecimalPlaces(runeFloorPrice, 5), - symbol: selectedRune?.runeSymbol, - })}`} + {noFloorPrice + ? t('NO_FLOOR_PRICE', { symbol: selectedRune?.runeSymbol }) + : t('MAGIC_EDEN_FLOOR_PRICE', { + sats: formatToXDecimalPlaces(runeFloorPrice, 5), + symbol: selectedRune?.runeSymbol, + })} diff --git a/src/app/screens/listRune/setCustomPriceModal.tsx b/src/app/screens/listRune/setCustomPriceModal.tsx index d4ee5a73d..09e1853ee 100644 --- a/src/app/screens/listRune/setCustomPriceModal.tsx +++ b/src/app/screens/listRune/setCustomPriceModal.tsx @@ -1,55 +1,36 @@ import FloorComparisonLabel from '@screens/listRune/floorComparisonLabel'; import Button from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; +import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; import { formatToXDecimalPlaces } from '@utils/helper'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -const ButtonContainer = styled.div((props) => ({ - marginTop: props.theme.space.l, - button: { - border: '0px !important', - }, -})); - -const Description = styled.div((props) => ({ +const Description = styled.p((props) => ({ ...props.theme.typography.body_medium_m, color: props.theme.colors.white_200, marginBottom: props.theme.space.l, })); -const AmountInputContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', +const InputContainer = styled.div((props) => ({ marginTop: props.theme.space.xs, - marginBottom: props.theme.space.xs, - border: `1px solid ${props.theme.colors.white_800}`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: 8, - paddingLeft: props.theme.space.xxs, - paddingRight: props.theme.space.xxs, - height: 44, -})); - -const InputFieldContainer = styled.div(() => ({ - flex: 1, -})); - -const InputField = styled.input((props) => ({ - ...props.theme.typography.body_m, - backgroundColor: props.theme.colors.elevation1, - color: props.theme.colors.white_200, - width: '100%', - border: 'transparent', - '&::-webkit-inner-spin-button': { + '& input': { '-webkit-appearance': 'none', - margin: 0, - }, - '&[type=number]': { '-moz-appearance': 'textfield', + backgroundColor: props.theme.colors.elevation1, + '&::-webkit-inner-spin-button': { + '-webkit-appearance': 'none', + margin: 0, + }, + }, +})); + +const ButtonContainer = styled.div((props) => ({ + margin: `${props.theme.space.l} 0 ${props.theme.space.xl}`, + button: { + border: '0px !important', }, })); @@ -90,49 +71,54 @@ function SetCustomPriceModal({ onClose(); }} > - - {t('MIN_PRICE_LABEL', { - symbol: runeSymbol, - price: formatToXDecimalPlaces(minPriceSats, 10), - })} - - - {t('LISTING_PRICE')} - - - - + + {t('MIN_PRICE_LABEL', { + symbol: runeSymbol, + price: formatToXDecimalPlaces(minPriceSats, 10), + })} + + + {t('LISTING_PRICE')} + + + setPriceSats(event.target.value)} + placeholder="0" + type="number" + complications={ + + sats/{runeSymbol} + + } + hideClear + autoFocus + /> + + {priceSats && ( + + )} + + - - diff --git a/src/app/components/confirmScreen/index.tsx b/src/app/components/confirmScreen/index.tsx index 8a052b940..06801b03c 100644 --- a/src/app/components/confirmScreen/index.tsx +++ b/src/app/components/confirmScreen/index.tsx @@ -1,4 +1,4 @@ -import ActionButton from '@components/button'; +import Button from '@ui-library/button'; import type { ReactNode } from 'react'; import styled from 'styled-components'; @@ -54,14 +54,14 @@ function ConfirmScreen({ <> {children} - + - ({ display: 'flex', flexDirection: 'column', + justifyContent: 'center', background: props.theme.colors.elevation1, borderRadius: 12, - padding: '16px 16px', - justifyContent: 'center', - marginBottom: 12, + padding: props.theme.space.m, + paddingBottom: 20, + marginBottom: props.theme.space.s, })); const RecipientTitleText = styled.p((props) => ({ @@ -33,16 +34,16 @@ const RecipientTitleText = styled.p((props) => ({ marginBottom: props.theme.space.xs, })); -const RowContainer = styled.div({ +const RowContainer = styled.div((props) => ({ display: 'flex', flexDirection: 'row', width: '100%', alignItems: 'flex-start', - marginBottom: 12, -}); + marginTop: props.theme.space.m, +})); const Icon = styled.img((props) => ({ - marginRight: props.theme.space.xs, + marginRight: props.theme.space.m, width: 32, height: 32, borderRadius: 30, @@ -56,11 +57,19 @@ const DownArrowIcon = styled.img((props) => ({ marginBottom: props.theme.space.xs, })); +const TitleContainer = styled.div({ + display: 'flex', + alignItems: 'center', +}); + const TitleText = styled.p((props) => ({ ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, - textAlign: 'center', - marginTop: 5, + color: props.theme.colors.white_0, +})); + +const Subtitle = styled(StyledP)((props) => ({ + color: props.theme.colors.white_400, + marginTop: props.theme.space.xxxs, })); const ValueText = styled.p((props) => ({ @@ -79,7 +88,6 @@ const ColumnContainer = styled.div({ flex: 1, justifyContent: 'flex-end', alignItems: 'flex-end', - paddingTop: 5, }); const MultipleAddressContainer = styled.div({ @@ -87,9 +95,9 @@ const MultipleAddressContainer = styled.div({ flexDirection: 'column', }); -const TokenContainer = styled.div({ - marginRight: 10, -}); +const TokenContainer = styled.div((props) => ({ + marginRight: props.theme.space.m, +})); const IconContainer = styled.div((props) => ({ display: 'flex', @@ -100,7 +108,11 @@ const IconContainer = styled.div((props) => ({ backgroundColor: props.theme.colors.elevation3, width: 32, height: 32, - marginRight: props.theme.space.xs, + marginRight: props.theme.space.m, +})); + +const FiatText = styled(StyledFiatAmountText)((props) => ({ + marginTop: props.theme.space.xxxs, })); type Props = { @@ -187,16 +199,36 @@ function RecipientComponent({ return ( - {recipientIndex && totalRecipient && totalRecipient !== 1 && ( - - {`${t('RECIPIENT')} ${recipientIndex}/${totalRecipient}`} - + {address && ( +
+ {showSenderAddress ? ( + + + + + + ) : ( + + )} +
)} + {heading && {heading}} {value && ( - {renderIcon()} - {title} + + {renderIcon()} +
+ {title} + {currencyType === 'BTC' && Bitcoin} + {currencyType === 'STX' && Stacks} +
+
{currencyType === 'NFT' || currencyType === 'Ordinal' || currencyType === 'RareSat' ? ( {value} @@ -211,24 +243,11 @@ function RecipientComponent({ suffix={currencyType === 'FT' ? ` ${getFtTicker()} ` : ` ${currencyType}`} renderText={(amount) => {amount}} /> - + )}
)} - {address && ( -
- {showSenderAddress ? ( - - - - - - ) : ( - - )} -
- )}
); } diff --git a/src/app/screens/sendRune/recipientSelector.tsx b/src/app/components/recipientSelector/index.tsx similarity index 54% rename from src/app/screens/sendRune/recipientSelector.tsx rename to src/app/components/recipientSelector/index.tsx index 83c1ea60d..f53cc1230 100644 --- a/src/app/screens/sendRune/recipientSelector.tsx +++ b/src/app/components/recipientSelector/index.tsx @@ -1,4 +1,6 @@ +import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; +import { StyledCallout } from '@screens/createInscription/index.styled'; import { validateBtcAddress } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; import Input from '@ui-library/input'; @@ -24,50 +26,80 @@ type Props = { onNext: () => void; isLoading: boolean; header?: React.ReactNode; + calloutText?: string; + insufficientFunds?: boolean; }; -// TODO: this could be extracted into a component for reuse function RecipientSelector({ recipientAddress, setRecipientAddress, onNext, isLoading, header, + calloutText, + insufficientFunds, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); const { network } = useWalletSelector(); const [addressIsValid, setAddressIsValid] = useState(true); + const [toOwnAddress, setToOwnAddress] = useState(false); + const [displayInsufficientFunds, setDisplayInsufficientFunds] = useState(false); + const selectedAccount = useSelectedAccount(); const handleNext = () => { - if (validateBtcAddress({ btcAddress: recipientAddress, network: network.type })) { - onNext(); - } else { + if (insufficientFunds) { + setDisplayInsufficientFunds(true); + return; + } + if (!validateBtcAddress({ btcAddress: recipientAddress, network: network.type })) { setAddressIsValid(false); + return; } + onNext(); }; const handleAddressChange = (e: React.ChangeEvent) => { setRecipientAddress(e.target.value); + setToOwnAddress( + [selectedAccount.btcAddress, selectedAccount.ordinalsAddress].includes(e.target.value), + ); setAddressIsValid(true); + setDisplayInsufficientFunds(false); }; const inputFeedback = useMemo(() => { - if (addressIsValid) { - return []; + if (toOwnAddress) { + return [ + { + variant: 'info' as const, + message: t('YOU_ARE_TRANSFERRING_TO_YOURSELF'), + }, + ]; + } + if (!addressIsValid) { + return [ + { + variant: 'danger' as const, + message: t('ERRORS.ADDRESS_INVALID'), + }, + ]; + } + if (displayInsufficientFunds) { + return [ + { + variant: 'danger' as const, + message: t('ERRORS.INSUFFICIENT_BALANCE_FEES'), + }, + ]; } - return [ - { - variant: 'danger' as const, - message: t('ERRORS.ADDRESS_INVALID'), - }, - ]; - }, [addressIsValid]); + }, [addressIsValid, displayInsufficientFunds, toOwnAddress]); return (
{header}
+ {calloutText && } )} {!hideAddress && ( - {getTruncatedAddress(address)} + {getTruncatedAddress(address, 6)} )} {!hideCopyButton && } diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 7c2e0c2ba..02a9f526a 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -2,7 +2,6 @@ import AmountWithInscriptionSatribute from '@components/confirmBtcTransaction/it import FiatAmountText from '@components/fiatAmountText'; import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { PencilSimple } from '@phosphor-icons/react'; import { btcTransaction, getBtcFiatEquivalent, @@ -13,7 +12,6 @@ import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -import Theme from 'theme'; const Container = styled.div((props) => ({ background: props.theme.colors.elevation1, @@ -40,32 +38,16 @@ const FeeContainer = styled.div({ alignItems: 'flex-end', }); -const CustomRow = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const EditButton = styled.button` - display: flex; - flex-direction: row; - background: transparent; - align-items: center; - gap: ${(props) => props.theme.space.xxs}; - cursor: ${(props) => (props.onClick ? 'pointer' : 'initial')}; - width: 100%; - margin-left: ${(props) => props.theme.space.xs}; +const FeeRate = styled(StyledP)` + margin: ${(props) => props.theme.space.xxxs} 0; `; type Props = { feePerVByte?: BigNumber; fee: BigNumber; currency: string; - title?: string; inscriptions?: btcTransaction.IOInscription[]; satributes?: btcTransaction.IOSatribute[]; - customFeeClick?: () => void; - subtitle?: string; onShowInscription?: (inscription: btcTransaction.IOInscription) => void; }; @@ -73,11 +55,8 @@ function TransferFeeView({ feePerVByte, fee, currency, - title, inscriptions = [], satributes = [], - customFeeClick, - subtitle, onShowInscription = () => {}, }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); @@ -90,27 +69,9 @@ function TransferFeeView({ - - {title ?? t('FEES')} + + {t('NETWORK_FEE')} - {customFeeClick && ( - - - Custom - - {}}> - - Edit - - - - - )} - {subtitle && ( - - {subtitle} - - )} ( - + {value} - + )} /> )} - + 0; - const handleOnConfirmClick = (txHex: string) => { + const handleConfirmClick = (txHex: string) => { if (isLedgerAccount(selectedAccount)) { const txType: LedgerTransactionType = 'ORDINALS'; const state: ConfirmOrdinalsTransactionState = { @@ -160,7 +160,7 @@ function ConfirmOrdinalTransaction() { recipients={[{ address: recipientAddress, amountSats: new BigNumber(0) }]} loadingBroadcastedTx={isLoading} signedTxHex={signedTxHex} - onConfirmClick={handleOnConfirmClick} + onConfirmClick={handleConfirmClick} onCancelClick={handleBackButtonClick} ordinalTxUtxo={ordinalUtxo} assetDetail={selectedOrdinal ? selectedOrdinal.number.toString() : ''} diff --git a/src/app/screens/nftCollection/index.tsx b/src/app/screens/nftCollection/index.tsx index 50322d01e..3d4f8a067 100644 --- a/src/app/screens/nftCollection/index.tsx +++ b/src/app/screens/nftCollection/index.tsx @@ -97,7 +97,7 @@ const BackButton = styled.button((props) => ({ marginBottom: props.theme.spacing(12), })); -const AssetDeatilButtonText = styled.div((props) => ({ +const AssetDetailButtonText = styled.div((props) => ({ ...props.theme.typography.body_m, fontWeight: 400, fontSize: 14, @@ -198,10 +198,12 @@ function NftCollection() { {isGalleryOpen && ( - + <> - {t('BACK_TO_GALLERY')} + + {t('BACK_TO_GALLERY')} + diff --git a/src/app/screens/nftDetail/index.tsx b/src/app/screens/nftDetail/index.tsx index f891caf31..c4b4b3ec0 100644 --- a/src/app/screens/nftDetail/index.tsx +++ b/src/app/screens/nftDetail/index.tsx @@ -198,7 +198,7 @@ function NftDetailScreen() { - + <> {t('MOVE_TO_ASSET_DETAIL')} @@ -232,7 +232,7 @@ function NftDetailScreen() { - + {t('MOVE_TO_ASSET_DETAIL')} diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index b611214c9..926640b9e 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -688,7 +688,7 @@ function OrdinalDetailScreen() { - {isExpanded && ( - {inputs.map((input) => ( + {summary?.inputs.map((input) => ( ))} {t('OUTPUT')} - {outputs.map((output, index) => ( + {summary?.outputs.map((output, index) => ( { + if (!inputs || !outputs) { + return 0; + } + const initialValue = 0; const totalUserSpend = inputs.reduce((accumulator: number, input) => { @@ -67,7 +73,10 @@ export const getOutputsWithAssetsFromUserAddress = ({ btcAddress, ordinalsAddress, outputs, -}: Omit) => { +}: Omit): { + outputsFromPayment: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; + outputsFromOrdinal: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; +} => { // we want to discard outputs that are script, are not from user address and do not have inscriptions or satributes const outputsFromPayment: ( | btcTransaction.TransactionOutput @@ -77,7 +86,7 @@ export const getOutputsWithAssetsFromUserAddress = ({ | btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput )[] = []; - outputs.forEach((output) => { + outputs?.forEach((output) => { if (isScriptOutput(output)) { return; } @@ -104,15 +113,18 @@ export const getOutputsWithAssetsFromUserAddress = ({ return { outputsFromPayment, outputsFromOrdinal }; }; -export const getInputsWitAssetsFromUserAddress = ({ +export const getInputsWithAssetsFromUserAddress = ({ btcAddress, ordinalsAddress, inputs, -}: Omit) => { +}: Omit): { + inputFromPayment: btcTransaction.EnhancedInput[]; + inputFromOrdinal: btcTransaction.EnhancedInput[]; +} => { // we want to discard inputs that are not from user address and do not have inscriptions or satributes const inputFromPayment: btcTransaction.EnhancedInput[] = []; const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; - inputs.forEach((input) => { + inputs?.forEach((input) => { if (!input.inscriptions.length && !input.satributes.length) { return; } @@ -132,10 +144,13 @@ export const getOutputsWithAssetsToUserAddress = ({ btcAddress, ordinalsAddress, outputs, -}: Omit) => { +}: Omit): { + outputsToPayment: btcTransaction.TransactionOutput[]; + outputsToOrdinal: btcTransaction.TransactionOutput[]; +} => { const outputsToPayment: btcTransaction.TransactionOutput[] = []; const outputsToOrdinal: btcTransaction.TransactionOutput[] = []; - outputs.forEach((output) => { + outputs?.forEach((output) => { // we want to discard outputs that are not spendable or are not to user address if ( isScriptOutput(output) || diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index cb14b8b83..d2c541f4a 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -478,6 +478,7 @@ const router = createHashRouter([ ), }, + // TODO can we move this into extended screen container? { path: RequestsRoutes.MintRune, element: ( @@ -486,6 +487,7 @@ const router = createHashRouter([ ), }, + // TODO can we move this into extended screen container? { path: RequestsRoutes.EtchRune, element: ( @@ -509,6 +511,7 @@ const router = createHashRouter([ path: 'send-stx', element: , }, + // TODO can we kill this one? { path: 'confirm-btc-tx', element: , diff --git a/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx b/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx index 1e34a7895..9f7f14488 100644 --- a/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx +++ b/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx @@ -27,6 +27,7 @@ function SendInscriptionsRequest() { isLoading, transaction, summary, + runeSummary, txError, } = useSendInscriptions(); @@ -59,11 +60,9 @@ function SendInscriptionsRequest() { )} {transaction && summary && !txError && ( { const [feeRate, setFeeRate] = useState(''); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); + const [runeSummary, setRuneSummary] = useState(); + const [isExecuting, setIsExecuting] = useState(false); const [isLoading, setIsLoading] = useState(false); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); const { popupPayloadSendInscriptions: { context: { tabId }, @@ -63,6 +73,9 @@ const useSendInscriptions = () => { setFeeRate(desiredFeeRate.toString()); setTransaction(tx); setSummary(txSummary); + if (hasRunesSupport) { + setRuneSummary(await parseSummaryForRunes(txContext, txSummary, txContext.network)); + } } catch (e) { setTransaction(undefined); setSummary(undefined); @@ -131,6 +144,7 @@ const useSendInscriptions = () => { return { transaction, summary, + runeSummary, txError, feeRate, isLoading, diff --git a/src/app/screens/etchRune/index.tsx b/src/app/screens/etchRune/index.tsx index a6b9460f0..49b9148b0 100644 --- a/src/app/screens/etchRune/index.tsx +++ b/src/app/screens/etchRune/index.tsx @@ -64,17 +64,7 @@ function EtchRune() { )} {orderTx && orderTx.summary && !etchError && ( { const payloadToken = params.get('payload') ?? ''; const payload = SuperJSON.parse>(payloadToken); - const mintRequest = { ...payload, network: undefined }; + const mintRequest = { ...payload }; return { mintRequest, tabId, requestId, network: payload.network }; }; -const useMintRequest = () => { +const useMintRequest = (): { + runeInfo: Rune | null; + mintRequest: MintRunesParams; + orderTx: TransactionBuildPayload | null; + mintError: { code: number | undefined; message: string } | null; + feeRate: string; + isExecuting: boolean; + handleMint: () => Promise; + payAndConfirmMintRequest: (ledgerTransport?: Transport) => Promise; + cancelMintRequest: () => Promise; +} => { const { mintRequest, requestId, tabId } = useRuneMintRequestParams(); const txContext = useTransactionContext(); const ordinalsServiceApi = useOrdinalsServiceApi(); diff --git a/src/app/screens/restoreFunds/recoverRunes/index.tsx b/src/app/screens/restoreFunds/recoverRunes/index.tsx index 8e3b962c3..b8df3dacd 100644 --- a/src/app/screens/restoreFunds/recoverRunes/index.tsx +++ b/src/app/screens/restoreFunds/recoverRunes/index.tsx @@ -6,6 +6,7 @@ import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useTransactionContext from '@hooks/useTransactionContext'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; import { + btcTransaction, parseSummaryForRunes, runesTransaction, type RuneSummary, @@ -60,8 +61,7 @@ const ButtonContainer = styled.div((props) => ({ padding: `0 ${props.theme.space.m}`, })); -// TODO: export this from core -type EnhancedTransaction = Awaited>; +type EnhancedTransaction = btcTransaction.EnhancedTransaction; function RecoverRunes() { const { t } = useTranslation('translation', { keyPrefix: 'RECOVER_RUNES_SCREEN' }); @@ -171,12 +171,9 @@ function RecoverRunes() { ) : ( ( + location.state?.recipientAddress || '', + ); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [amountSats, setAmountSats] = useState(location.state?.amount || ''); @@ -46,9 +58,10 @@ function SendBtcScreen() { const [currentStep, setCurrentStep] = useState(initialStep); - const transactionContext = useTransactionContext(); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); + const [runeSummary, setRuneSummary] = useState(); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useEffect(() => { if (!feeRate && btcFeeRate && !feeRatesLoading) { @@ -76,6 +89,7 @@ function SendBtcScreen() { if (!debouncedRecipient || !feeRate) { setTransaction(undefined); setSummary(undefined); + setRuneSummary(undefined); return; } @@ -86,13 +100,20 @@ function SendBtcScreen() { setIsLoading(true); try { const transactionDetails = await generateTransactionAndSummary(); - if (isCancelled.current) return; - setTransaction(transactionDetails.transaction); - - setSummary(transactionDetails.summary); - + if (transactionDetails.summary) { + setSummary(transactionDetails.summary); + if (hasRunesSupport) { + setRuneSummary( + await parseSummaryForRunes( + transactionContext, + transactionDetails.summary, + transactionContext.network, + ), + ); + } + } if (sendMax && transactionDetails.summary) { setAmountSats(transactionDetails.summary.outputs[0].amount.toString()); } @@ -103,7 +124,6 @@ function SendBtcScreen() { // don't log the error if it's just an insufficient funds error console.error(e); } - setTransaction(undefined); setSummary(undefined); } finally { @@ -192,6 +212,8 @@ function SendBtcScreen() { return ( ); } diff --git a/src/app/screens/sendBtc/stepDisplay.tsx b/src/app/screens/sendBtc/stepDisplay.tsx index e874819ba..00d7be006 100644 --- a/src/app/screens/sendBtc/stepDisplay.tsx +++ b/src/app/screens/sendBtc/stepDisplay.tsx @@ -1,5 +1,6 @@ import RecipientSelector from '@components/recipientSelector'; import TokenImage from '@components/tokenImage'; +import type { RuneSummary } from '@secretkeylabs/xverse-core'; import ConfirmBtcTransaction from 'app/components/confirmBtcTransaction'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -29,6 +30,7 @@ const Container = styled.div` type Props = { summary: TransactionSummary | undefined; + runeSummary: RuneSummary | undefined; currentStep: Step; setCurrentStep: (step: Step) => void; recipientAddress: string; @@ -51,6 +53,7 @@ type Props = { function StepDisplay({ summary, + runeSummary, currentStep, setCurrentStep, recipientAddress, @@ -104,9 +107,9 @@ function StepDisplay({ setFeeRate={setFeeRate} sendMax={sendMax} setSendMax={setSendMax} - fee={summary?.fee.toString()} + fee={(summary as TransactionSummary)?.fee.toString()} getFeeForFeeRate={getFeeForFeeRate} - dustFiltered={summary?.dustFiltered ?? false} + dustFiltered={(summary as TransactionSummary)?.dustFiltered ?? false} onNext={() => setCurrentStep(getNextStep(Step.SelectAmount, amountEditable))} hasSufficientFunds={!!summary || isLoading} isLoading={isLoading} @@ -121,10 +124,8 @@ function StepDisplay({ } return ( (Step.SelectRecipient); const [feeRate, setFeeRate] = useState(''); - const [recipientAddress, setRecipientAddress] = useState(location.state?.recipientAddress ?? ''); + const [recipientAddress, setRecipientAddress] = useState( + location.state?.recipientAddress ?? '', + ); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); + const [runeSummary, setRuneSummary] = useState(); const [insufficientFundsError, setInsufficientFundsError] = useState(false); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useResetUserFlow(RoutePaths.SendOrdinal); @@ -49,6 +61,7 @@ function SendRuneScreen() { if (!recipientAddress || !feeRate) { setTransaction(undefined); setSummary(undefined); + setRuneSummary(undefined); setInsufficientFundsError(false); return; } @@ -66,6 +79,15 @@ function SendRuneScreen() { if (!transactionDetails) return; setTransaction(transactionDetails); setSummary(await transactionDetails.getSummary()); + if (hasRunesSupport) { + setRuneSummary( + await parseSummaryForRunes( + context, + await transactionDetails.getSummary(), + context.network, + ), + ); + } } catch (e) { if (e instanceof Error) { // don't log the error if it's just an insufficient funds error @@ -160,8 +182,9 @@ function SendRuneScreen() { return ( void; recipientAddress: string; @@ -48,8 +49,9 @@ type Props = { }; function StepDisplay({ - ordinal, summary, + runeSummary, + ordinal, currentStep, setCurrentStep, recipientAddress, @@ -65,6 +67,7 @@ function StepDisplay({ insufficientFunds, }: Props) { const { t } = useTranslation('translation'); + const header = ( } /> @@ -95,10 +98,8 @@ function StepDisplay({ } return ( ( + location.state?.recipientAddress || '', + ); const [amountError, setAmountError] = useState(''); const [amountToSend, setAmountToSend] = useState(location.state?.amount || ''); const [feeRate, setFeeRate] = useState(''); @@ -227,9 +230,9 @@ function SendRuneScreen() { return ( >; +type PsbtSummary = btcTransaction.PsbtSummary; +type ParsedPsbt = { summary: PsbtSummary; runeSummary: RuneSummary | undefined }; function SignBatchPsbtRequest() { const selectedAccount = useSelectedAccount(); @@ -80,13 +83,45 @@ function SignBatchPsbtRequest() { >(undefined); const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); useTrackMixPanelPageViewed(); + const [parsedPsbts, setParsedPsbts] = useState([]); + + const individualParsedTxSummaryContext = useMemo( + () => ({ + summary: parsedPsbts[currentPsbtIndex]?.summary, + runeSummary: parsedPsbts[currentPsbtIndex]?.runeSummary, + }), + [parsedPsbts, currentPsbtIndex], + ); - const [parsedPsbts, setParsedPsbts] = useState< - { summary: PsbtSummary; runeSummary: RuneSummary | undefined }[] - >([]); + const aggregatedParsedTxSummaryContext: ParsedTxSummaryContextProps = useMemo( + () => ({ + summary: { + inputs: parsedPsbts.map((psbt) => psbt.summary.inputs).flat(), + outputs: parsedPsbts.map((psbt) => psbt.summary.outputs).flat(), + feeOutput: undefined, + isFinal: parsedPsbts.reduce((acc, psbt) => acc && psbt.summary.isFinal, true), + hasSigHashNone: parsedPsbts.reduce( + (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashNone, + false, + ), + hasSigHashSingle: parsedPsbts.reduce( + (acc, psbt) => acc || (psbt.summary as btcTransaction.PsbtSummary)?.hasSigHashSingle, + false, + ), + } as PsbtSummary, + runeSummary: { + burns: parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(), + transfers: parsedPsbts.map((psbt) => psbt.runeSummary?.transfers ?? []).flat(), + receipts: parsedPsbts.map((psbt) => psbt.runeSummary?.receipts ?? []).flat(), + mint: undefined, + inputsHadRunes: false, + } as RuneSummary, + }), + [parsedPsbts], + ); const handlePsbtParsing = useCallback( - async (psbt: SignMultiplePsbtPayload, index: number) => { + async (psbt: SignMultiplePsbtPayload, index: number): Promise => { try { const parsedPsbt = new btcTransaction.EnhancedPsbt(txnContext, psbt.psbtBase64); const summary = await parsedPsbt.getSummary(); @@ -112,13 +147,15 @@ function SignBatchPsbtRequest() { useEffect(() => { (async () => { - const parsedPsbtsResult = await Promise.all(payload.psbts.map(handlePsbtParsing)); - if (parsedPsbtsResult.some((item) => item === undefined)) { - return setIsLoading(false); + const parsedPsbtsRes = await Promise.all(payload.psbts.map(handlePsbtParsing)); + if (parsedPsbtsRes.some((item) => item === undefined)) { + setIsLoading(false); + return; } - setParsedPsbts( - parsedPsbtsResult as { summary: PsbtSummary; runeSummary: RuneSummary | undefined }[], + const validParsedPsbts = parsedPsbtsRes.filter( + (item): item is ParsedPsbt => item !== undefined, ); + setParsedPsbts(validParsedPsbts); setIsLoading(false); })(); }, [payload.psbts.length, handlePsbtParsing]); @@ -171,26 +208,22 @@ function SignBatchPsbtRequest() { for (const psbt of payload.psbts) { // eslint-disable-next-line no-await-in-loop await delay(100); - // eslint-disable-next-line no-await-in-loop const signedPsbt = await confirmSignPsbt(psbt); signedPsbts.push({ txId: signedPsbt.txId, psbtBase64: signedPsbt.signingResponse, }); - if (payload.psbts.findIndex((item) => item === psbt) !== payload.psbts.length - 1) { setSigningPsbtIndex((prevIndex) => prevIndex + 1); } } - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { protocol: 'bitcoin', action: 'sign-psbt', wallet_type: selectedAccount.accountType || 'software', batch: payload.psbts.length, }); - setIsSigningComplete(true); setIsSigning(false); @@ -231,33 +264,18 @@ function SignBatchPsbtRequest() { window.close(); }; - const totalNetAmount = parsedPsbts.reduce( - (sum, psbt) => - psbt && psbt.summary - ? sum.plus( - new BigNumber( - getNetAmount({ - inputs: psbt.summary.inputs, - outputs: psbt.summary.outputs, - btcAddress: selectedAccount.btcAddress, - ordinalsAddress: selectedAccount.ordinalsAddress, - }), - ), - ) - : sum, - new BigNumber(0), - ); - const totalFeeAmount = parsedPsbts.reduce((sum, psbt) => { - const feeAmount = psbt.summary.feeOutput?.amount ?? 0; - return sum.plus(new BigNumber(feeAmount)); - }, new BigNumber(0)); - const hasOutputScript = useMemo( () => parsedPsbts.some((psbt) => psbt.summary.outputs.some((output) => isScriptOutput(output))), [parsedPsbts.length], ); const signingStatus: ConfirmationStatus = isSigningComplete ? 'SUCCESS' : 'LOADING'; + const runeBurns = parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(); + const runeDelegations = parsedPsbts + .filter((psbt) => !psbt.summary.isFinal) + .map((psbt) => psbt.runeSummary?.transfers ?? []) + .flat(); + const hasSomeRuneDelegation = runeDelegations.length > 0; if (isSigning || isSigningComplete) { return ( @@ -278,14 +296,6 @@ function SignBatchPsbtRequest() { ); } - const transactionIsFinal = parsedPsbts.reduce((acc, psbt) => acc && psbt.summary.isFinal, true); - const runeBurns = parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(); - const runeDelegations = parsedPsbts - .filter((psbt) => !psbt.summary.isFinal) - .map((psbt) => psbt.runeSummary?.transfers ?? []) - .flat(); - const hasSomeRuneDelegation = runeDelegations.length > 0; - return ( <> @@ -302,63 +312,35 @@ function SignBatchPsbtRequest() { ) : ( - - {t('SIGN_TRANSACTIONS', { count: parsedPsbts.length })} - - setReviewTransaction(true)}> - {t('REVIEW_ALL')} - - - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={{ - content_type: inscriptionToShow.contentType, - id: inscriptionToShow.id, - inscription_number: inscriptionToShow.number, - }} - /> - )} - {hasSomeRuneDelegation && } - psbt.summary.inputs).flat()} - outputs={parsedPsbts.map((psbt) => psbt.summary.outputs).flat()} - hasExternalInputs={parsedPsbts - .map((psbt) => psbt.summary.inputs) - .flat() - .some( - (input) => - input.extendedUtxo.address !== selectedAccount.btcAddress && - input.extendedUtxo.address !== selectedAccount.ordinalsAddress, - )} - runeTransfers={parsedPsbts - .map((psbt) => psbt.runeSummary?.transfers ?? []) - .flat()} - netAmount={(totalNetAmount.toNumber() + totalFeeAmount.toNumber()) * -1} - transactionIsFinal={transactionIsFinal} - onShowInscription={setInscriptionToShow} - /> - psbt.summary.outputs).flat()} - hasExternalInputs={parsedPsbts - .map((psbt) => psbt.summary.inputs) - .flat() - .some( - (input) => - input.extendedUtxo.address !== selectedAccount.btcAddress && - input.extendedUtxo.address !== selectedAccount.ordinalsAddress, + + + {t('SIGN_TRANSACTIONS', { count: parsedPsbts.length })} + + setReviewTransaction(true)}> + {t('REVIEW_ALL')} + + + {inscriptionToShow && ( + setInscriptionToShow(undefined)} + inscription={{ + content_type: inscriptionToShow.contentType, + id: inscriptionToShow.id, + inscription_number: inscriptionToShow.number, + }} + /> + )} + {hasSomeRuneDelegation && } + + + {!hasSomeRuneDelegation && } + psbt.runeSummary?.mint)} /> + + {hasOutputScript && + !parsedPsbts.some((psbt) => psbt.runeSummary !== undefined) && ( + )} - runeReceipts={parsedPsbts.map((psbt) => psbt.runeSummary?.receipts ?? []).flat()} - onShowInscription={setInscriptionToShow} - netAmount={totalNetAmount.toNumber()} - transactionIsFinal={transactionIsFinal} - /> - {!hasSomeRuneDelegation && } - psbt.runeSummary?.mint)} /> - - {hasOutputScript && !parsedPsbts.some((psbt) => psbt.runeSummary !== undefined) && ( - - )} + )} @@ -389,16 +371,9 @@ function SignBatchPsbtRequest() { {t('TRANSACTION')} {currentPsbtIndex + 1}/{parsedPsbts.length} {!!parsedPsbts[currentPsbtIndex] && ( - + + + )} diff --git a/src/app/screens/signPsbtRequest/index.tsx b/src/app/screens/signPsbtRequest/index.tsx index 9c518f9c2..d4e3bf0a1 100644 --- a/src/app/screens/signPsbtRequest/index.tsx +++ b/src/app/screens/signPsbtRequest/index.tsx @@ -24,8 +24,7 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import useSignPsbt from './useSignPsbt'; import useSignPsbtValidationGate from './useSignPsbtValidationGate'; -// TODO: export this from core -type PSBTSummary = Awaited>; +type PSBTSummary = btcTransaction.PsbtSummary; function SignPsbtRequest() { const navigate = useNavigate(); @@ -47,14 +46,11 @@ function SignPsbtRequest() { payload, parsedPsbt, }); - // extend in future if necessary const isInAppPsbt = magicEdenPsbt && runeId; useTrackMixPanelPageViewed(); - useSignPsbtValidationGate({ payload, parsedPsbt }); - const { network } = useWalletSelector(); const { submitRuneSellPsbt } = useSubmitRuneSellPsbt(); @@ -64,6 +60,7 @@ function SignPsbtRequest() { parsedPsbt .getSummary() + // TODO move this block into useSignPsbt .then(async (txSummary) => { setSummary(txSummary); if (hasRunesSupport) { @@ -100,7 +97,7 @@ function SignPsbtRequest() { wallet_type: selectedAccount?.accountType || 'software', }); if (ledgerTransport) { - await ledgerTransport?.close(); + await ledgerTransport.close(); } if (signedPsbt && magicEdenPsbt && runeId) { return await submitRuneSellPsbt(signedPsbt, location.state.selectedRune?.name ?? '') @@ -178,17 +175,11 @@ function SignPsbtRequest() { return ( Date: Fri, 26 Jul 2024 16:49:57 +0800 Subject: [PATCH 122/219] trackSwapMixPanel --- src/app/screens/swap/index.tsx | 45 ++++++----------- src/app/screens/swap/mixpanel.ts | 56 +++++++++++++++++++++ src/app/screens/swap/quoteSummary/index.tsx | 24 ++++----- 3 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 src/app/screens/swap/mixpanel.ts diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 04bbc1401..9cbffb558 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -37,6 +37,7 @@ import PsbtConfimation from './components/psbtConfirmation/psbtConfirmation'; import RouteItem from './components/routeItem'; import TokenFromBottomSheet from './components/tokenFromBottomSheet'; import TokenToBottomSheet from './components/tokenToBottomSheet'; +import trackSwapMixPanel from './mixpanel'; import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; import { @@ -172,20 +173,13 @@ export default function SwapScreen() { return; } - trackMixPanel(AnalyticsEvents.FetchSwapQuote, { - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, - fromAmount: - fromToken === 'BTC' - ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed(2) - : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2), - toAmount: - toToken.protocol === 'btc' - ? getBtcFiatEquivalent( - new BigNumber(quote?.receiveAmount ?? 0), - new BigNumber(btcFiatRate), - ).toFixed(2) - : new BigNumber(quote?.receiveAmount ?? 0).multipliedBy(runeFloorPrice ?? 0).toFixed(2), + trackSwapMixPanel(AnalyticsEvents.FetchSwapQuote, { + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, }); fetchQuotes({ @@ -380,21 +374,14 @@ export default function SwapScreen() { if (!fromToken || !toToken || !provider) { return; } - trackMixPanel(AnalyticsEvents.SignSwap, { - provider: provider.name, - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, - fromAmount: - fromToken === 'BTC' - ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed(2) - : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2), - toAmount: - toToken.protocol === 'btc' - ? getBtcFiatEquivalent( - new BigNumber(quote?.receiveAmount ?? 0), - new BigNumber(btcFiatRate), - ).toFixed(2) - : new BigNumber(quote?.receiveAmount ?? 0).multipliedBy(runeFloorPrice ?? 0).toFixed(2), + trackSwapMixPanel(AnalyticsEvents.SignSwap, { + provider, + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, }); }; diff --git a/src/app/screens/swap/mixpanel.ts b/src/app/screens/swap/mixpanel.ts new file mode 100644 index 000000000..34c174243 --- /dev/null +++ b/src/app/screens/swap/mixpanel.ts @@ -0,0 +1,56 @@ +import { + getBtcFiatEquivalent, + type FungibleToken, + type Provider, + type Quote, + type Token, +} from '@secretkeylabs/xverse-core'; +import { trackMixPanel } from '@utils/mixpanel'; +import BigNumber from 'bignumber.js'; + +function trackSwapMixPanel( + eventName, + { + provider, + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, + }: { + provider?: Provider; + fromToken?: FungibleToken | 'BTC'; + toToken?: Token; + amount: string; + quote?: Quote; + btcFiatRate: string; + runeFloorPrice?: number; + }, +) { + const from = fromToken === 'BTC' ? 'BTC' : fromToken?.name; + const to = toToken?.protocol === 'btc' ? 'BTC' : toToken?.name ?? toToken?.ticker; + + const fromAmount = + fromToken === 'BTC' + ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed(2) + : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2); + + const toAmount = + toToken?.protocol === 'btc' + ? getBtcFiatEquivalent( + new BigNumber(quote?.receiveAmount ?? 0), + new BigNumber(btcFiatRate), + ).toFixed(2) + : new BigNumber(quote?.receiveAmount ?? 0).multipliedBy(runeFloorPrice ?? 0).toFixed(2); + + trackMixPanel(eventName, { + ...(provider ? { provider: provider?.name } : {}), + ...(from ? { from } : {}), + ...(to ? { to } : {}), + fromAmount, + toAmount, + }); +} + +export default trackSwapMixPanel; diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 8c971fbf4..20b00a106 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -28,6 +28,7 @@ import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled, { useTheme } from 'styled-components'; +import trackSwapMixPanel from '../mixpanel'; import QuoteTile from '../quotesModal/quoteTile'; import { SlippageModalContent } from '../slippageModal'; import { mapFTNativeSwapTokenToTokenBasic } from '../utils'; @@ -208,21 +209,14 @@ export default function QuoteSummary({ return; } - trackMixPanel(AnalyticsEvents.ConfirmSwap, { - provider: quote.provider.name, - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, - fromAmount: - fromToken === 'BTC' - ? getBtcFiatEquivalent(new BigNumber(amount), new BigNumber(btcFiatRate)).toFixed(2) - : new BigNumber(fromToken?.tokenFiatRate ?? 0).multipliedBy(amount).toFixed(2), - toAmount: - toToken.protocol === 'btc' - ? getBtcFiatEquivalent( - new BigNumber(quote.receiveAmount), - new BigNumber(btcFiatRate), - ).toFixed(2) - : new BigNumber(quote.receiveAmount).multipliedBy(runeFloorPrice ?? 0).toFixed(2), + trackSwapMixPanel(AnalyticsEvents.ConfirmSwap, { + provider: quote.provider, + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, }); if (selectedIdentifiers) { From 466cd86464eb684f030d93bcf56dc0425089fdb5 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 26 Jul 2024 16:57:03 +0800 Subject: [PATCH 123/219] Align Swap Route vertically --- src/app/screens/swap/quoteSummary/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 20b00a106..29034312d 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -87,7 +87,7 @@ const ListingDescriptionRow = styled.div` const RouteContainer = styled.div` display: flex; flex-direction: row; - justify-content: right; + align-items: center; flex: 1; gap: 4px; `; From bd92c3430ece64dfabe91b8f34bdfbf29a7d81f9 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 26 Jul 2024 17:00:13 +0800 Subject: [PATCH 124/219] Fiat Value prefix --- src/app/screens/swap/quoteSummary/index.tsx | 1 - src/app/screens/swap/quotesModal/quoteTile.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index 29034312d..edf2d1efb 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -288,7 +288,6 @@ export default function QuoteSummary({ ( {value} From 813a953b18ee3ee21f013864a5323ce8e89f7d16 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 26 Jul 2024 17:53:22 +0800 Subject: [PATCH 125/219] Improve Green Ellipse --- .../screens/swap/quotesModal/quoteTile.tsx | 20 ++++++++++++++++++- src/locales/en.json | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/quotesModal/quoteTile.tsx b/src/app/screens/swap/quotesModal/quoteTile.tsx index b5c7256d1..fd2f1491c 100644 --- a/src/app/screens/swap/quotesModal/quoteTile.tsx +++ b/src/app/screens/swap/quotesModal/quoteTile.tsx @@ -19,6 +19,8 @@ const Container = styled.button<{ clickable: boolean }>` background: transparent; width: 100%; cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')}; + align-items: center; + justify-content: center; `; const RowCenter = styled.div` @@ -37,6 +39,19 @@ const InfoContainer = styled.div` margin-right: ${({ theme }) => theme.space.s}; `; +const GreenEllipse = styled.div` + width: 6px; + height: 6px; + background-color: ${({ theme }) => theme.colors.success_light}; + border-radius: 50%; +`; + +const SubtitleContainer = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.space.xxs}; +`; + interface Props { provider: string; price: string; @@ -88,7 +103,10 @@ function QuoteTile({ {subtitle && subtitleColor && ( - {subtitle} + + {subtitleColor === 'success_light' && } + {subtitle} + )} {fiatValue && ( diff --git a/src/locales/en.json b/src/locales/en.json index 7a63fc86b..f5d85e940 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1323,8 +1323,8 @@ "EXCHANGE": "EXCHANGE", "MARKETPLACE": "MARKETPLACE", "FLOOR_PRICE": "Floor price", - "RECOMMENDED": "• Recommended", - "BEST": "• Best", + "RECOMMENDED": "Recommended", + "BEST": "Best", "SELECT_BUNDLES": "Select bundles to proceed with the swap", "SWAP_FROM": "Swap from", "SWAP_TO": "Swap to", From 65b96d152a057b3d769df847eaee0ac38e927d40 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 26 Jul 2024 18:46:46 +0800 Subject: [PATCH 126/219] Prevent swapping from/to the same asset --- .../components/tokenFromBottomSheet/index.tsx | 7 ++++--- .../tokenFromBottomSheet/useFromTokens.ts | 17 +++++++++++++---- .../tokenToBottomSheet/useToTokens.ts | 8 +++++++- src/app/screens/swap/index.tsx | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index ecadce0fe..33e53fe9c 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -1,5 +1,5 @@ import TokenTile from '@components/tokenTile'; -import type { FungibleToken } from '@secretkeylabs/xverse-core'; +import type { FungibleToken, Token } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import { useTranslation } from 'react-i18next'; @@ -24,11 +24,12 @@ interface Props { title: string; onSelectCoin: (token: FungibleToken | 'BTC') => void; onClose: () => void; + to?: Token; } -export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onClose }: Props) { +export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onClose, to }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const fromTokens = useFromTokens(); + const fromTokens = useFromTokens(to); return ( diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index b43600ee7..39cc8d584 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -1,10 +1,10 @@ import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import useCoinRates from '@hooks/queries/useCoinRates'; import useSelectedAccount from '@hooks/useSelectedAccount'; -import { type FungibleToken } from '@secretkeylabs/xverse-core'; +import { type FungibleToken, type Token } from '@secretkeylabs/xverse-core'; import { sortFtByFiatBalance } from '@utils/tokens'; -const useFromTokens = () => { +const useFromTokens = (toToken?: Token) => { const { unfilteredData: runesCoinsList } = useRuneFungibleTokensQuery(); const { stxBtcRate, btcFiatRate } = useCoinRates(); const { btcAddress } = useSelectedAccount(); @@ -12,9 +12,18 @@ const useFromTokens = () => { const tokens: (FungibleToken | 'BTC')[] = (runesCoinsList ?? []).sort((a, b) => sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate), ); - if (btcAddress) tokens.unshift('BTC'); - return tokens; + if (tokens.includes('BTC')) { + tokens.splice(tokens.indexOf('BTC'), 1); + } + + if (btcAddress && toToken?.protocol !== 'btc') tokens.unshift('BTC'); + + const filteredTokens = toToken + ? tokens.filter((token) => token === 'BTC' || token.assetName !== toToken.name) + : tokens; + + return filteredTokens; }; export default useFromTokens; diff --git a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts index d1e46ec39..71fc4c184 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts +++ b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts @@ -29,7 +29,13 @@ const useToTokens = (protocol: Protocol, from?: TokenBasic, query?: string) => { userTokens, }); - return response.items.sort((a) => (a.protocol === 'btc' ? -1 : 1)); + const sortedResponse = response.items.sort((a) => (a.protocol === 'btc' ? -1 : 1)); + + const filteredResponse = from + ? sortedResponse.filter((s) => s.ticker !== from.ticker) + : sortedResponse; + + return filteredResponse; }; return useQuery({ diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 9cbffb558..6926870d8 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -543,6 +543,7 @@ export default function SwapScreen() { onSelectCoin={onChangeFromToken} visible={tokenSelectionBottomSheet === 'from'} title={t('SWAP_SCREEN.SWAP_FROM')} + to={toToken} /> setTokenSelectionBottomSheet(null)} From 995f7c97d5ac17f9c7836ab9202d2cb5c7d19380 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Fri, 26 Jul 2024 15:21:21 +0200 Subject: [PATCH 127/219] Add ME flow --- src/app/screens/swap/utxoSelection/index.tsx | 4 +- .../screens/swap/utxoSelection/utxoItem.tsx | 2 +- tests/pages/wallet.ts | 6 + tests/specs/flowSwapME.spec.ts | 144 ++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 tests/specs/flowSwapME.spec.ts diff --git a/src/app/screens/swap/utxoSelection/index.tsx b/src/app/screens/swap/utxoSelection/index.tsx index 726cc406f..440938752 100644 --- a/src/app/screens/swap/utxoSelection/index.tsx +++ b/src/app/screens/swap/utxoSelection/index.tsx @@ -274,7 +274,7 @@ export default function UtxoSelection({ suffix=" BTC" thousandSeparator renderText={(value: string) => ( - + {value} )} @@ -308,7 +308,7 @@ export default function UtxoSelection({ suffix={` ${toToken?.symbol}`} thousandSeparator renderText={(value: string) => ( - + {value} )} diff --git a/src/app/screens/swap/utxoSelection/utxoItem.tsx b/src/app/screens/swap/utxoSelection/utxoItem.tsx index 9a984d6b3..06ba5b63e 100644 --- a/src/app/screens/swap/utxoSelection/utxoItem.tsx +++ b/src/app/screens/swap/utxoSelection/utxoItem.tsx @@ -89,7 +89,7 @@ function UtxoItem({ utxo, selected, token, onSelect }: Props) { suffix={` ${token?.symbol}`} thousandSeparator renderText={(value: string) => ( - + {value} )} diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 8c799a067..f88f02f7a 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -352,6 +352,10 @@ export default class Wallet { readonly labelTotalFee: Locator; + readonly itemUTXO: Locator; + + readonly titleUTXO: Locator; + constructor(readonly page: Page) { this.page = page; this.navigationDashboard = page.getByTestId('nav-dashboard'); @@ -506,6 +510,8 @@ export default class Wallet { this.buttonSlippage = page.getByTestId('slippage-button'); this.minReceivedAmount = page.getByTestId('min-received-amount'); this.nameRune = page.getByTestId('rune-name'); + this.itemUTXO = page.getByTestId('utxo-item'); + this.titleUTXO = page.getByTestId('utxo-title'); this.buttonDetails = page.getByRole('button', { name: 'Details' }); this.buttonInsufficientBalance = page.getByRole('button', { name: 'Insufficient balance' }); diff --git a/tests/specs/flowSwapME.spec.ts b/tests/specs/flowSwapME.spec.ts new file mode 100644 index 000000000..d65dd3868 --- /dev/null +++ b/tests/specs/flowSwapME.spec.ts @@ -0,0 +1,144 @@ +import { expect, test } from '../fixtures/base'; +import Wallet from '../pages/wallet'; + +test.describe('Swap Flow ME', () => { + // Enables the feature flag for Swap + test.beforeEach(async ({ page }) => { + await page.route('https://api-3.xverse.app/v1/app-features', (route) => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + CROSS_CHAIN_SWAPS: { enabled: true }, + }), + }); + }); + }); + + test('Cancel swap token via ME', async ({ page, extensionId }) => { + // Restore wallet and setup Testnet network + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // get own BTC & Ordinals Address for address check on review page + await wallet.allUpperButtons.nth(1).click(); + const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); + + // Reload the page to close the modal window for the addresses as the X button needs to have a better locator + await page.reload(); + + await wallet.checkVisualsStartpage(); + + // Save initial Balance for later Balance checks + const initialBTCBalance = await wallet.getTokenBalance('Bitcoin'); + + await wallet.allUpperButtons.nth(2).click(); + await wallet.checkVisualsSwapPage(); + + // Select the first Coin + await wallet.buttonDownArrow.nth(0).click(); + + // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded + await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.first()).not.toContainText('Select asset'); + await expect(wallet.imageToken.first()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // Select the second Coin + await wallet.buttonDownArrow.nth(1).click(); + // Had problems with loading of all tokens so I check that a 'DOG' is loaded + await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.last()).not.toContainText('Select asset'); + await expect(wallet.imageToken.last()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // tried a calculated value but had multiple problems with that, for now we stick to a specific value + const swapAmount = 0.00000546; + + const numericUSDValue = await wallet.fillSwapAmount(swapAmount); + + // Save rune token name + const tokenName1 = await wallet.nameToken.last().innerText(); + + await wallet.buttonGetQuotes.click(); + await expect(wallet.nameSwapPlace.last()).toBeVisible(); + await expect(wallet.quoteAmount.last()).toBeVisible(); + await expect(wallet.infoMessage.last()).toBeVisible(); + await expect(wallet.buttonSwapPlace.last()).toBeVisible(); + + await wallet.buttonSwapPlace.filter({ hasText: 'Magic Eden' }).click(); + await expect(wallet.itemUTXO.first()).toBeVisible(); + + // click only on a UTXO with value from 1000 e(not enough funds for higher) + await wallet.itemUTXO.filter({ hasText: '1,000' }).first().locator('input').click(); + await expect(wallet.buttonNext).toBeVisible(); + await expect(wallet.textUSD).toBeVisible(); + await expect(wallet.quoteAmount).toBeVisible(); + + const quoteAmount = await wallet.quoteAmount.innerText(); + const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toBeGreaterThan(0); + + const usdAmount = await wallet.textUSD.innerText(); + const numericUSDValueSwap = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); + await expect(numericUSDValueSwap).toBeGreaterThan(0); + + await wallet.buttonNext.click(); + + await wallet.checkVisualsQuotePage(tokenName1, false, numericQuoteValue, numericUSDValueSwap); + + // We can only continue if the FeeRate is above 0 + await wallet.waitForTextAboveZero(wallet.feeAmount, 30000); + + // Save the current fee amount for comparison + const originalFee = await wallet.feeAmount.innerText(); + const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); + await expect(numericOriginalFee).toBeGreaterThan(0); + + // Click on edit Fee button + await wallet.buttonEditFee.click(); + await expect(wallet.buttonSelectFee.first()).toBeVisible(); + await expect(wallet.labelTotalFee.first()).toBeVisible(); + + // Compare medium fee to previous saved fee + const mediumFee = await wallet.labelTotalFee.last().innerText(); + const numericMediumFee = parseFloat(mediumFee.replace(/[^0-9.]/g, '')); + await expect(numericMediumFee).toBe(numericOriginalFee); + + // Save high fee rate for comparison + const highFee = await wallet.labelTotalFee.first().innerText(); + const numericHighFee = parseFloat(highFee.replace(/[^0-9.]/g, '')); + + // Switch to high fee + await wallet.buttonSelectFee.first().click(); + + const newFee = await wallet.feeAmount.innerText(); + const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); + await expect(numericNewFee).toBe(numericHighFee); + + await wallet.buttonSwap.click(); + await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); + + const sendRuneAmount = await wallet.sendRuneAmount.innerText(); + const sendAmountNumerical = parseFloat(sendRuneAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toEqual(sendAmountNumerical); + + // Check Rune token name + await expect(wallet.nameRune).toContainText(tokenName1); + + // Cancel the transaction + await expect(wallet.buttonCancel).toBeEnabled(); + await wallet.buttonCancel.click(); + + // Check Startpage + await wallet.checkVisualsStartpage(); + + // Check BTC Balance after cancel the transaction + const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); + await expect(initialBTCBalance).toEqual(balanceAfterCancel); + }); +}); From 447fc57c06aa4ab02990213c48be3b099142b648 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Fri, 26 Jul 2024 22:00:53 +0800 Subject: [PATCH 128/219] Address fede feedback --- .../components/tokenFromBottomSheet/useFromTokens.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index 39cc8d584..7ce7dd4bc 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -9,18 +9,15 @@ const useFromTokens = (toToken?: Token) => { const { stxBtcRate, btcFiatRate } = useCoinRates(); const { btcAddress } = useSelectedAccount(); - const tokens: (FungibleToken | 'BTC')[] = (runesCoinsList ?? []).sort((a, b) => + // Create a copy of the tokens array to avoid global changes + const tokens: (FungibleToken | 'BTC')[] = [...(runesCoinsList ?? [])].sort((a, b) => sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate), ); - if (tokens.includes('BTC')) { - tokens.splice(tokens.indexOf('BTC'), 1); - } - if (btcAddress && toToken?.protocol !== 'btc') tokens.unshift('BTC'); const filteredTokens = toToken - ? tokens.filter((token) => token === 'BTC' || token.assetName !== toToken.name) + ? tokens.filter((token) => token === 'BTC' || token.principal !== toToken.ticker) : tokens; return filteredTokens; From 8f3db94c2d696fe1653b7100e2028ac7eafea4b6 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Mon, 29 Jul 2024 10:50:54 +0300 Subject: [PATCH 129/219] update confirmation screen --- src/app/screens/transferRunesRequest/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/screens/transferRunesRequest/index.tsx b/src/app/screens/transferRunesRequest/index.tsx index aec4d4827..a5c0266d6 100644 --- a/src/app/screens/transferRunesRequest/index.tsx +++ b/src/app/screens/transferRunesRequest/index.tsx @@ -61,11 +61,8 @@ function TransferRunesRequest() { {transaction && summary && !txError && ( Date: Mon, 29 Jul 2024 10:42:52 +0200 Subject: [PATCH 130/219] Allow pasting seed phrase (#360) --- src/app/components/seedPhraseInput/index.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/components/seedPhraseInput/index.tsx b/src/app/components/seedPhraseInput/index.tsx index d6f54698d..fdcc03800 100644 --- a/src/app/components/seedPhraseInput/index.tsx +++ b/src/app/components/seedPhraseInput/index.tsx @@ -68,11 +68,9 @@ const SeedWordInput = React.forwardRef( const handlePasteInput = (e: React.ClipboardEvent) => { e.preventDefault(); - if (DEV_MODE || process.env.WALLET_LABEL) { - const { clipboardData } = e; - const pastedText = clipboardData.getData('text'); - handlePaste(pastedText); - } + const { clipboardData } = e; + const pastedText = clipboardData.getData('text'); + handlePaste(pastedText); }; return ( From 204527d2691d65972cb9b8007a127d434d5da46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Fri, 26 Jul 2024 13:52:45 +0200 Subject: [PATCH 131/219] Remove RPC response source filtering --- src/content-scripts/content-script.ts | 6 ++-- src/inpage/sats.inpage.ts | 50 ++++++++++++++++++--------- src/inpage/utils.ts | 5 --- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/content-scripts/content-script.ts b/src/content-scripts/content-script.ts index a82004548..bceeda6a2 100644 --- a/src/content-scripts/content-script.ts +++ b/src/content-scripts/content-script.ts @@ -64,10 +64,8 @@ function sendMessageToBackground( // Receives message from background script to execute in browser chrome.runtime.onMessage.addListener((message: LegacyMessageToContentScript) => { - if (message.source === MESSAGE_SOURCE) { - // Forward to web app (browser) - window.postMessage(message, window.location.origin); - } + // Forward to web app (browser) + window.postMessage(message, window.location.origin); }); interface ForwardDomEventToBackgroundArgs { diff --git a/src/inpage/sats.inpage.ts b/src/inpage/sats.inpage.ts index d96c5ade8..f4b81a2c1 100644 --- a/src/inpage/sats.inpage.ts +++ b/src/inpage/sats.inpage.ts @@ -18,20 +18,22 @@ import { type SignMessageResponseMessage, type SignPsbtResponseMessage, } from '@common/types/message-types'; -import type { - BitcoinProvider, - CreateInscriptionResponse, - CreateRepeatInscriptionsResponse, - GetAddressResponse, - Params, - Requests, - RpcRequest, - RpcResponse, - SignMultipleTransactionsResponse, - SignTransactionResponse, +import { + rpcResponseMessageSchema, + type BitcoinProvider, + type CreateInscriptionResponse, + type CreateRepeatInscriptionsResponse, + type GetAddressResponse, + type Params, + type Requests, + type RpcRequest, + type RpcResponse, + type SignMultipleTransactionsResponse, + type SignTransactionResponse, } from '@sats-connect/core'; import { nanoid } from 'nanoid'; -import { isValidLegacyEvent, isValidRpcEvent } from './utils'; +import * as v from 'valibot'; +import { isValidLegacyEvent } from './utils'; const SatsMethodsProvider: BitcoinProvider = { connect: async (btcAddressRequest): Promise => { @@ -218,14 +220,28 @@ const SatsMethodsProvider: BitcoinProvider = { const rpcRequestEvent = new CustomEvent(DomEventName.rpcRequest, { detail: rpcRequest }); document.dispatchEvent(rpcRequestEvent); return new Promise((resolve) => { - function handleRpcResponseEvent(eventMessage: MessageEvent) { - if (!isValidRpcEvent(eventMessage)) return; - const response = eventMessage.data; - if (response.id !== id) { + function handleRpcResponseEvent(message: MessageEvent) { + const parseResult = v.safeParse(rpcResponseMessageSchema, message.data); + + if (!parseResult.success) { + // Ignore message if it's not an RPC message. + return; + } + + const rpcResponseMessage = parseResult.output; + + if (rpcResponseMessage.id !== id) { + // Ignore message if it's not a response to the current request. return; } + window.removeEventListener('message', handleRpcResponseEvent); - return resolve(response); + + // NOTE: Ideally the response would be runtime type-checked before the + // promise is resolved since the message crosses a type assertion + // boundary. For now, since all the responses are typed, it's relatively + // safe to assume that the message will conform to the expected type. + return resolve(rpcResponseMessage as RpcResponse); } window.addEventListener('message', handleRpcResponseEvent); }); diff --git a/src/inpage/utils.ts b/src/inpage/utils.ts index 0001914cd..957c531f9 100644 --- a/src/inpage/utils.ts +++ b/src/inpage/utils.ts @@ -24,11 +24,6 @@ export const isValidLegacyEvent = ( return correctSource && correctMethod && !!data.payload; }; -export const isValidRpcEvent = (event: MessageEvent) => { - const { data } = event; - return data.source === MESSAGE_SOURCE; -}; - export const callAndReceive = async ( methodName: CallableMethods | 'getURL', opts: any = {}, From b6bbf529564c2f22a02978c858591491630714d9 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Mon, 29 Jul 2024 14:12:19 +0200 Subject: [PATCH 132/219] spelling --- tests/pages/wallet.ts | 2 +- tests/specs/runesSend.spec.ts | 2 +- tests/specs/runesSendCancel.spec.ts | 2 +- tests/specs/tabCollectiblesInscriptions.spec.ts | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index f88f02f7a..31b302450 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -864,7 +864,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await expect(this.runeContainer.first()).toBeVisible(); } - async invalidAdressCheck(adressfield) { + async invalidAddressCheck(adressfield) { await adressfield.fill(`Test Address 123`); await this.buttonNext.click(); await expect(this.errorMessageAddressInvalid).toBeVisible(); diff --git a/tests/specs/runesSend.spec.ts b/tests/specs/runesSend.spec.ts index 89079f48c..3dcbce074 100644 --- a/tests/specs/runesSend.spec.ts +++ b/tests/specs/runesSend.spec.ts @@ -67,7 +67,7 @@ test.describe('Send runes', () => { await wallet.checkVisualsSendPage1('send-rune'); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); diff --git a/tests/specs/runesSendCancel.spec.ts b/tests/specs/runesSendCancel.spec.ts index 5f4d66b0c..a2ea11f8e 100644 --- a/tests/specs/runesSendCancel.spec.ts +++ b/tests/specs/runesSendCancel.spec.ts @@ -29,7 +29,7 @@ test.describe('Send runes', () => { await wallet.checkVisualsSendPage1('send-rune'); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); diff --git a/tests/specs/tabCollectiblesInscriptions.spec.ts b/tests/specs/tabCollectiblesInscriptions.spec.ts index cc68fb26d..5a180dda0 100644 --- a/tests/specs/tabCollectiblesInscriptions.spec.ts +++ b/tests/specs/tabCollectiblesInscriptions.spec.ts @@ -55,7 +55,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); // Check Info message await wallet.receiveAddress.fill(addressOrdinals); @@ -139,7 +139,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); @@ -207,7 +207,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); // Check Info message await wallet.receiveAddress.fill(addressOrdinals); From cf3af4b6960d5d54f468376b6f161ae33031c840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Mon, 29 Jul 2024 15:40:36 +0200 Subject: [PATCH 133/219] suggestions --- package-lock.json | 15 ++++---- package.json | 2 +- .../screens/transferRunesRequest/index.tsx | 27 +++++++++++++- .../useTransferRunesRequest.ts | 37 +++++-------------- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f65755a2..4b3528e1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "1.45.2", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "^0.1.2-cc71c98", + "@sats-connect/core": "0.1.2-aa7093b", "@scure/btc-signer": "1.2.1", "@secretkeylabs/xverse-core": "18.2.0-6b1dc9e", "@stacks/connect": "7.4.1", @@ -1752,9 +1752,10 @@ ] }, "node_modules/@sats-connect/core": { - "version": "0.1.2-cc71c98", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.2-cc71c98.tgz", - "integrity": "sha512-utbTi1o4u8JPDA1Ml0EiFEUmUJLC9gTEBKaHQCMKrAnAP7CnubibRzsCmkWjIwQj6lZ457OX9+/BEEOyaClC7A==", + "version": "0.1.2-aa7093b", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.2-aa7093b.tgz", + "integrity": "sha512-cmGHQH4fVCW3Ihs+hHnA+O7tRBZFmweol7g0w1dHYqjeNqNEeOXwnypSVICUORTeIgXXbAU7ptEQrCIF5bKWvw==", + "license": "ISC", "dependencies": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", @@ -15244,9 +15245,9 @@ "optional": true }, "@sats-connect/core": { - "version": "0.1.2-cc71c98", - "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.2-cc71c98.tgz", - "integrity": "sha512-utbTi1o4u8JPDA1Ml0EiFEUmUJLC9gTEBKaHQCMKrAnAP7CnubibRzsCmkWjIwQj6lZ457OX9+/BEEOyaClC7A==", + "version": "0.1.2-aa7093b", + "resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.1.2-aa7093b.tgz", + "integrity": "sha512-cmGHQH4fVCW3Ihs+hHnA+O7tRBZFmweol7g0w1dHYqjeNqNEeOXwnypSVICUORTeIgXXbAU7ptEQrCIF5bKWvw==", "requires": { "axios": "1.6.8", "bitcoin-address-validation": "2.2.3", diff --git a/package.json b/package.json index e597c65fc..1fc10ab55 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@phosphor-icons/react": "^2.0.10", "@playwright/test": "1.45.2", "@react-spring/web": "^9.6.1", - "@sats-connect/core": "^0.1.2-cc71c98", + "@sats-connect/core": "0.1.2-aa7093b", "@scure/btc-signer": "1.2.1", "@secretkeylabs/xverse-core": "18.2.0-6b1dc9e", "@stacks/connect": "7.4.1", diff --git a/src/app/screens/transferRunesRequest/index.tsx b/src/app/screens/transferRunesRequest/index.tsx index a5c0266d6..311a8a5c7 100644 --- a/src/app/screens/transferRunesRequest/index.tsx +++ b/src/app/screens/transferRunesRequest/index.tsx @@ -1,10 +1,16 @@ +import { getPopupPayload, type Context } from '@common/utils/popup'; import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import RequestError from '@components/requests/requestError'; +import { + transferRunesRequestSchema, + type TransferRunesRequest as TTransferRunesRequest, +} from '@sats-connect/core'; import { type Transport } from '@secretkeylabs/xverse-core'; import Spinner from '@ui-library/spinner'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import * as v from 'valibot'; import useTransferRunes from './useTransferRunesRequest'; const LoaderContainer = styled.div(() => ({ @@ -14,7 +20,11 @@ const LoaderContainer = styled.div(() => ({ alignItems: 'center', })); -function TransferRunesRequest() { +type TransferRunesRequestInnerProps = { + context: Context; + data: TTransferRunesRequest; +}; +function TransferRunesRequestInner({ data, context }: TransferRunesRequestInnerProps) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const navigate = useNavigate(); const { @@ -29,7 +39,11 @@ function TransferRunesRequest() { summary, runesSummary, txError, - } = useTransferRunes(); + } = useTransferRunes({ + tabId: context.tabId, + messageId: data.id, + recipients: data.params.recipients, + }); const onClickCancel = async () => { await cancelRunesTransferRequest(); @@ -80,4 +94,13 @@ function TransferRunesRequest() { ); } +export function TransferRunesRequest() { + const [error, data] = getPopupPayload(v.parser(transferRunesRequestSchema)); + if (error) { + throw new Error('Invalid payload'); + } + + return ; +} + export default TransferRunesRequest; diff --git a/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts b/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts index c7df594f1..a49f2a877 100644 --- a/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts +++ b/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts @@ -1,10 +1,9 @@ -import { getPopupPayload } from '@common/utils/popup'; import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; import { sendUserRejectionMessage } from '@common/utils/rpc/responseMessages/errors'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import { RpcErrorCode, transferRunesSchema } from '@sats-connect/core'; +import { RpcErrorCode, type TransferRunesRequest } from '@sats-connect/core'; import { type TransactionSummary } from '@screens/sendBtc/helpers'; import { btcTransaction, @@ -14,20 +13,13 @@ import { type Transport, } from '@secretkeylabs/xverse-core'; import { useEffect, useState } from 'react'; -import * as v from 'valibot'; -const useTransferRunesRequest = () => { - const [error, popupPayloadTransferRunes] = getPopupPayload((data) => - v.parse(transferRunesSchema, data), - ); - if (!popupPayloadTransferRunes) { - throw new Error('Invalid payload'); - } - - return { popupPayloadTransferRunes, error }; +type Args = { + tabId: number; + messageId: string; + recipients: TransferRunesRequest['params']['recipients']; }; - -const useTransferRunes = () => { +const useTransferRunes = ({ tabId, messageId, recipients }: Args) => { const [txError, setTxError] = useState<{ code: number | undefined; message: string; @@ -38,15 +30,6 @@ const useTransferRunes = () => { const [runesSummary, setRunesSummary] = useState(); const [isExecuting, setIsExecuting] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { - popupPayloadTransferRunes: { - context: { tabId }, - data: { - params: { recipients }, - id, - }, - }, - } = useTransferRunesRequest(); const { data: btcFeeRates } = useBtcFeeRate(); const { network } = useWalletSelector(); const txContext = useTransactionContext(); @@ -115,20 +98,20 @@ const useTransferRunes = () => { rbfEnabled: false, }); if (!txid) { - const response = makeRPCError(id, { + const response = makeRPCError(messageId, { code: RpcErrorCode.INTERNAL_ERROR, message: 'Failed to broadcast transaction', }); sendRpcResponse(+tabId, response); return; } - const runesTransferResponse = makeRpcSuccessResponse<'runes_transfer'>(id, { + const runesTransferResponse = makeRpcSuccessResponse<'runes_transfer'>(messageId, { txid, }); sendRpcResponse(+tabId, runesTransferResponse); return txid; } catch (err) { - const errorResponse = makeRPCError(id, { + const errorResponse = makeRPCError(messageId, { code: RpcErrorCode.INTERNAL_ERROR, message: JSON.stringify((err as any).response.data), }); @@ -140,7 +123,7 @@ const useTransferRunes = () => { }; const cancelRunesTransferRequest = async () => { - sendUserRejectionMessage({ tabId: +tabId, messageId: id }); + sendUserRejectionMessage({ tabId, messageId }); }; return { From e02b4693bebabd1d49008bce62419ec5131566f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Mon, 29 Jul 2024 15:52:59 +0200 Subject: [PATCH 134/219] suggestion2 --- .../useTransferRunesRequest.ts | 148 +++++++++--------- 1 file changed, 78 insertions(+), 70 deletions(-) diff --git a/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts b/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts index a49f2a877..b4da67fb4 100644 --- a/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts +++ b/src/app/screens/transferRunesRequest/useTransferRunesRequest.ts @@ -12,7 +12,7 @@ import { type RuneSummary, type Transport, } from '@secretkeylabs/xverse-core'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; type Args = { tabId: number; @@ -34,44 +34,50 @@ const useTransferRunes = ({ tabId, messageId, recipients }: Args) => { const { network } = useWalletSelector(); const txContext = useTransactionContext(); - const generateTransferTxAndSummary = async (desiredFeeRate: number) => { - const requestRecipients = recipients.map((recipient) => ({ - toAddress: recipient.address, - runeName: recipient.runeName, - amount: BigInt(recipient.amount), - })); - const tx = await runesTransaction.sendManyRunes( - txContext, - requestRecipients, - Number(desiredFeeRate), - ); - const txSummary = await tx.getSummary(); - const parsedRunesSummary = await parseSummaryForRunes(txContext, txSummary, network.type); - return { tx, txSummary, parsedRunesSummary }; - }; - - const buildTx = async (desiredFeeRate: number | undefined) => { - try { - if (!desiredFeeRate) return; - setIsLoading(true); - const { tx, txSummary, parsedRunesSummary } = await generateTransferTxAndSummary( - desiredFeeRate, + const generateTransferTxAndSummary = useCallback( + async (desiredFeeRate: number) => { + const requestRecipients = recipients.map((recipient) => ({ + toAddress: recipient.address, + runeName: recipient.runeName, + amount: BigInt(recipient.amount), + })); + const tx = await runesTransaction.sendManyRunes( + txContext, + requestRecipients, + Number(desiredFeeRate), ); - setFeeRate(desiredFeeRate.toString()); - setTransaction(tx); - setSummary(txSummary); - setRunesSummary(parsedRunesSummary); - } catch (e) { - setTransaction(undefined); - setSummary(undefined); - setTxError({ - code: RpcErrorCode.INTERNAL_ERROR, - message: (e as any).message, - }); - } finally { - setIsLoading(false); - } - }; + const txSummary = await tx.getSummary(); + const parsedRunesSummary = await parseSummaryForRunes(txContext, txSummary, network.type); + return { tx, txSummary, parsedRunesSummary }; + }, + [network.type, recipients, txContext], + ); + + const buildTx = useCallback( + async (desiredFeeRate: number | undefined) => { + try { + if (!desiredFeeRate) return; + setIsLoading(true); + const { tx, txSummary, parsedRunesSummary } = await generateTransferTxAndSummary( + desiredFeeRate, + ); + setFeeRate(desiredFeeRate.toString()); + setTransaction(tx); + setSummary(txSummary); + setRunesSummary(parsedRunesSummary); + } catch (e) { + setTransaction(undefined); + setSummary(undefined); + setTxError({ + code: RpcErrorCode.INTERNAL_ERROR, + message: (e as any).message, + }); + } finally { + setIsLoading(false); + } + }, + [generateTransferTxAndSummary], + ); const getFeeForFeeRate = async (desiredFeeRate: number): Promise => { const { txSummary } = await generateTransferTxAndSummary(desiredFeeRate); @@ -87,44 +93,46 @@ const useTransferRunes = ({ tabId, messageId, recipients }: Args) => { } buildTx(initialFeeRate); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [feeRate, btcFeeRates?.priority]); + }, [feeRate, btcFeeRates?.priority, buildTx]); - const confirmRunesTransferRequest = async (ledgerTransport?: Transport) => { - try { - setIsExecuting(true); - const txid = await transaction?.broadcast({ - ledgerTransport, - rbfEnabled: false, - }); - if (!txid) { - const response = makeRPCError(messageId, { + const confirmRunesTransferRequest = useCallback( + async (ledgerTransport?: Transport) => { + try { + setIsExecuting(true); + const txid = await transaction?.broadcast({ + ledgerTransport, + rbfEnabled: false, + }); + if (!txid) { + const response = makeRPCError(messageId, { + code: RpcErrorCode.INTERNAL_ERROR, + message: 'Failed to broadcast transaction', + }); + sendRpcResponse(tabId, response); + return; + } + const runesTransferResponse = makeRpcSuccessResponse<'runes_transfer'>(messageId, { + txid, + }); + sendRpcResponse(tabId, runesTransferResponse); + return txid; + } catch (err) { + const errorResponse = makeRPCError(messageId, { code: RpcErrorCode.INTERNAL_ERROR, - message: 'Failed to broadcast transaction', + message: JSON.stringify((err as any).response.data), }); - sendRpcResponse(+tabId, response); - return; + setTxError(errorResponse.error); + sendRpcResponse(tabId, errorResponse); + } finally { + setIsExecuting(false); } - const runesTransferResponse = makeRpcSuccessResponse<'runes_transfer'>(messageId, { - txid, - }); - sendRpcResponse(+tabId, runesTransferResponse); - return txid; - } catch (err) { - const errorResponse = makeRPCError(messageId, { - code: RpcErrorCode.INTERNAL_ERROR, - message: JSON.stringify((err as any).response.data), - }); - setTxError(errorResponse.error); - sendRpcResponse(+tabId, errorResponse); - } finally { - setIsExecuting(false); - } - }; + }, + [messageId, tabId, transaction], + ); - const cancelRunesTransferRequest = async () => { + const cancelRunesTransferRequest = useCallback(async () => { sendUserRejectionMessage({ tabId, messageId }); - }; + }, [messageId, tabId]); return { transaction, From 53f2b7b8c12841b4c15e76384814edab93fe5b41 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 11:31:40 +0800 Subject: [PATCH 135/219] Disable hook entirely on Testnet --- .../hooks/queries/runes/useRuneFloorPriceQuery.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts index 850286a17..39525660d 100644 --- a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts +++ b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts @@ -9,18 +9,16 @@ export default function useRuneFloorPriceQuery(runeName: string, backgroundRefet const runesApi = useRunesApi(); const queryFn = useCallback( async () => - network.type === 'Mainnet' - ? runesApi - .getRuneMarketData(runeName) - .then((res) => Number(res.floorUnitPrice?.formatted ?? 0)) - : undefined, - [network.type, runeName, runesApi], + runesApi + .getRuneMarketData(runeName) + .then((res) => Number(res.floorUnitPrice?.formatted ?? 0)), + [runeName, runesApi], ); return useQuery({ refetchOnWindowFocus: backgroundRefetch, refetchOnReconnect: backgroundRefetch, queryKey: ['get-rune-floor-price', runeName], - enabled: Boolean(runeName), + enabled: Boolean(runeName) && network.type === 'Mainnet', queryFn, }); } From 5364be145708f39d643705e40b38e9b7d9c8283c Mon Sep 17 00:00:00 2001 From: jordankzf Date: Tue, 30 Jul 2024 11:32:46 +0800 Subject: [PATCH 136/219] Correct fallback for ticker symbol --- src/app/screens/swap/quoteSummary/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index edf2d1efb..ba63b3009 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -192,14 +192,10 @@ export default function QuoteSummary({ const fromUnit = fromToken === 'BTC' ? 'Sats' - : (fromToken as FungibleToken)?.runeSymbol ?? - (fromToken as FungibleToken)?.ticker ?? - RUNE_DISPLAY_DEFAULTS.symbol; + : (fromToken as FungibleToken)?.runeSymbol ?? RUNE_DISPLAY_DEFAULTS.symbol; const toUnit = - toToken?.protocol === 'btc' - ? 'Sats' - : toToken?.symbol ?? toToken?.ticker ?? RUNE_DISPLAY_DEFAULTS.symbol; + toToken?.protocol === 'btc' ? 'Sats' : toToken?.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol; const [showSlippageModal, setShowSlippageModal] = useState(false); const [slippage, setSlippage] = useState(0.05); From 1566b25bc63e4cc2b8c97fd28e87f4d87254c0fe Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Wed, 31 Jul 2024 14:26:34 +0300 Subject: [PATCH 137/219] minor fixes --- src/app/components/confirmBtcTransaction/sendSection.tsx | 5 ++++- src/common/utils/rpc/runes/transfer.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/sendSection.tsx b/src/app/components/confirmBtcTransaction/sendSection.tsx index 10243caa9..670bef849 100644 --- a/src/app/components/confirmBtcTransaction/sendSection.tsx +++ b/src/app/components/confirmBtcTransaction/sendSection.tsx @@ -119,7 +119,10 @@ function SendSection({ - {runeSummary?.transfers.length > index + 1 && } + {/* { + // unnecessary divider, re-add if needed + runeSummary?.transfers.length > index + 1 && + } */} ))} {hasInscriptionsRareSatsInOrdinal && ( diff --git a/src/common/utils/rpc/runes/transfer.ts b/src/common/utils/rpc/runes/transfer.ts index 7fed534e0..4c3c3b2f2 100644 --- a/src/common/utils/rpc/runes/transfer.ts +++ b/src/common/utils/rpc/runes/transfer.ts @@ -1,13 +1,13 @@ import { getTabIdFromPort } from '@common/utils'; import { makeContext, openPopup } from '@common/utils/popup'; -import { transferRunesSchema, type RpcRequestMessage } from '@sats-connect/core'; +import { transferRunesRequestSchema, type RpcRequestMessage } from '@sats-connect/core'; import RoutePaths from 'app/routes/paths'; import * as v from 'valibot'; import { handleInvalidMessage } from '../handle-invalid-message'; import { makeSendPopupClosedUserRejectionMessage } from '../helpers'; const handleTransferRunes = async (message: RpcRequestMessage, port: chrome.runtime.Port) => { - const parseResult = v.safeParse(transferRunesSchema, message); + const parseResult = v.safeParse(transferRunesRequestSchema, message); if (!parseResult.success) { handleInvalidMessage(message, getTabIdFromPort(port), parseResult.issues); From d895213617d140fb7ca287d0325cc3e418367051 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Wed, 31 Jul 2024 15:17:46 +0300 Subject: [PATCH 138/219] remove redundant export --- src/app/screens/transferRunesRequest/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/screens/transferRunesRequest/index.tsx b/src/app/screens/transferRunesRequest/index.tsx index 311a8a5c7..4600ad272 100644 --- a/src/app/screens/transferRunesRequest/index.tsx +++ b/src/app/screens/transferRunesRequest/index.tsx @@ -94,7 +94,7 @@ function TransferRunesRequestInner({ data, context }: TransferRunesRequestInnerP ); } -export function TransferRunesRequest() { +function TransferRunesRequest() { const [error, data] = getPopupPayload(v.parser(transferRunesRequestSchema)); if (error) { throw new Error('Invalid payload'); From 6cb18fee03110638497678f1c42ec7b445a2833a Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Wed, 31 Jul 2024 17:39:42 +0200 Subject: [PATCH 139/219] Add confirm exchange swap --- tests/pages/wallet.ts | 3 +- tests/specs/flowSwapExchange.spec.ts | 20 +--- tests/specs/flowSwapExchangeCancel.spec.ts | 132 +++++++++++++++++++++ 3 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 tests/specs/flowSwapExchangeCancel.spec.ts diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 31b302450..ba07d160b 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -845,7 +845,8 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); // min-received-amount value should be the same as quoteAmount const minReceivedAmount = await this.minReceivedAmount.innerText(); const numericMinReceivedAmount = parseFloat(minReceivedAmount.replace(/[^0-9.]/g, '')); - await expect(numericMinReceivedAmount).toEqual(numericQuoteValue); + const formattedNumericMinReceivedAmount = parseFloat(numericMinReceivedAmount.toFixed(3)); + await expect(formattedNumericMinReceivedAmount).toEqual(numericQuoteValue); // check if quoteAmount is the same from the page before const quoteAmount2Page = await this.quoteAmount.last().innerText(); diff --git a/tests/specs/flowSwapExchange.spec.ts b/tests/specs/flowSwapExchange.spec.ts index 60f40e6a5..4539835aa 100644 --- a/tests/specs/flowSwapExchange.spec.ts +++ b/tests/specs/flowSwapExchange.spec.ts @@ -15,10 +15,10 @@ test.describe('Swap Flow Exchange', () => { }); }); - test('Cancel exchange token via DotSwap', async ({ page, extensionId }) => { + test('Exchange token via DotSwap testnet', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); - await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own BTC & Ordinals Address for address check on review page await wallet.allUpperButtons.nth(1).click(); @@ -51,13 +51,13 @@ test.describe('Swap Flow Exchange', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - await wallet.divTokenRow.first().click(); + await wallet.divTokenRow.filter({ hasText: 'COOK•RUNES•ON•TESTNET' }).click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); // tried a calculated value but had multiple problems with that, for now we stick to a specific value - const swapAmount = 0.00000546; + const swapAmount = 0.00002646; const numericUSDValue = await wallet.fillSwapAmount(swapAmount); @@ -118,15 +118,7 @@ test.describe('Swap Flow Exchange', () => { // Check Rune token name await expect(wallet.nameRune).toContainText(tokenName1); - // Cancel the transaction - await expect(wallet.buttonCancel).toBeEnabled(); - await wallet.buttonCancel.click(); - - // Check Startpage - await wallet.checkVisualsStartpage(); - - // Check BTC Balance after cancel the transaction - const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); - await expect(initialBTCBalance).toEqual(balanceAfterCancel); + await wallet.confirmSendTransaction(); + await wallet.checkVisualsStartpage('testnet'); }); }); diff --git a/tests/specs/flowSwapExchangeCancel.spec.ts b/tests/specs/flowSwapExchangeCancel.spec.ts new file mode 100644 index 000000000..60f40e6a5 --- /dev/null +++ b/tests/specs/flowSwapExchangeCancel.spec.ts @@ -0,0 +1,132 @@ +import { expect, test } from '../fixtures/base'; +import Wallet from '../pages/wallet'; + +test.describe('Swap Flow Exchange', () => { + // Enables the feature flag for Swap + test.beforeEach(async ({ page }) => { + await page.route('https://api-3.xverse.app/v1/app-features', (route) => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + CROSS_CHAIN_SWAPS: { enabled: true }, + }), + }); + }); + }); + + test('Cancel exchange token via DotSwap', async ({ page, extensionId }) => { + // Restore wallet and setup Testnet network + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // get own BTC & Ordinals Address for address check on review page + await wallet.allUpperButtons.nth(1).click(); + const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); + + // Reload the page to close the modal window for the addresses as the X button needs to have a better locator + await page.reload(); + + await wallet.checkVisualsStartpage(); + + // Save initial Balance for later Balance checks + const initialBTCBalance = await wallet.getTokenBalance('Bitcoin'); + + await wallet.allUpperButtons.nth(2).click(); + await wallet.checkVisualsSwapPage(); + + // Select the first Coin + await wallet.buttonDownArrow.nth(0).click(); + + // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded + await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.first()).not.toContainText('Select asset'); + await expect(wallet.imageToken.first()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // Select the second Coin + await wallet.buttonDownArrow.nth(1).click(); + // Had problems with loading of all tokens so I check that a 'DOG' is loaded + await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.last()).not.toContainText('Select asset'); + await expect(wallet.imageToken.last()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // tried a calculated value but had multiple problems with that, for now we stick to a specific value + const swapAmount = 0.00000546; + + const numericUSDValue = await wallet.fillSwapAmount(swapAmount); + + // Save rune token name + const tokenName1 = await wallet.nameToken.last().innerText(); + + await wallet.buttonGetQuotes.click(); + await expect(wallet.nameSwapPlace.first()).toBeVisible(); + await expect(wallet.quoteAmount.first()).toBeVisible(); + await expect(wallet.infoMessage.first()).toBeVisible(); + await expect(wallet.buttonSwapPlace.first()).toBeVisible(); + + const quoteAmount = await wallet.quoteAmount.first().innerText(); + const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toBeGreaterThan(0); + + await wallet.buttonSwapPlace.first().click(); + + await wallet.checkVisualsQuotePage(tokenName1, true, numericQuoteValue, numericUSDValue); + + // We can only continue if the FeeRate is above 0 + await wallet.waitForTextAboveZero(wallet.feeAmount, 30000); + + // Save the current fee amount for comparison + const originalFee = await wallet.feeAmount.innerText(); + const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); + await expect(numericOriginalFee).toBeGreaterThan(0); + + // Click on edit Fee button + await wallet.buttonEditFee.click(); + await expect(wallet.buttonSelectFee.first()).toBeVisible(); + await expect(wallet.labelTotalFee.first()).toBeVisible(); + + // Compare medium fee to previous saved fee + const mediumFee = await wallet.labelTotalFee.last().innerText(); + const numericMediumFee = parseFloat(mediumFee.replace(/[^0-9.]/g, '')); + await expect(numericMediumFee).toBe(numericOriginalFee); + + // Save high fee rate for comparison + const highFee = await wallet.labelTotalFee.first().innerText(); + const numericHighFee = parseFloat(highFee.replace(/[^0-9.]/g, '')); + + // Switch to high fee + await wallet.buttonSelectFee.first().click(); + + const newFee = await wallet.feeAmount.innerText(); + const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); + await expect(numericNewFee).toBe(numericHighFee); + + await wallet.buttonSwap.click(); + await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); + + // second confirm-balance is the same as swapAmount + const swapSendAmount = await wallet.confirmBalance.last().innerText(); + const numericValueSwap = parseFloat(swapSendAmount.replace(/[^0-9.]/g, '')); + await expect(numericValueSwap).toEqual(swapAmount); + + // Check Rune token name + await expect(wallet.nameRune).toContainText(tokenName1); + + // Cancel the transaction + await expect(wallet.buttonCancel).toBeEnabled(); + await wallet.buttonCancel.click(); + + // Check Startpage + await wallet.checkVisualsStartpage(); + + // Check BTC Balance after cancel the transaction + const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); + await expect(initialBTCBalance).toEqual(balanceAfterCancel); + }); +}); From 3423eeae5ef7270607017888894e564cee541355 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 1 Aug 2024 22:49:03 +0800 Subject: [PATCH 140/219] Bump xverse-core --- package-lock.json | 38 +++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5eb40867..fd732bcde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.1.2", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.3.0", + "@secretkeylabs/xverse-core": "18.4.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", @@ -1860,9 +1860,9 @@ } }, "node_modules/@secretkeylabs/xverse-core": { - "version": "18.3.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.3.0/e219dc5f744708b3a66404dfcc9c5c0a1cd54aa4", - "integrity": "sha512-L0Nmaq7olkJJ5pL8+yTWf9PbapHfqGdrU/HQiKYHCX530zbMchRYByZb8E8SdSjsftJ5qrhsF6I0YVXHLYCs/w==", + "version": "18.4.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.4.0/c220930401e6b111095663db49b4466d66b0efe5", + "integrity": "sha512-UrqCBxXGq1rK3pQDw1Bbc8jfeqR4Ix83aHCLy1RKrQI1KR2Ge6BMotBpAW0w0WNS0tOp4wtoCnMelZVvPzSJDA==", "license": "ISC", "dependencies": { "@bitcoinerlab/secp256k1": "^1.0.2", @@ -3463,9 +3463,9 @@ } }, "node_modules/@zondax/ledger-stacks/node_modules/@types/node": { - "version": "18.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.41.tgz", - "integrity": "sha512-LX84pRJ+evD2e2nrgYCHObGWkiQJ1mL+meAgbvnwk/US6vmMY7S2ygBTGV2Jw91s9vUsLSXeDEkUHZIJGLrhsg==", + "version": "18.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", + "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", "dependencies": { "undici-types": "~5.26.4" } @@ -13235,9 +13235,9 @@ "dev": true }, "node_modules/uglify-js": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.0.tgz", - "integrity": "sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==", + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", + "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==", "bin": { "uglifyjs": "bin/uglifyjs" }, @@ -15325,9 +15325,9 @@ } }, "@secretkeylabs/xverse-core": { - "version": "18.3.0", - "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.3.0/e219dc5f744708b3a66404dfcc9c5c0a1cd54aa4", - "integrity": "sha512-L0Nmaq7olkJJ5pL8+yTWf9PbapHfqGdrU/HQiKYHCX530zbMchRYByZb8E8SdSjsftJ5qrhsF6I0YVXHLYCs/w==", + "version": "18.4.0", + "resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/18.4.0/c220930401e6b111095663db49b4466d66b0efe5", + "integrity": "sha512-UrqCBxXGq1rK3pQDw1Bbc8jfeqR4Ix83aHCLy1RKrQI1KR2Ge6BMotBpAW0w0WNS0tOp4wtoCnMelZVvPzSJDA==", "requires": { "@bitcoinerlab/secp256k1": "^1.0.2", "@noble/curves": "^1.2.0", @@ -16656,9 +16656,9 @@ } }, "@types/node": { - "version": "18.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.41.tgz", - "integrity": "sha512-LX84pRJ+evD2e2nrgYCHObGWkiQJ1mL+meAgbvnwk/US6vmMY7S2ygBTGV2Jw91s9vUsLSXeDEkUHZIJGLrhsg==", + "version": "18.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", + "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", "requires": { "undici-types": "~5.26.4" } @@ -23938,9 +23938,9 @@ "dev": true }, "uglify-js": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.0.tgz", - "integrity": "sha512-wNKHUY2hYYkf6oSFfhwwiHo4WCHzHmzcXsqXYTN9ja3iApYIFbb2U6ics9hBcYLHcYGQoAlwnZlTrf3oF+BL/Q==" + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", + "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==" }, "unbox-primitive": { "version": "1.0.2", diff --git a/package.json b/package.json index 053603e1f..9a7d62bf7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@react-spring/web": "^9.6.1", "@sats-connect/core": "0.1.2", "@scure/btc-signer": "1.2.1", - "@secretkeylabs/xverse-core": "18.3.0", + "@secretkeylabs/xverse-core": "18.4.0", "@stacks/connect": "7.4.1", "@stacks/stacks-blockchain-api-types": "6.1.1", "@stacks/transactions": "6.13.1", From 94e47252fdf74394dfee15b0293d6cf0d0e9b7a0 Mon Sep 17 00:00:00 2001 From: jordankzf Date: Thu, 1 Aug 2024 22:49:17 +0800 Subject: [PATCH 141/219] Add MixPanel tracking --- .../components/tokenFromBottomSheet/index.tsx | 9 ++++++++- .../swap/components/tokenToBottomSheet/index.tsx | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 33e53fe9c..7bd4adacc 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -1,7 +1,8 @@ import TokenTile from '@components/tokenTile'; -import type { FungibleToken, Token } from '@secretkeylabs/xverse-core'; +import { AnalyticsEvents, type FungibleToken, type Token } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; +import { trackMixPanel } from '@utils/mixpanel'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import useFromTokens from './useFromTokens'; @@ -43,6 +44,9 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC currency="BTC" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { + token: 'Bitcoin', + }); onClose(); }} /> @@ -56,6 +60,9 @@ export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onC currency="FT" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { + token: token.name, + }); onClose(); }} fungibleToken={token} diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index 6ba0e75c9..51dfa62e2 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -2,11 +2,18 @@ import TokenTile from '@components/tokenTile'; import useDebounce from '@hooks/useDebounce'; import { MagnifyingGlass } from '@phosphor-icons/react'; import { mapFTProtocolToSwapProtocol, mapSwapTokenToFT } from '@screens/swap/utils'; -import type { FungibleToken, Protocol, Token, TokenBasic } from '@secretkeylabs/xverse-core'; +import { + AnalyticsEvents, + type FungibleToken, + type Protocol, + type Token, + type TokenBasic, +} from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; +import { trackMixPanel } from '@utils/mixpanel'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -152,6 +159,9 @@ export default function TokenToBottomSheet({ currency="BTC" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { + token: 'Bitcoin', + }); handleClose(); }} hideBalance @@ -166,6 +176,9 @@ export default function TokenToBottomSheet({ currency="FT" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { + token: token.name ?? token.ticker, + }); handleClose(); }} fungibleToken={mapSwapTokenToFT(token)} From b112dbdb2b813925e45947cf2a42daef2d0cbf59 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Thu, 1 Aug 2024 18:21:15 +0300 Subject: [PATCH 142/219] send rpc response on tx broadcasted --- src/app/screens/confirmBtcTransaction/index.tsx | 4 +++- .../ledger/confirmLedgerTransaction/index.tsx | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/screens/confirmBtcTransaction/index.tsx b/src/app/screens/confirmBtcTransaction/index.tsx index c592d0fd3..6b114460f 100644 --- a/src/app/screens/confirmBtcTransaction/index.tsx +++ b/src/app/screens/confirmBtcTransaction/index.tsx @@ -183,11 +183,13 @@ function ConfirmBtcTransaction() { if (isLedgerAccount(selectedAccount)) { const txType: LedgerTransactionType = 'BTC'; - const state: ConfirmBtcTransactionState = { + const state = { recipients: recipient, type: txType, feeRateInput: currentFeeRate, fee: currentFee, + tabMessageId: requestId, + tabId: +tabId, }; navigate('/confirm-ledger-tx', { state }); diff --git a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx index d741f904b..5b608790c 100644 --- a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx +++ b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx @@ -38,6 +38,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; import useSelectedAccount from '@hooks/useSelectedAccount'; import { ConfirmTxIconBig, @@ -93,6 +94,8 @@ function ConfirmLedgerTransaction(): JSX.Element { ordinalUtxo, feeRateInput, fee, + tabId, + tabMessageId: messageId, }: { amount: BigNumber; recipients: Recipient[] | StacksRecipient[]; @@ -102,6 +105,8 @@ function ConfirmLedgerTransaction(): JSX.Element { feeRateInput?: string; fee?: BigNumber; messageId?: string; + tabId?: number; + tabMessageId?: string; } = location.state; const transition = useTransition(currentStep, DEFAULT_TRANSITION_OPTIONS); @@ -129,6 +134,10 @@ function ConfirmLedgerTransaction(): JSX.Element { const transactionId = await btcClient.sendRawTransaction(txHex || taprootSignedValue); setTxId(transactionId.tx.hash); setCurrentStep(Steps.TransactionConfirmed); + if (tabId) { + const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash }); + sendRpcResponse(tabId, response); + } } catch (err) { console.error(err); setIsTxRejected(true); @@ -153,6 +162,10 @@ function ConfirmLedgerTransaction(): JSX.Element { const transactionId = await btcClient.sendRawTransaction(result); setTxId(transactionId.tx.hash); setCurrentStep(Steps.TransactionConfirmed); + if (tabId) { + const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash }); + sendRpcResponse(tabId, response); + } } catch (err) { console.error(err); setIsTxRejected(true); From 5b84c586d7763e60ab234672040b183567d826c4 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Thu, 1 Aug 2024 18:24:34 +0300 Subject: [PATCH 143/219] version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5eb40867..5a16bd625 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xverse-web-extension", - "version": "0.39.1", + "version": "0.39.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "xverse-web-extension", - "version": "0.39.1", + "version": "0.39.2", "dependencies": { "@aryzing/superqs": "0.0.6", "@ledgerhq/hw-transport-webusb": "^6.27.13", diff --git a/package.json b/package.json index 053603e1f..86ca4a39f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xverse-web-extension", "description": "A Bitcoin wallet for Web3", - "version": "0.39.1", + "version": "0.39.2", "private": true, "engines": { "node": "^18.18.2" From e3a3108afc12023fd827b8b05409e4a770dd5975 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Thu, 1 Aug 2024 17:46:31 +0200 Subject: [PATCH 144/219] Add Swapping flow and rename files --- ...pExchange.spec.ts => swapExchange.spec.ts} | 37 ++---- ...cel.spec.ts => swapExchangeCancel.spec.ts} | 13 +- tests/specs/swapME.spec.ts | 115 ++++++++++++++++++ ...lowSwapME.spec.ts => swapMECancel.spec.ts} | 6 +- .../{flowSwap.spec.ts => swapVisuals.spec.ts} | 2 +- 5 files changed, 139 insertions(+), 34 deletions(-) rename tests/specs/{flowSwapExchange.spec.ts => swapExchange.spec.ts} (75%) rename tests/specs/{flowSwapExchangeCancel.spec.ts => swapExchangeCancel.spec.ts} (93%) create mode 100644 tests/specs/swapME.spec.ts rename tests/specs/{flowSwapME.spec.ts => swapMECancel.spec.ts} (97%) rename tests/specs/{flowSwap.spec.ts => swapVisuals.spec.ts} (98%) diff --git a/tests/specs/flowSwapExchange.spec.ts b/tests/specs/swapExchange.spec.ts similarity index 75% rename from tests/specs/flowSwapExchange.spec.ts rename to tests/specs/swapExchange.spec.ts index 4539835aa..455891352 100644 --- a/tests/specs/flowSwapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -15,7 +15,9 @@ test.describe('Swap Flow Exchange', () => { }); }); - test('Exchange token via DotSwap testnet', async ({ page, extensionId }) => { + const marketplace = 'DotSwap'; + + test('Exchange token via DotSwap with standard fee testnet', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); @@ -29,9 +31,6 @@ test.describe('Swap Flow Exchange', () => { await wallet.checkVisualsStartpage(); - // Save initial Balance for later Balance checks - const initialBTCBalance = await wallet.getTokenBalance('Bitcoin'); - await wallet.allUpperButtons.nth(2).click(); await wallet.checkVisualsSwapPage(); @@ -74,7 +73,8 @@ test.describe('Swap Flow Exchange', () => { const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); await expect(numericQuoteValue).toBeGreaterThan(0); - await wallet.buttonSwapPlace.first().click(); + // Click on DotSwap + await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); await wallet.checkVisualsQuotePage(tokenName1, true, numericQuoteValue, numericUSDValue); @@ -86,32 +86,13 @@ test.describe('Swap Flow Exchange', () => { const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); await expect(numericOriginalFee).toBeGreaterThan(0); - // Click on edit Fee button - await wallet.buttonEditFee.click(); - await expect(wallet.buttonSelectFee.first()).toBeVisible(); - await expect(wallet.labelTotalFee.first()).toBeVisible(); - - // Compare medium fee to previous saved fee - const mediumFee = await wallet.labelTotalFee.last().innerText(); - const numericMediumFee = parseFloat(mediumFee.replace(/[^0-9.]/g, '')); - await expect(numericMediumFee).toBe(numericOriginalFee); - - // Save high fee rate for comparison - const highFee = await wallet.labelTotalFee.first().innerText(); - const numericHighFee = parseFloat(highFee.replace(/[^0-9.]/g, '')); - - // Switch to high fee - await wallet.buttonSelectFee.first().click(); - - const newFee = await wallet.feeAmount.innerText(); - const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); - await expect(numericNewFee).toBe(numericHighFee); - await wallet.buttonSwap.click(); await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); - // second confirm-balance is the same as swapAmount - const swapSendAmount = await wallet.confirmBalance.last().innerText(); + // Confirm Amount is the same as swapAmount + const swapSendAmount = await wallet.confirmAmount + .filter({ hasText: swapAmount.toString() }) + .innerText(); const numericValueSwap = parseFloat(swapSendAmount.replace(/[^0-9.]/g, '')); await expect(numericValueSwap).toEqual(swapAmount); diff --git a/tests/specs/flowSwapExchangeCancel.spec.ts b/tests/specs/swapExchangeCancel.spec.ts similarity index 93% rename from tests/specs/flowSwapExchangeCancel.spec.ts rename to tests/specs/swapExchangeCancel.spec.ts index 60f40e6a5..0c7a2cb78 100644 --- a/tests/specs/flowSwapExchangeCancel.spec.ts +++ b/tests/specs/swapExchangeCancel.spec.ts @@ -15,6 +15,8 @@ test.describe('Swap Flow Exchange', () => { }); }); + const marketplace = 'DotSwap'; + test('Cancel exchange token via DotSwap', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); @@ -74,7 +76,8 @@ test.describe('Swap Flow Exchange', () => { const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); await expect(numericQuoteValue).toBeGreaterThan(0); - await wallet.buttonSwapPlace.first().click(); + // Click on DotSwap + await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); await wallet.checkVisualsQuotePage(tokenName1, true, numericQuoteValue, numericUSDValue); @@ -110,8 +113,12 @@ test.describe('Swap Flow Exchange', () => { await wallet.buttonSwap.click(); await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); - // second confirm-balance is the same as swapAmount - const swapSendAmount = await wallet.confirmBalance.last().innerText(); + await expect(await wallet.confirmAmount.count()).toBeGreaterThan(3); + + // Confirm Amount is the same as swapAmount + const swapSendAmount = await wallet.confirmAmount + .filter({ hasText: swapAmount.toString() }) + .innerText(); const numericValueSwap = parseFloat(swapSendAmount.replace(/[^0-9.]/g, '')); await expect(numericValueSwap).toEqual(swapAmount); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts new file mode 100644 index 000000000..d9d08e3ab --- /dev/null +++ b/tests/specs/swapME.spec.ts @@ -0,0 +1,115 @@ +import { expect, test } from '../fixtures/base'; +import Wallet from '../pages/wallet'; + +test.describe('Swap Flow ME', () => { + // Enables the feature flag for Swap + test.beforeEach(async ({ page }) => { + await page.route('https://api-3.xverse.app/v1/app-features', (route) => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + CROSS_CHAIN_SWAPS: { enabled: true }, + }), + }); + }); + }); + + const marketplace = 'Magic Eden'; + + test('Swap token via ME with standard fee mainnet', async ({ page, extensionId }) => { + // Restore wallet and setup Testnet network + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // get own BTC & Ordinals Address for address check on review page + await wallet.allUpperButtons.nth(1).click(); + const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); + + // Reload the page to close the modal window for the addresses as the X button needs to have a better locator + await page.reload(); + + await wallet.checkVisualsStartpage(); + + await wallet.allUpperButtons.nth(2).click(); + await wallet.checkVisualsSwapPage(); + + // Select the first Coin + await wallet.buttonDownArrow.nth(0).click(); + + // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded + await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.first()).not.toContainText('Select asset'); + await expect(wallet.imageToken.first()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // Select the second Coin + await wallet.buttonDownArrow.nth(1).click(); + // Had problems with loading of all tokens so I check that a 'DOG' is loaded + await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.last()).not.toContainText('Select asset'); + await expect(wallet.imageToken.last()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // tried a calculated value but had multiple problems with that, for now we stick to a specific value + const swapAmount = 0.00000546; + + await wallet.fillSwapAmount(swapAmount); + + // Save rune token name + const tokenName1 = await wallet.nameToken.last().innerText(); + + await wallet.buttonGetQuotes.click(); + await expect(wallet.nameSwapPlace.last()).toBeVisible(); + await expect(wallet.quoteAmount.last()).toBeVisible(); + await expect(wallet.infoMessage.last()).toBeVisible(); + await expect(wallet.buttonSwapPlace.last()).toBeVisible(); + + await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); + await expect(wallet.itemUTXO.first()).toBeVisible(); + + // click only on a UTXO with value from 1000 e(not enough funds for higher) + await wallet.itemUTXO.filter({ hasText: '1,000' }).first().locator('input').click(); + await expect(wallet.buttonNext).toBeVisible(); + await expect(wallet.textUSD).toBeVisible(); + await expect(wallet.quoteAmount).toBeVisible(); + + const quoteAmount = await wallet.quoteAmount.innerText(); + const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toBeGreaterThan(0); + + const usdAmount = await wallet.textUSD.innerText(); + const numericUSDValueSwap = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); + await expect(numericUSDValueSwap).toBeGreaterThan(0); + + await wallet.buttonNext.click(); + + await wallet.checkVisualsQuotePage(tokenName1, false, numericQuoteValue, numericUSDValueSwap); + + // We can only continue if the FeeRate is above 0 + await wallet.waitForTextAboveZero(wallet.feeAmount, 30000); + + // Save the current fee amount for comparison + const originalFee = await wallet.feeAmount.innerText(); + const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); + await expect(numericOriginalFee).toBeGreaterThan(0); + + await wallet.buttonSwap.click(); + await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); + + const sendRuneAmount = await wallet.sendRuneAmount.innerText(); + const sendAmountNumerical = parseFloat(sendRuneAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toEqual(sendAmountNumerical); + + // Check Rune token name + await expect(wallet.nameRune).toContainText(tokenName1); + + await wallet.confirmSendTransaction(); + await wallet.checkVisualsStartpage(); + // TODO: locally check if transaction was successful, might need to wait for transaction to be processed + }); +}); diff --git a/tests/specs/flowSwapME.spec.ts b/tests/specs/swapMECancel.spec.ts similarity index 97% rename from tests/specs/flowSwapME.spec.ts rename to tests/specs/swapMECancel.spec.ts index d65dd3868..43811e95c 100644 --- a/tests/specs/flowSwapME.spec.ts +++ b/tests/specs/swapMECancel.spec.ts @@ -15,6 +15,8 @@ test.describe('Swap Flow ME', () => { }); }); + const marketplace = 'Magic Eden'; + test('Cancel swap token via ME', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); @@ -59,7 +61,7 @@ test.describe('Swap Flow ME', () => { // tried a calculated value but had multiple problems with that, for now we stick to a specific value const swapAmount = 0.00000546; - const numericUSDValue = await wallet.fillSwapAmount(swapAmount); + await wallet.fillSwapAmount(swapAmount); // Save rune token name const tokenName1 = await wallet.nameToken.last().innerText(); @@ -70,7 +72,7 @@ test.describe('Swap Flow ME', () => { await expect(wallet.infoMessage.last()).toBeVisible(); await expect(wallet.buttonSwapPlace.last()).toBeVisible(); - await wallet.buttonSwapPlace.filter({ hasText: 'Magic Eden' }).click(); + await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); await expect(wallet.itemUTXO.first()).toBeVisible(); // click only on a UTXO with value from 1000 e(not enough funds for higher) diff --git a/tests/specs/flowSwap.spec.ts b/tests/specs/swapVisuals.spec.ts similarity index 98% rename from tests/specs/flowSwap.spec.ts rename to tests/specs/swapVisuals.spec.ts index 2c03b13f6..7ab34dfa9 100644 --- a/tests/specs/flowSwap.spec.ts +++ b/tests/specs/swapVisuals.spec.ts @@ -18,7 +18,7 @@ test.describe('Swap Flow Visuals', () => { }); }); - test('Visual check swap page', async ({ page, extensionId }) => { + test('Check swap page', async ({ page, extensionId }) => { const onboardingPage = new Onboarding(page); const wallet = new Wallet(page); await onboardingPage.createWalletSkipBackup(strongPW); From ec9dfe4030242c0ac9f1887b07a980f4217e3f0a Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Thu, 1 Aug 2024 17:48:52 +0200 Subject: [PATCH 145/219] Change copyAddress to be more stable (#442) Switch from retrieving addresses via the copy button and the clipboard to retrieving addresses via the QR code page --- .../components/receiveCardComponent/index.tsx | 4 +- src/app/screens/receive/index.tsx | 4 +- tests/pages/wallet.ts | 44 +++++++++---------- tests/specs/createWallet.spec.ts | 27 +++--------- tests/specs/runesList.spec.ts | 16 ++----- tests/specs/runesListCancel.spec.ts | 12 +---- tests/specs/runesSend.spec.ts | 14 ++---- tests/specs/runesSendCancel.spec.ts | 8 +--- .../specs/tabCollectiblesInscriptions.spec.ts | 12 +---- tests/specs/transactionBTC.spec.ts | 18 ++------ tests/specs/transactionSTX.spec.ts | 6 +-- 11 files changed, 47 insertions(+), 118 deletions(-) diff --git a/src/app/components/receiveCardComponent/index.tsx b/src/app/components/receiveCardComponent/index.tsx index 18bef79e2..b138c4dc7 100644 --- a/src/app/components/receiveCardComponent/index.tsx +++ b/src/app/components/receiveCardComponent/index.tsx @@ -118,7 +118,7 @@ function ReceiveCardComponent({ {children} {title} - + {showVerifyButton ? addressText : getShortTruncatedAddress(address)} @@ -147,7 +147,7 @@ function ReceiveCardComponent({ place="top" hidden={!isCopied} /> - diff --git a/src/app/screens/receive/index.tsx b/src/app/screens/receive/index.tsx index 7b7c09f88..71429e5bd 100644 --- a/src/app/screens/receive/index.tsx +++ b/src/app/screens/receive/index.tsx @@ -162,7 +162,7 @@ function Receive() { {renderData[currency].title} {renderData[currency].desc} - + {showBnsName && {selectedAccount?.bnsName}} - {renderData[currency].address} + {renderData[currency].address} diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index ab8c21a7f..5b9ccb5dd 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -40,14 +40,6 @@ export default class Wallet { readonly buttonDenyDataCollection: Locator; - readonly buttonCopyBitcoinAddress: Locator; - - readonly buttonCopyOrdinalsAddress: Locator; - - readonly buttonCopyStacksAddress: Locator; - - readonly buttonConfirmCopyAddress: Locator; - readonly buttonNetwork: Locator; readonly buttonSave: Locator; @@ -328,6 +320,12 @@ export default class Wallet { readonly buttonInsufficientFunds: Locator; + readonly buttonQRAddress: Locator; + + readonly labelAddress: Locator; + + readonly containerQRCode: Locator; + constructor(readonly page: Page) { this.page = page; this.navigationDashboard = page.getByTestId('nav-dashboard'); @@ -454,14 +452,9 @@ export default class Wallet { this.divAppTitle = page.getByTestId('app-title'); // Receive - this.buttonCopyBitcoinAddress = page.locator('#copy-address-Bitcoin'); - this.buttonCopyOrdinalsAddress = page.locator( - '#copy-address-Ordinals\\,\\ BRC-20\\ \\&\\ Runes', - ); - this.buttonCopyStacksAddress = page.locator( - '#copy-address-Stacks\\ NFTs\\ \\&\\ SIP-10\\ tokens', - ); - this.buttonConfirmCopyAddress = page.getByRole('button', { name: 'I understand' }); + this.buttonQRAddress = page.getByTestId('qr-button'); + this.labelAddress = page.getByTestId('address-label'); + this.containerQRCode = page.getByTestId('qr-container'); // Swap this.buttonSelectCoin = page.getByTestId('select-coin-button'); @@ -803,16 +796,19 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await this.buttonClose.click(); } - async getAddress(button: Locator, ClickConfirm = true): Promise { - await expect(button).toBeVisible(); - await button.click(); + async getAddress(whichAddress): Promise { + // click on 'Receive' button + await this.allupperButtons.nth(1).click(); - if (ClickConfirm) { - await expect(this.buttonConfirmCopyAddress).toBeVisible(); - await this.buttonConfirmCopyAddress.click(); - } + // Need to click on the QR Code button to get the full Address + + await this.buttonQRAddress.nth(whichAddress).click(); + await expect(this.containerQRCode).toBeVisible(); + + const address = await this.labelAddress.innerText(); + + await this.buttonBack.click(); - const address = await this.page.evaluate('navigator.clipboard.readText()'); return address; } diff --git a/tests/specs/createWallet.spec.ts b/tests/specs/createWallet.spec.ts index ac2fa15fa..2fedefd04 100644 --- a/tests/specs/createWallet.spec.ts +++ b/tests/specs/createWallet.spec.ts @@ -75,20 +75,10 @@ test.describe('Create and Restore Wallet Flow', () => { await expect(newWallet.balance).toHaveText('$0.00'); - // TODO: find better selector for the receive button - await newWallet.allupperButtons.nth(1).click(); - // Get the addresses and save it in variables - const addressBitcoin = await newWallet.getAddress(newWallet.buttonCopyBitcoinAddress); - const addressOrdinals = await newWallet.getAddress(newWallet.buttonCopyOrdinalsAddress); - const addressStack = await newWallet.getAddress(newWallet.buttonCopyStacksAddress, false); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); - // click close for the modal window - // TODO: find better locator for close button --> issue https://linear.app/xverseapp/issue/ENG-4039/adjust-id-or-add-titles-for-copy-address-button-for-receive-menu - // await expect(page.locator('button.sc-hceviv > svg')).toBeVisible(); - // await page.locator('button.sc-hceviv > svg').click(); + const addressBitcoin = await newWallet.getAddress(0); + const addressOrdinals = await newWallet.getAddress(1); + const addressStack = await newWallet.getAddress(2); // Save the Address in a file so that other tests can access them const dataAddress = JSON.stringify({ @@ -149,15 +139,10 @@ test.describe('Create and Restore Wallet Flow', () => { const balanceText = newWallet.balance; await await expect(balanceText).toHaveText('$0.00'); - await newWallet.allupperButtons.nth(1).click(); - // Get the Addresses - const addressBitcoinCheck = await newWallet.getAddress(newWallet.buttonCopyBitcoinAddress); - const addressOrdinalsCheck = await newWallet.getAddress(newWallet.buttonCopyOrdinalsAddress); - const addressStackCheck = await newWallet.getAddress( - newWallet.buttonCopyStacksAddress, - false, - ); + const addressBitcoinCheck = await newWallet.getAddress(0); + const addressOrdinalsCheck = await newWallet.getAddress(1); + const addressStackCheck = await newWallet.getAddress(2); // Read and parse the file const rawData = fs.readFileSync(filePathAddresses, 'utf8'); diff --git a/tests/specs/runesList.spec.ts b/tests/specs/runesList.spec.ts index ffe18a1d8..c8ca824e6 100644 --- a/tests/specs/runesList.spec.ts +++ b/tests/specs/runesList.spec.ts @@ -13,12 +13,8 @@ test.describe('List runes', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - const selfOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTC = await wallet.getAddress(0); + const selfOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); @@ -150,12 +146,8 @@ test.describe('List runes', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - const selfOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTC = await wallet.getAddress(0); + const selfOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); diff --git a/tests/specs/runesListCancel.spec.ts b/tests/specs/runesListCancel.spec.ts index f56abb540..7919158ec 100644 --- a/tests/specs/runesListCancel.spec.ts +++ b/tests/specs/runesListCancel.spec.ts @@ -10,11 +10,7 @@ test.describe('Cancel runes listing', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); @@ -76,11 +72,7 @@ test.describe('Cancel runes listing', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); diff --git a/tests/specs/runesSend.spec.ts b/tests/specs/runesSend.spec.ts index 7721f114e..4d8cf4785 100644 --- a/tests/specs/runesSend.spec.ts +++ b/tests/specs/runesSend.spec.ts @@ -12,11 +12,7 @@ test.describe('Send runes', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const addressOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const addressOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune('SKIBIDI•OHIO•RIZZ'); @@ -50,12 +46,8 @@ test.describe('Send runes', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - const addressOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTC = await wallet.getAddress(0); + const addressOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); diff --git a/tests/specs/runesSendCancel.spec.ts b/tests/specs/runesSendCancel.spec.ts index 3ccd3c8a6..45681e275 100644 --- a/tests/specs/runesSendCancel.spec.ts +++ b/tests/specs/runesSendCancel.spec.ts @@ -12,12 +12,8 @@ test.describe('Send runes', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - const addressOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTC = await wallet.getAddress(0); + const addressOrdinals = await wallet.getAddress(1); // Check if Rune is enabled and if not enable the rune and click on it await wallet.checkAndClickOnSpecificRune(runeName); diff --git a/tests/specs/tabCollectiblesInscriptions.spec.ts b/tests/specs/tabCollectiblesInscriptions.spec.ts index abbe40c13..a96cfd558 100644 --- a/tests/specs/tabCollectiblesInscriptions.spec.ts +++ b/tests/specs/tabCollectiblesInscriptions.spec.ts @@ -9,11 +9,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const addressOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const addressOrdinals = await wallet.getAddress(1); // Navigate to Collectibles tab await wallet.navigateToCollectibles(); @@ -166,11 +162,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const addressOrdinals = await wallet.getAddress(wallet.buttonCopyOrdinalsAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const addressOrdinals = await wallet.getAddress(1); // Navigate to Collectibles tab await wallet.navigateToCollectibles(); diff --git a/tests/specs/transactionBTC.spec.ts b/tests/specs/transactionBTC.spec.ts index 1ebc5af28..35de66d07 100644 --- a/tests/specs/transactionBTC.spec.ts +++ b/tests/specs/transactionBTC.spec.ts @@ -13,11 +13,7 @@ test.describe('Transaction BTC', () => { await wallet.setupTest(extensionId, 'SEED_WORDS2', false); // get own BTC & Ordinals Address for address check on review page - await wallet.allupperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTC = await wallet.getAddress(0); // Save initial Balance for later Balance checks const initalBTCBalance = await wallet.getTokenBalance('Bitcoin'); @@ -63,11 +59,7 @@ test.describe('Transaction BTC', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own BTC Address - await wallet.allupperButtons.nth(1).click(); - const selfBTCTest = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTCTest = await wallet.getAddress(0); // Save initial Balance for later Balance checks const initalBTCBalance = await wallet.getTokenBalance('Bitcoin'); @@ -128,11 +120,7 @@ test.describe('Transaction BTC', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', true); // get own BTC Address - await wallet.allupperButtons.nth(1).click(); - const selfBTCTest = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfBTCTest = await wallet.getAddress(0); // Save initial Balance for later Balance checks const initalBTCBalance = await wallet.getTokenBalance('Bitcoin'); diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 4742c91bb..19832bb7b 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -11,11 +11,7 @@ test.describe('Transaction STX', () => { await wallet.setupTest(extensionId, 'SEED_WORDS1', false); // get own STX Address - await wallet.allupperButtons.nth(1).click(); - const selfSTXMain = await wallet.getAddress(wallet.buttonCopyStacksAddress, false); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + const selfSTXMain = await wallet.getAddress(2); await wallet.checkVisualsStartpage(); From 014bdbb6681b13419ab2731bcc7a0b8e2088fd0c Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Thu, 1 Aug 2024 17:54:40 +0200 Subject: [PATCH 146/219] Adjust to new getAddress function --- tests/pages/wallet.ts | 2 +- tests/specs/swapExchange.spec.ts | 8 ++------ tests/specs/swapExchangeCancel.spec.ts | 8 ++------ tests/specs/swapME.spec.ts | 8 ++------ tests/specs/swapMECancel.spec.ts | 8 ++------ 5 files changed, 9 insertions(+), 25 deletions(-) diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 2f47dc6b4..dcfb76fa1 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -930,7 +930,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await this.allUpperButtons.nth(1).click(); // Need to click on the QR Code button to get the full Address - + // TODO change it to name of the Address instead of index await this.buttonQRAddress.nth(whichAddress).click(); await expect(this.containerQRCode).toBeVisible(); diff --git a/tests/specs/swapExchange.spec.ts b/tests/specs/swapExchange.spec.ts index 455891352..2a0ce6274 100644 --- a/tests/specs/swapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -22,12 +22,8 @@ test.describe('Swap Flow Exchange', () => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); - // get own BTC & Ordinals Address for address check on review page - await wallet.allUpperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + // get own BTC Address + const selfBTC = await wallet.getAddress(0); await wallet.checkVisualsStartpage(); diff --git a/tests/specs/swapExchangeCancel.spec.ts b/tests/specs/swapExchangeCancel.spec.ts index 0c7a2cb78..9bbbe1d93 100644 --- a/tests/specs/swapExchangeCancel.spec.ts +++ b/tests/specs/swapExchangeCancel.spec.ts @@ -22,12 +22,8 @@ test.describe('Swap Flow Exchange', () => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // get own BTC & Ordinals Address for address check on review page - await wallet.allUpperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + // get own BTC Address + const selfBTC = await wallet.getAddress(0); await wallet.checkVisualsStartpage(); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts index d9d08e3ab..1cda00ae5 100644 --- a/tests/specs/swapME.spec.ts +++ b/tests/specs/swapME.spec.ts @@ -22,12 +22,8 @@ test.describe('Swap Flow ME', () => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // get own BTC & Ordinals Address for address check on review page - await wallet.allUpperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + // get own BTC Address + const selfBTC = await wallet.getAddress(0); await wallet.checkVisualsStartpage(); diff --git a/tests/specs/swapMECancel.spec.ts b/tests/specs/swapMECancel.spec.ts index 43811e95c..241bfaa71 100644 --- a/tests/specs/swapMECancel.spec.ts +++ b/tests/specs/swapMECancel.spec.ts @@ -22,12 +22,8 @@ test.describe('Swap Flow ME', () => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); - // get own BTC & Ordinals Address for address check on review page - await wallet.allUpperButtons.nth(1).click(); - const selfBTC = await wallet.getAddress(wallet.buttonCopyBitcoinAddress); - - // Reload the page to close the modal window for the addresses as the X button needs to have a better locator - await page.reload(); + // get own BTC Address + const selfBTC = await wallet.getAddress(0); await wallet.checkVisualsStartpage(); From a7794b462c16b5bd7f45e580f00d84400aaac6ab Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Thu, 1 Aug 2024 18:07:28 +0200 Subject: [PATCH 147/219] Spelling --- src/app/components/sendForm/index.tsx | 2 +- tests/pages/wallet.ts | 43 ++++++++------------------- tests/specs/createWallet.spec.ts | 26 ++++++++-------- tests/specs/runesSend.spec.ts | 2 +- tests/specs/runesSendCancel.spec.ts | 2 +- tests/specs/swapExchange.spec.ts | 2 +- tests/specs/tabSettings.spec.ts | 2 +- tests/specs/transactionBTC.spec.ts | 14 ++++----- tests/specs/transactionSTX.spec.ts | 4 +-- 9 files changed, 40 insertions(+), 57 deletions(-) diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index 87389a14b..2abbd8774 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -277,7 +277,7 @@ function SendForm({ { test('create and restore a wallet via Menu', async ({ page, extensionId, context }) => { const onboardingPage = new Onboarding(page); const wallet = new Wallet(page); - await test.step('backup seedphrase and successfully create a wallet', async () => { + await test.step('backup seedPhrase and successfully create a wallet', async () => { await onboardingPage.navigateToBackupPage(); await onboardingPage.buttonBackupNow.click(); await expect(page.url()).toContain('backupWalletSteps'); @@ -30,34 +30,34 @@ test.describe('Create and Restore Wallet Flow', () => { // check if 12 words are displayed await expect(onboardingPage.buttonSeedWords).toHaveCount(12); await expect(onboardingPage.secondParagraphBackupStep).toBeVisible(); - let seedword = await onboardingPage.selectSeedWord(seedWords); + let seedWord = await onboardingPage.selectSeedWord(seedWords); - // Save the seedwords into a file to read it out later to restore + // Save the seedWords into a file to read it out later to restore fs.writeFileSync(filePathSeedWords, JSON.stringify(seedWords), 'utf8'); - // get all displayed values and filter the value from the actual seedphrase out to do an error message check + // get all displayed values and filter the value from the actual seedPhrase out to do an error message check const buttonValues = await onboardingPage.buttonSeedWords.evaluateAll((buttons) => buttons.map((button) => { // Assert that the button is an HTMLButtonElement to access the `value` property if (button instanceof HTMLButtonElement) { return button.value; } - return 'testvalue'; + return 'testValue'; }), ); - const filteredValues = buttonValues.filter((value) => value !== seedword); + const filteredValues = buttonValues.filter((value) => value !== seedWord); const randomValue = filteredValues[Math.floor(Math.random() * filteredValues.length)]; await page.locator(`button[value="${randomValue}"]`).first().click(); - // Check if error message is displayed when clicking the wrong seedword + // Check if error message is displayed when clicking the wrong seedWord await expect(page.locator('p:has-text("This word is not")')).toBeVisible(); - await page.locator(`button[value="${seedword}"]`).click(); - seedword = await onboardingPage.selectSeedWord(seedWords); - await page.locator(`button[value="${seedword}"]`).click(); - seedword = await onboardingPage.selectSeedWord(seedWords); - await page.locator(`button[value="${seedword}"]`).click(); + await page.locator(`button[value="${seedWord}"]`).click(); + seedWord = await onboardingPage.selectSeedWord(seedWords); + await page.locator(`button[value="${seedWord}"]`).click(); + seedWord = await onboardingPage.selectSeedWord(seedWords); + await page.locator(`button[value="${seedWord}"]`).click(); await onboardingPage.inputPassword.fill(strongPW); await onboardingPage.buttonContinue.click(); diff --git a/tests/specs/runesSend.spec.ts b/tests/specs/runesSend.spec.ts index 245e30736..3ede90de9 100644 --- a/tests/specs/runesSend.spec.ts +++ b/tests/specs/runesSend.spec.ts @@ -91,7 +91,7 @@ test.describe('Send runes', () => { await wallet.confirmSendTransaction(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await wallet.checkAndClickOnSpecificRune(runeName); const BalanceAmount = await wallet.checkVisualsRunesDashboard(runeName); diff --git a/tests/specs/runesSendCancel.spec.ts b/tests/specs/runesSendCancel.spec.ts index c327e6acb..ec4271c9f 100644 --- a/tests/specs/runesSendCancel.spec.ts +++ b/tests/specs/runesSendCancel.spec.ts @@ -58,7 +58,7 @@ test.describe('Send runes', () => { await wallet.buttonCancel.click(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await wallet.checkAndClickOnSpecificRune(runeName); const BalanceAmount = await wallet.checkVisualsRunesDashboard(runeName); diff --git a/tests/specs/swapExchange.spec.ts b/tests/specs/swapExchange.spec.ts index 2a0ce6274..5b8fc5be5 100644 --- a/tests/specs/swapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -96,6 +96,6 @@ test.describe('Swap Flow Exchange', () => { await expect(wallet.nameRune).toContainText(tokenName1); await wallet.confirmSendTransaction(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); }); }); diff --git a/tests/specs/tabSettings.spec.ts b/tests/specs/tabSettings.spec.ts index 08b9c2f65..17f3224cb 100644 --- a/tests/specs/tabSettings.spec.ts +++ b/tests/specs/tabSettings.spec.ts @@ -51,7 +51,7 @@ test.describe('Settings Tab', () => { await wallet.labelAccountName.click(); await expect(page.url()).toContain('account-list'); await wallet.labelAccountName.last().click(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 2'); await page.goto(`chrome-extension://${extensionId}/popup.html#/settings`); await wallet.switchToMainnetNetwork(); diff --git a/tests/specs/transactionBTC.spec.ts b/tests/specs/transactionBTC.spec.ts index b37893dd1..0b4716f48 100644 --- a/tests/specs/transactionBTC.spec.ts +++ b/tests/specs/transactionBTC.spec.ts @@ -28,17 +28,17 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.inputBTCAdress.fill(`Test Address 123`); + await wallet.inputBTCAddress.fill(`Test Address 123`); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.errorMessageAddressInvalid).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in own Address to check info message - await wallet.inputBTCAdress.fill(selfBTC); + await wallet.inputBTCAddress.fill(selfBTC); await expect(wallet.buttonNext).toBeEnabled(); await expect(wallet.infoMessageSendSelf).toBeVisible(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCMain); + await wallet.inputBTCAddress.fill(BTCMain); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); // Check visuals from 2 page (send BTC) @@ -73,7 +73,7 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCTest); + await wallet.inputBTCAddress.fill(BTCTest); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.containerFeeRate).toBeVisible(); @@ -107,7 +107,7 @@ test.describe('Transaction BTC', () => { await wallet.buttonCancel.click(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check BTC Balance after cancel the transaction const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); @@ -134,7 +134,7 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCTest); + await wallet.inputBTCAddress.fill(BTCTest); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.containerFeeRate).toBeVisible(); @@ -160,7 +160,7 @@ test.describe('Transaction BTC', () => { await wallet.checkAmountsSendingBTC(selfBTCTest, BTCTest, amountBTCSend); await wallet.confirmSendTransaction(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check BTC Balance after the transaction const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 7b01adbf3..8070264ac 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -96,7 +96,7 @@ test.describe('Transaction STX', () => { await wallet.buttonCancel.click(); // Check startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check STX Balance after cancel the transaction const balanceAfterCancel = await wallet.getTokenBalance('Stacks'); @@ -152,7 +152,7 @@ test.describe('Transaction STX', () => { // Confirm the transaction await wallet.confirmSendTransaction(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Can't check amounts or transaction as E2E test is faster than the UI or API to how that transaction --> has to be checked manually }); From 308d116ef0f28795be87b34cc79902f91ad94882 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Fri, 2 Aug 2024 10:49:20 +0200 Subject: [PATCH 148/219] adjust localexecution --- tests/specs/swapExchange.spec.ts | 5 ++++- tests/specs/swapME.spec.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/specs/swapExchange.spec.ts b/tests/specs/swapExchange.spec.ts index 5b8fc5be5..0e3b0de63 100644 --- a/tests/specs/swapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -17,7 +17,10 @@ test.describe('Swap Flow Exchange', () => { const marketplace = 'DotSwap'; - test('Exchange token via DotSwap with standard fee testnet', async ({ page, extensionId }) => { + test('Exchange token via DotSwap with standard fee testnet #localexecution', async ({ + page, + extensionId, + }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts index 1cda00ae5..c6c864c56 100644 --- a/tests/specs/swapME.spec.ts +++ b/tests/specs/swapME.spec.ts @@ -17,7 +17,10 @@ test.describe('Swap Flow ME', () => { const marketplace = 'Magic Eden'; - test('Swap token via ME with standard fee mainnet', async ({ page, extensionId }) => { + test('Swap token via ME with standard fee mainnet #localexecution', async ({ + page, + extensionId, + }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); From 5b3d204a015a573c32c494eacabaa7d2334839cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Fri, 2 Aug 2024 15:02:49 +0200 Subject: [PATCH 149/219] Extract origin from url params for legacy requests (#451) --- .../connect/btcSelectAddressScreen/useBtcAddressRequest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts b/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts index 3ab294410..e83224b77 100644 --- a/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts +++ b/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts @@ -143,6 +143,7 @@ export default function useRequestHelper(): UseRequestHelperReturn { { const params = new URLSearchParams(search); const token = params.get('addressRequest') ?? ''; + const origin = params.get('origin') ?? ''; let legacyRequestNetworkType; let request: GetAddressOptions | undefined; if (token) { From ab0520929ffc714148aea15f523a26a217d8f4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduard=20Bardaj=C3=AD=20Puig?= Date: Fri, 2 Aug 2024 23:52:19 +0200 Subject: [PATCH 150/219] Expand connection screen permissions list (#453) --- src/app/screens/connectionRequest/index.tsx | 2 ++ src/locales/en.json | 1 + 2 files changed, 3 insertions(+) diff --git a/src/app/screens/connectionRequest/index.tsx b/src/app/screens/connectionRequest/index.tsx index e35f09e35..06646c88a 100644 --- a/src/app/screens/connectionRequest/index.tsx +++ b/src/app/screens/connectionRequest/index.tsx @@ -93,6 +93,8 @@ function ConnectionRequestInner({ data, context }: ConnectionRequestInnerProps) + + diff --git a/src/locales/en.json b/src/locales/en.json index f5d85e940..2c59aaf8f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -590,6 +590,7 @@ "PERMISSIONS_TITLE": "The app will be able to:", "PERMISSION_WALLET_BALANCE": "See your wallet balance and activity", "PERMISSION_REQUEST_TX": "Request transaction signing", + "PERMISSION_WALLET_TYPE_ACCESS": "See which type of wallet you are using (software or hardware wallet)", "NO_STACKS_AUTH_SUPPORT": { "TITLE": "This wallet does not have a Stacks address.", "LINK": "Create a Stacks address" From 5f00aab4b215d839002758a7a851c4e51e7679fe Mon Sep 17 00:00:00 2001 From: Den <36603049+dhriaznov@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:27:04 +0200 Subject: [PATCH 151/219] [ENG-4347] SEND BRC-20 Transaction Review Screen - New Design (#439) --- .../hooks/useParsedTxSummaryContext.ts | 8 +- .../confirmBtcTransaction/index.tsx | 6 +- .../itemRow/inscription.tsx | 77 +++- .../itemRow/rareSats.tsx | 8 +- .../confirmBtcTransactionComponent/bundle.tsx | 55 ++- .../confirmBtcTransactionComponent/index.tsx | 3 +- src/app/components/sendForm/index.tsx | 2 +- src/app/components/tokenImage/index.tsx | 8 +- src/app/components/transferFeeView/index.tsx | 2 +- .../queries/ordinals/useAddressInscription.ts | 2 +- src/app/hooks/useResetUserFlow.ts | 1 - src/app/hooks/useTextOrdinalContent.ts | 8 +- src/app/routes/index.tsx | 9 - .../confirmBrc20Transaction/amountRow.tsx | 42 +- .../brc20FeesComponent.tsx | 105 ++--- .../confirmBrc20Transaction/editFees.tsx | 289 ------------ .../confirmBrc20Transaction/index.styled.ts | 80 ++++ .../screens/confirmBrc20Transaction/index.tsx | 314 ++++--------- .../confirmBrc20Transaction/recipientCard.tsx | 66 +-- .../confirmOrdinalTransaction/index.tsx | 8 +- src/app/screens/listRune/listRuneItem.tsx | 2 +- src/app/screens/listRune/setRunePriceItem.tsx | 8 +- src/app/screens/ordinalDetail/index.styled.ts | 337 ++++++++++++++ src/app/screens/ordinalDetail/index.tsx | 428 +++--------------- .../screens/rareSatsBundle/index.styled.ts | 136 ++++++ src/app/screens/rareSatsBundle/index.tsx | 183 ++------ src/app/screens/sendBrc20OneStep/index.tsx | 48 +- src/app/screens/sendOrdinal/index.tsx | 57 ++- src/app/screens/sendOrdinal/stepDisplay.tsx | 42 +- src/app/screens/sendRareSat/index.tsx | 260 ----------- src/locales/en.json | 3 +- tests/pages/wallet.ts | 46 +- tests/specs/runesSend.spec.ts | 4 +- tests/specs/runesSendCancel.spec.ts | 4 +- .../specs/tabCollectiblesInscriptions.spec.ts | 14 +- tests/specs/tabCollectiblesRareSats.spec.ts | 49 +- tests/specs/tabSettings.spec.ts | 6 +- tests/specs/transactionBTC.spec.ts | 14 +- tests/specs/transactionSTX.spec.ts | 4 +- 39 files changed, 1147 insertions(+), 1591 deletions(-) delete mode 100644 src/app/screens/confirmBrc20Transaction/editFees.tsx create mode 100644 src/app/screens/confirmBrc20Transaction/index.styled.ts create mode 100644 src/app/screens/ordinalDetail/index.styled.ts create mode 100644 src/app/screens/rareSatsBundle/index.styled.ts delete mode 100644 src/app/screens/sendRareSat/index.tsx diff --git a/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts b/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts index f19989123..fec8b4bdd 100644 --- a/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts +++ b/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts @@ -9,20 +9,24 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; import { + type Brc20Definition, type btcTransaction, type RuneSummary, type RuneSummaryActions, } from '@secretkeylabs/xverse-core'; import { createContext, useContext } from 'react'; +import type { Color } from 'theme'; export type ParsedTxSummaryContextProps = { summary?: TransactionSummary | btcTransaction.PsbtSummary; runeSummary?: RuneSummary | RuneSummaryActions; + brc20Summary?: Brc20Definition & { status: string; statusColor: Color }; }; export const ParsedTxSummaryContext = createContext({ summary: undefined, runeSummary: undefined, + brc20Summary: undefined, }); export const useParsedTxSummaryContext = (): { @@ -31,6 +35,7 @@ export const useParsedTxSummaryContext = (): { | btcTransaction.PsbtSummary | undefined; runeSummary: RuneSummary | RuneSummaryActions | undefined; + brc20Summary: (Brc20Definition & { status: string; statusColor: Color }) | undefined; hasExternalInputs: boolean; isUnconfirmedInput: boolean; showCenotaphCallout: boolean; @@ -67,7 +72,7 @@ export const useParsedTxSummaryContext = (): { paymentRuneReceipts: RuneSummary['receipts']; }; } => { - const { summary, runeSummary } = useContext(ParsedTxSummaryContext); + const { summary, runeSummary, brc20Summary } = useContext(ParsedTxSummaryContext); const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { hasActivatedRareSatsKey } = useWalletSelector(); @@ -177,6 +182,7 @@ export const useParsedTxSummaryContext = (): { return { summary, runeSummary, + brc20Summary, hasExternalInputs, hasInsufficientRunes, hasOutputScript, diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index d7bbf9075..83c017100 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -51,6 +51,7 @@ const SuccessActionsContainer = styled.div((props) => ({ type Props = { summary?: ParsedTxSummaryContextProps['summary']; runeSummary?: ParsedTxSummaryContextProps['runeSummary']; + brc20Summary?: ParsedTxSummaryContextProps['brc20Summary']; isLoading: boolean; isSubmitting: boolean; isBroadcast?: boolean; @@ -93,10 +94,11 @@ function ConfirmBtcTransaction({ feeRate, title, selectedBottomTab, + brc20Summary, }: Props) { const parsedTxSummaryContextValue = useMemo( - () => ({ summary, runeSummary }), - [summary, runeSummary], + () => ({ summary, runeSummary, brc20Summary }), + [summary, runeSummary, brc20Summary], ); const [isModalVisible, setIsModalVisible] = useState(false); const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger); diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx index 786524f75..6a2022555 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/inscription.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import Theme from 'theme'; +import { useParsedTxSummaryContext } from '../hooks/useParsedTxSummaryContext'; type Props = { inscription: btcTransaction.IOInscription; @@ -44,6 +45,10 @@ const ContentType = styled(StyledP)` word-break: break-word; `; +const Dot = styled(StyledP)` + margin: 0 ${(props) => props.theme.space.xxs}; +`; + export default function Inscription({ inscription, bundleSize, @@ -51,6 +56,7 @@ export default function Inscription({ onShowInscription, }: Props) { const { t } = useTranslation('translation'); + const { brc20Summary } = useParsedTxSummaryContext(); return ( @@ -93,28 +99,59 @@ export default function Inscription({ )} - - { - onShowInscription(inscription); - }} - > + {brc20Summary ? ( + - - {inscription.number} - - + + BRC20 {brc20Summary.op.charAt(0).toUpperCase() + brc20Summary.op.slice(1)} + + {!!brc20Summary.status && ( + <> + + · + + + {brc20Summary.status} + + + )} - - - {inscription.contentType} - - + ( + + {text} + + )} + suffix={` ${brc20Summary.tick}`} + /> + + ) : ( + + { + onShowInscription(inscription); + }} + > + + + {inscription.number} + + + + + + {inscription.contentType} + + + )} ); diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx index 68a3032c4..361a79ada 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -84,9 +84,13 @@ function RareSats({ } /> - {`${ + {`${ satributesInfo.totalExoticSats - } ${t('NFT_DASHBOARD_SCREEN.RARE_SATS')}`} + } ${t( + satributesInfo.totalExoticSats > 1 + ? 'NFT_DASHBOARD_SCREEN.RARE_SATS' + : 'NFT_DASHBOARD_SCREEN.RARE_SAT', + )}`} {bundleSize && ( ` - margin-top: ${(props) => (props.addMargin ? props.theme.space.m : 0)}; +const BundleItemsContainer = styled.div<{ + $withMargin: boolean; +}>` + margin-top: ${(props) => (props.$withMargin ? props.theme.space.m : 0)}; `; const SatsBundleContainer = styled.div` @@ -45,21 +43,31 @@ const BundleTitle = styled(StyledP)` margin-left: ${(props) => props.theme.space.s}; `; -const BundleValue = styled(StyledP)` - margin-right: ${(props) => props.theme.space.xs}; -`; - const Title = styled(StyledP)((props) => ({ marginBottom: props.theme.space.xs, })); +const IconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + background-color: ${(props) => props.theme.colors.white_0}; + border-radius: 50%; + padding: 6px; +`; + function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { + const { t } = useTranslation('translation'); + const [showBundleDetail, setShowBundleDetail] = useState(false); const [inscriptionToShow, setInscriptionToShow] = useState( undefined, ); - const { t } = useTranslation('translation'); + const arrowRotation = useSpring({ + transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', + config: { ...config.stiff }, + }); return ( <> @@ -76,22 +84,27 @@ function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { onClick={() => setShowBundleDetail((prevState) => !prevState)} > - bundle - - {t('RARE_SATS.SATS_BUNDLE')} + + + + + {`${bundle.totalExoticSats} ${t( + bundle.totalExoticSats > 1 + ? 'NFT_DASHBOARD_SCREEN.RARE_SATS' + : 'NFT_DASHBOARD_SCREEN.RARE_SAT', + )}`} - {`${bundle.totalExoticSats} ${t( - 'NFT_DASHBOARD_SCREEN.RARE_SATS', - )}`} - + + + {showBundleDetail && bundle.satRanges.map((item: BundleSatRange, index: number) => ( - + { diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 762e9f113..66a4c47d6 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -380,7 +380,6 @@ function ConfirmBtcTransactionComponent({ )} - {currencyType !== 'BTC' && bundle && } {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} @@ -410,6 +409,8 @@ function ConfirmBtcTransactionComponent({ )) )} + {currencyType !== 'BTC' && bundle && } + {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index 87389a14b..2abbd8774 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -277,7 +277,7 @@ function SendForm({ ((props) => ({ - width: props.isSquare ? 18 : 22, - height: props.isSquare ? 18 : 22, - borderRadius: props.isSquare ? 0 : 22, + width: props.isSquare ? 18 : 20, + height: props.isSquare ? 18 : 20, + borderRadius: props.isSquare ? 0 : 20, position: 'absolute', - right: props.isSquare ? -9 : -11, + right: props.isSquare ? -9 : -10, bottom: -2, backgroundColor: props.theme.colors.elevation0, padding: 2, diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 02a9f526a..31996f41d 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -69,7 +69,7 @@ function TransferFeeView({ - + {t('NETWORK_FEE')} diff --git a/src/app/hooks/queries/ordinals/useAddressInscription.ts b/src/app/hooks/queries/ordinals/useAddressInscription.ts index 9202dbb4b..d9537ede9 100644 --- a/src/app/hooks/queries/ordinals/useAddressInscription.ts +++ b/src/app/hooks/queries/ordinals/useAddressInscription.ts @@ -7,7 +7,7 @@ import { handleRetries, InvalidParamsError } from '@utils/query'; /** * Get inscription details belonging to an address by ordinalId */ -const useAddressInscription = (ordinalId: string, ordinal?: Inscription | null) => { +const useAddressInscription = (ordinalId?: string, ordinal?: Inscription | null) => { const { ordinalsAddress } = useSelectedAccount(); const { network } = useWalletSelector(); const fetchOrdinals = async (): Promise => { diff --git a/src/app/hooks/useResetUserFlow.ts b/src/app/hooks/useResetUserFlow.ts index ebe39e2f6..1ed4a7856 100644 --- a/src/app/hooks/useResetUserFlow.ts +++ b/src/app/hooks/useResetUserFlow.ts @@ -25,7 +25,6 @@ const userFlowConfig: Record = { '/confirm-nft-tx': { resetTo: '/nft-dashboard?tab=nfts' }, '/rare-sats-detail': { resetTo: '/nft-dashboard?tab=rareSats' }, '/rare-sats-bundle': { resetTo: '/nft-dashboard?tab=rareSats' }, - '/send-rare-sat': { resetTo: '/nft-dashboard?tab=rareSats' }, '/verify-ledger': { resetTo: '/verify-ledger?mismatch=true' }, '/add-stx-address-ledger': { resetTo: '/add-stx-address-ledger?mismatch=true' }, '/send-rune': { resetTo: '/' }, diff --git a/src/app/hooks/useTextOrdinalContent.ts b/src/app/hooks/useTextOrdinalContent.ts index 00a6040ac..998749382 100644 --- a/src/app/hooks/useTextOrdinalContent.ts +++ b/src/app/hooks/useTextOrdinalContent.ts @@ -6,11 +6,15 @@ import useWalletSelector from './useWalletSelector'; const queue = new PQueue({ concurrency: 1 }); -const useTextOrdinalContent = (ordinal: Inscription | CondensedInscription) => { +const useTextOrdinalContent = (ordinal?: Inscription | CondensedInscription) => { const { network } = useWalletSelector(); const { data: textContent } = useQuery({ + enabled: !!ordinal?.id, queryKey: ['ordinal-text', ordinal?.id, network.type], - queryFn: async () => queue.add(() => getTextOrdinalContent(network.type, ordinal?.id)), + queryFn: async () => { + if (!ordinal?.id) return; + return queue.add(() => getTextOrdinalContent(network.type, ordinal?.id)); + }, staleTime: 5 * 60 * 1000, // 5 min }); diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index d2c541f4a..30b612b32 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -59,7 +59,6 @@ import SendBtcScreen from '@screens/sendBtc'; import SendSip10Screen from '@screens/sendFt'; import SendNft from '@screens/sendNft'; import SendOrdinal from '@screens/sendOrdinal'; -import SendRareSat from '@screens/sendRareSat'; import SendRuneScreen from '@screens/sendRune'; import SendStxScreen from '@screens/sendStx'; import Setting from '@screens/settings'; @@ -576,14 +575,6 @@ const router = createHashRouter([ ), }, - { - path: 'nft-dashboard/send-rare-sat', - element: ( - - - - ), - }, { path: 'nft-dashboard/confirm-ordinal-tx/:id', element: ( diff --git a/src/app/screens/confirmBrc20Transaction/amountRow.tsx b/src/app/screens/confirmBrc20Transaction/amountRow.tsx index 0fc722753..075bfb3e3 100644 --- a/src/app/screens/confirmBrc20Transaction/amountRow.tsx +++ b/src/app/screens/confirmBrc20Transaction/amountRow.tsx @@ -1,5 +1,5 @@ -import styled from 'styled-components'; import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; const RowContainer = styled.div` display: flex; @@ -10,45 +10,53 @@ const RowContainer = styled.div` const LabelContainer = styled.div` display: flex; align-items: center; - gap: ${(props) => props.theme.spacing(5)}px; + gap: ${(props) => props.theme.space.m}; `; -const SubTextContainer = styled.div` - margin-top: -5px; - min-height: 16px; - width: 100%; +const TextContainer = styled.div` text-align: end; `; +const StyledSubtext = styled(StyledP)` + margin-top: ${(props) => props.theme.space.xxxs}; +`; + function AmountRow({ icon, amountLabel, amount, amountSubText, + ticker, }: { icon: React.ReactNode; amountLabel: string; amount: React.ReactNode; amountSubText: React.ReactNode; + ticker: string; }) { return (
{icon} - - {amountLabel} - +
+ + {amountLabel} + + + {ticker} Token + +
- - {amount} - + + + {amount} + + + {amountSubText} + +
- - - {amountSubText} - -
); } diff --git a/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx b/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx index c53b97732..9178a9573 100644 --- a/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx +++ b/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx @@ -1,33 +1,23 @@ -import { currencySymbolMap, type SupportedCurrency } from '@secretkeylabs/xverse-core'; +import { StyledFiatAmountText } from '@components/fiatAmountText'; +import { type SupportedCurrency } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; -import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -const Card = styled.div` - background: ${(props) => props.theme.colors.elevation1}; - border-radius: ${(props) => props.theme.radius(2)}px; - padding: ${(props) => props.theme.spacing(8)}px; -`; - -const CardTitle = styled.div` - margin-bottom: ${(props) => props.theme.spacing(8)}px; -`; - const TextLabel = styled.label` - ${(props) => props.theme.body_medium_m} - color: ${(props) => props.theme.colors.white_200}; + ${(props) => props.theme.typography.body_medium_m} + color: ${(props) => props.theme.colors.white_0}; `; const TextValue = styled.p` - ${(props) => props.theme.body_medium_m} + ${(props) => props.theme.typography.body_medium_m} color: ${(props) => props.theme.colors.white_0}; `; const Rows = styled.div` display: flex; flex-direction: column; - gap: ${(props) => props.theme.spacing(8)}px; + gap: ${(props) => props.theme.space.m}; `; const RowItem = styled.div` @@ -35,59 +25,48 @@ const RowItem = styled.div` justify-content: space-between; `; -const FiatAmountText = styled.p` - ${(props) => props.theme.body_medium_s} +const FeeRateText = styled.p` + ${(props) => props.theme.typography.body_medium_s} color: ${(props) => props.theme.colors.white_400}; display: flex; justify-content: flex-end; + margin-top: ${(props) => props.theme.space.xxs}; `; -function Brc20FeesComponent({ - fees, -}: { - fees: { - label: string; - value: BigNumber; - suffix: string; - fiatValue?: BigNumber; - fiatCurrency?: SupportedCurrency; - }[]; -}) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); +type Props = { + label: string; + value: BigNumber; + suffix: string; + fiatValue?: BigNumber; + fiatCurrency?: SupportedCurrency; + feeRate?: number; +}; + +function Brc20FeesComponent({ label, value, suffix, fiatValue, fiatCurrency, feeRate }: Props) { return ( - - - {t('FEES')} - - - {fees.map(({ label, value, suffix, fiatValue, fiatCurrency }) => ( -
- - {label} - - - - - {fiatValue && fiatCurrency && ( - - - - )} -
- ))} -
-
+ +
+ + {label} + + + + + {feeRate && ( + + + + )} + {fiatValue && fiatCurrency && ( + + )} +
+
); } diff --git a/src/app/screens/confirmBrc20Transaction/editFees.tsx b/src/app/screens/confirmBrc20Transaction/editFees.tsx deleted file mode 100644 index 2908f33d3..000000000 --- a/src/app/screens/confirmBrc20Transaction/editFees.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import { BetterBarLoader } from '@components/barLoader'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; -import FiatAmountText from '@components/fiatAmountText'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; -import { InputFeedback } from '@ui-library/inputFeedback'; -import { handleKeyDownFeeRateInput } from '@utils/helper'; -import BigNumber from 'bignumber.js'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(2), -})); - -const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), -})); - -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(8), -})); - -// TODO create input component in ui-library -const InputContainer = styled.div<{ withError?: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(6), - border: `1px solid ${ - props.withError ? props.theme.colors.danger_dark_200 : props.theme.colors.white_800 - }`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: props.theme.radius(1), - padding: props.theme.spacing(5), -})); - -const InputField = styled.input((props) => ({ - ...props.theme.body_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - border: 'transparent', - width: '50%', - '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&[type=number]': { - '-moz-appearance': 'textfield', - }, -})); - -const FeeText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_0, -})); - -const ButtonContainer = styled.div` - display: flex; - flex-direction: row; - margin-top: ${(props) => props.theme.spacing(6)}px; - gap: ${(props) => props.theme.spacing(4)}px; -`; - -const FeeButton = styled.button<{ - isSelected: boolean; -}>((props) => ({ - ...props.theme.body_medium_m, - color: `${props.isSelected ? props.theme.colors.elevation2 : props.theme.colors.white_400}`, - background: `${props.isSelected ? props.theme.colors.white : 'transparent'}`, - border: `1px solid ${props.isSelected ? 'transparent' : props.theme.colors.elevation6}`, - borderRadius: 40, - height: 40, - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', -})); - -const FeeContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const TickerContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-end', - flex: 1, - minHeight: 34, -}); - -const ApplyButtonContainer = styled.div` - display: flex; - flex-direction: column; - margin: 20px 16px 40px; -`; - -const StyledInputFeedback = styled(InputFeedback)` - margin-bottom: ${(props) => props.theme.spacing(2)}px; -`; - -const StyledFiatAmountText = styled(FiatAmountText)((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - -const buttons = [ - { - value: 'standard', - label: 'TRANSACTION_SETTING.STANDARD', - }, - { - value: 'high', - label: 'TRANSACTION_SETTING.HIGH', - }, - { - value: 'custom', - label: 'TRANSACTION_SETTING.CUSTOM', - }, -]; - -export type OnChangeFeeRate = (feeRate: string) => void; - -export function EditFees({ - visible, - onClose, - onClickApply, - onChangeFeeRate, - fee, - initialFeeRate, - isFeeLoading, - error, -}: { - visible: boolean; - onClose: () => void; - onClickApply: OnChangeFeeRate; - onChangeFeeRate: OnChangeFeeRate; - fee: string; - initialFeeRate: string; - isFeeLoading: boolean; - error: string; -}) { - const { t } = useTranslation('translation'); - - const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { data: feeRates } = useBtcFeeRate(); - - // save the previous state in case user clicks X without applying - const [previousFeeRate, setPreviousFeeRate] = useState(initialFeeRate); - const [previousSelectedOption, setPreviousSelectedOption] = useState('standard'); - - const [feeRateInput, setFeeRateInput] = useState(previousFeeRate); - const [selectedOption, setSelectedOption] = useState(previousSelectedOption); - - const inputRef = useRef(null); - - useEffect(() => { - onChangeFeeRate(feeRateInput); - }, [feeRateInput, onChangeFeeRate]); - - /* callbacks */ - const handleChangeFeeRateInput = (e: React.ChangeEvent) => { - setFeeRateInput(e.target.value); - if (selectedOption !== 'custom') { - setSelectedOption('custom'); - } - }; - - const handleClickFeeButton = (e: React.MouseEvent) => { - setSelectedOption(e.currentTarget.value); - if (feeRates) { - switch (e.currentTarget.value) { - case 'high': - setFeeRateInput(feeRates.priority.toString()); - break; - case 'standard': - setFeeRateInput(feeRates.regular.toString()); - break; - case 'custom': - inputRef.current?.focus(); - break; - default: - break; - } - } - }; - - const handleClickClose = () => { - // reset state - setFeeRateInput(previousFeeRate); - setSelectedOption(previousSelectedOption); - onClose(); - }; - - const handleClickApply = () => { - // save state - setPreviousFeeRate(feeRateInput); - setPreviousSelectedOption(selectedOption); - // apply state to parent - onClickApply(feeRateInput); - onClose(); - }; - - const fiatFee = getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)); - - return ( - - - {t('TRANSACTION_SETTING.FEE_RATE')} - - - - {t('UNITS.SATS_PER_VB')} - - {isFeeLoading ? ( - <> - - - - ) : ( - <> - {value}} - /> - - - )} - - - - - - {buttons.map(({ value, label }) => ( - - {t(label)} - - ))} - - {t('TRANSACTION_SETTING.FEE_INFO')} - - - - - - ); -} diff --git a/src/app/screens/confirmBrc20Transaction/index.styled.ts b/src/app/screens/confirmBrc20Transaction/index.styled.ts new file mode 100644 index 000000000..59fdae2ef --- /dev/null +++ b/src/app/screens/confirmBrc20Transaction/index.styled.ts @@ -0,0 +1,80 @@ +import styled from 'styled-components'; + +export const ScrollContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; +`; + +export const OuterContainer = styled.div` + display: flex; + flex-direction: column; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginTop: props.theme.space.l, + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, + marginBottom: props.theme.space.m, +})); + +export const Subtitle = styled.p` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.xs}; +`; + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + gap: props.theme.space.s, + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, + marginTop: props.theme.space.l, + marginBottom: props.theme.space.l, +})); + +export const ErrorContainer = styled.div((props) => ({ + marginTop: props.theme.space.m, +})); + +export const ErrorText = styled.p((props) => ({ + ...props.theme.body_xs, + color: props.theme.colors.feedback.error, +})); + +export const ReviewTransactionText = styled.h1((props) => ({ + ...props.theme.headline_s, + color: props.theme.colors.white_0, + marginBottom: props.theme.spacing(10), + textAlign: 'left', +})); + +export const StyledCallouts = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +export const RecipientCardContainer = styled.div` + margin-bottom: ${(props) => props.theme.space.s}; +`; + +export const FeeContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.m}; + padding: ${(props) => props.theme.space.m}; + background-color: ${(props) => props.theme.colors.elevation1}; + border-radius: ${(props) => props.theme.radius(2)}px; +`; diff --git a/src/app/screens/confirmBrc20Transaction/index.tsx b/src/app/screens/confirmBrc20Transaction/index.tsx index 256ab37e0..c54a28cb2 100644 --- a/src/app/screens/confirmBrc20Transaction/index.tsx +++ b/src/app/screens/confirmBrc20Transaction/index.tsx @@ -1,26 +1,27 @@ -import AccountHeaderComponent from '@components/accountHeader'; -import ActionButton from '@components/button'; import InfoContainer from '@components/infoContainer'; import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; import TransactionDetailComponent from '@components/transactionDetailComponent'; import useCoinRates from '@hooks/queries/useCoinRates'; +import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useDebounce from '@hooks/useDebounce'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import { FadersHorizontal } from '@phosphor-icons/react'; -import type { BRC20ErrorCode, SettingsNetwork } from '@secretkeylabs/xverse-core'; +import type { BRC20ErrorCode } from '@secretkeylabs/xverse-core'; import { AnalyticsEvents, + brc20TransferEstimateFees, getBtcFiatEquivalent, useBrc20TransferFees, validateBtcAddressIsTaproot, } from '@secretkeylabs/xverse-core'; +import SelectFeeRate from '@ui-components/selectFeeRate'; +import Button from '@ui-library/button'; import Callout, { type CalloutProps } from '@ui-library/callout'; import { getFeeValuesForBrc20OneStepTransfer, - type Brc20TransferEstimateFeesParams, type ConfirmBrc20TransferState, type ExecuteBrc20TransferState, } from '@utils/brc20'; @@ -30,112 +31,23 @@ import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import Brc20FeesComponent from './brc20FeesComponent'; -import { EditFees, type OnChangeFeeRate } from './editFees'; +import { + ButtonContainer, + Container, + ErrorContainer, + ErrorText, + FeeContainer, + OuterContainer, + RecipientCardContainer, + ReviewTransactionText, + ScrollContainer, + StyledCallouts, + Subtitle, +} from './index.styled'; import RecipientCard, { type RecipientCardProps } from './recipientCard'; -const ScrollContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - overflow-y: auto; -`; - -const OuterContainer = styled.div` - display: flex; - flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - marginTop: props.theme.spacing(16), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - gap: props.theme.spacing(8), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginTop: props.theme.spacing(12), - marginBottom: props.theme.spacing(12), -})); - -const EditFeesButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: props.theme.spacing(3), - borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - marginTop: props.theme.spacing(12), -})); - -const ButtonText = styled.div((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const FadersHorizontalIcon = styled(FadersHorizontal)((props) => ({ - color: props.theme.colors.white_0, -})); - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), -})); - -const ErrorText = styled.p((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.feedback.error, -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white[0], - marginBottom: props.theme.spacing(10), - textAlign: 'left', -})); - -const StyledCallouts = styled.div` - display: flex; - flex-direction: column; - gap: ${(props) => props.theme.spacing(6)}px; - margin-bottom: ${(props) => props.theme.spacing(12)}px; -`; - -const RecipientCardContainer = styled.div` - margin-bottom: ${(props) => props.theme.spacing(6)}px; -`; - -const useConfirmBrc20Transfer = (): { - callouts: CalloutProps[]; - errorMessage: string; - estimateFeesParams: Brc20TransferEstimateFeesParams; - fees: any[]; - handleClickAdvancedSetting: () => void; - handleClickApplyFee: OnChangeFeeRate; - handleClickCancel: () => void; - handleClickCloseFees: () => void; - handleClickConfirm: () => void; - isConfirmLoading: boolean; - isFeeLoading: boolean; - network: SettingsNetwork; - recipient: RecipientCardProps; - showFeeSettings: boolean; - txFee: BigNumber; - showFeeWarning: boolean; -} => { +function ConfirmBrc20Transaction() { /* hooks */ const { t } = useTranslation('translation'); const { network, fiatCurrency, feeMultipliers } = useWalletSelector(); @@ -151,15 +63,15 @@ const useConfirmBrc20Transfer = (): { }: ConfirmBrc20TransferState = useLocation().state; const [showFeeWarning, setShowFeeWarning] = useState(false); const transactionContext = useTransactionContext(); + const { data: recommendedFees } = useBtcFeeRate(); useResetUserFlow('/confirm-brc20-tx'); /* state */ const [isConfirmLoading, setIsConfirmLoading] = useState(false); - const [showFeeSettings, setShowFeeSettings] = useState(false); - const [userInputFeeRate, setUserInputFeeRate] = useState(''); + const [userInputFeeRate, setUserInputFeeRate] = useState(estimateFeesParams.feeRate.toString()); const [error, setError] = useState(''); - const debouncedUserInputFeeRate = useDebounce(userInputFeeRate, 100); + const debouncedUserInputFeeRate = useDebounce(userInputFeeRate, 500); const { commitValueBreakdown, @@ -168,7 +80,7 @@ const useConfirmBrc20Transfer = (): { } = useBrc20TransferFees({ ...estimateFeesParams, feeRate: Number(debouncedUserInputFeeRate), - skipInitialFetch: true, + skipInitialFetch: false, context: transactionContext, }); @@ -208,45 +120,7 @@ const useConfirmBrc20Transfer = (): { setIsConfirmLoading(false); }; - const handleClickCancel = () => { - navigate(-1); - }; - - const handleClickApplyFee: OnChangeFeeRate = (feeRate: string) => { - if (feeRate) { - setUserInputFeeRate(feeRate); - } - }; - - const handleClickAdvancedSetting = () => { - setShowFeeSettings(true); - }; - - const handleClickCloseFees = () => { - setShowFeeSettings(false); - }; - /* other */ - const fees = [ - { - label: 'Transaction Fee', - value: txFee, - suffix: 'sats', - }, - { - label: 'Inscription Fee', - value: inscriptionFee, - suffix: 'sats', - }, - { - label: 'Total Fee', - value: totalFee, - suffix: 'sats', - fiatValue: getBtcFiatEquivalent(totalFee, BigNumber(btcFiatRate)), - fiatCurrency, - }, - ]; - const errorMessage = errorCode ? t(`CONFIRM_BRC20.ERROR_CODES.${errorCode}`) : error; const recipient: RecipientCardProps = { @@ -270,50 +144,31 @@ const useConfirmBrc20Transfer = (): { }); } - return { - callouts, - errorMessage, - estimateFeesParams, - fees, - handleClickAdvancedSetting, - handleClickApplyFee, - handleClickCancel, - handleClickCloseFees, - handleClickConfirm, - isConfirmLoading, - isFeeLoading, - network, - recipient, - showFeeSettings, - txFee, - showFeeWarning, + const handleGoBack = () => { + navigate(`/send-brc20-one-step?principal=${token.principal}`, { + state: { + amount: estimateFeesParams.amount.toString(), + recipientAddress, + }, + }); }; -}; -function ConfirmBrc20Transaction() { - const { t } = useTranslation('translation'); - const { - callouts, - errorMessage, - estimateFeesParams, - fees, - handleClickAdvancedSetting, - handleClickApplyFee, - handleClickCancel, - handleClickCloseFees, - handleClickConfirm, - isConfirmLoading, - isFeeLoading, - network, - recipient, - showFeeSettings, - txFee, - showFeeWarning, - } = useConfirmBrc20Transfer(); + const handleClickCancel = () => { + navigate(`/coinDashboard/FT?ftKey=${token.principal}&protocol=brc-20`); + }; + + const getFeeForFeeRate = async (feeRate) => { + const estimatedFees = await brc20TransferEstimateFees( + { ...estimateFeesParams, feeRate }, + transactionContext, + ); + const { txFee: newTxFee } = getFeeValuesForBrc20OneStepTransfer(estimatedFees.valueBreakdown); + return newTxFee.toNumber(); + }; return ( <> - + @@ -334,6 +189,9 @@ function ConfirmBrc20Transaction() { ))} )} + + {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} + + + {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} + - -
- - - {t('CONFIRM_TRANSACTION.EDIT_FEES')} - -
+ + {t('CONFIRM_TRANSACTION.FEES')} + + + + {recommendedFees && ( + setUserInputFeeRate(newFeeRate)} + baseToFiat={(amount) => + getBtcFiatEquivalent(new BigNumber(amount), BigNumber(btcFiatRate)).toString() + } + fiatUnit={fiatCurrency} + getFeeForFeeRate={getFeeForFeeRate} + feeRates={{ + medium: recommendedFees.regular, + high: recommendedFees.priority, + }} + feeRateLimits={recommendedFees.limits} + isLoading={isFeeLoading} + amount={estimateFeesParams.amount} + /> + )} + + + {errorMessage && ( {errorMessage} @@ -361,29 +256,20 @@ function ConfirmBrc20Transaction() {
- - - + {!isInOptions() && }
diff --git a/src/app/screens/confirmBrc20Transaction/recipientCard.tsx b/src/app/screens/confirmBrc20Transaction/recipientCard.tsx index c22af4b22..9ab9a938a 100644 --- a/src/app/screens/confirmBrc20Transaction/recipientCard.tsx +++ b/src/app/screens/confirmBrc20Transaction/recipientCard.tsx @@ -1,11 +1,9 @@ -import btcIcon from '@assets/img/ledger/btc_icon.svg'; -import OutputIcon from '@assets/img/transactions/output.svg'; import FiatAmountText from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; -import TransferDetailView from '@components/transferDetailView'; -import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent, type FungibleToken } from '@secretkeylabs/xverse-core'; +import { type FungibleToken } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getTruncatedAddress } from '@utils/helper'; import { getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; @@ -16,10 +14,20 @@ import AmountRow from './amountRow'; const Container = styled.div` display: flex; flex-direction: column; - padding: ${(props) => props.theme.spacing(8)}px; + padding: ${(props) => props.theme.space.m}; background: ${(props) => props.theme.colors.elevation1}; border-radius: ${(props) => props.theme.radius(2)}px; - gap: ${(props) => props.theme.spacing(8)}px; + gap: ${(props) => props.theme.space.m}; +`; + +const RowBlock = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Title = styled(StyledP)` + color: ${(props) => props.theme.colors.white_400}; `; export type RecipientCardProps = { @@ -32,10 +40,28 @@ export type RecipientCardProps = { function RecipientCard({ address, amountBrc20, amountSats, fungibleToken }: RecipientCardProps) { const { t } = useTranslation('translation'); const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); return ( + + {t('CONFIRM_TRANSACTION.TO')} + {getTruncatedAddress(address, 6)} + + + {t('COMMON.BUNDLE')} + ( + + {value} + + )} + /> + } amountLabel={t('CONFIRM_TRANSACTION.AMOUNT')} @@ -55,29 +81,7 @@ function RecipientCard({ address, amountBrc20, amountSats, fungibleToken }: Reci /> ) } - /> - } - amountLabel={t('CONFIRM_TRANSACTION.AMOUNT')} - amount={ - - } - amountSubText={ - - } - /> - ); diff --git a/src/app/screens/confirmOrdinalTransaction/index.tsx b/src/app/screens/confirmOrdinalTransaction/index.tsx index b49ed4af4..e3da4514d 100644 --- a/src/app/screens/confirmOrdinalTransaction/index.tsx +++ b/src/app/screens/confirmOrdinalTransaction/index.tsx @@ -34,9 +34,9 @@ const NftContainer = styled.div((props) => ({ aspectRatio: 1, justifyContent: 'center', alignItems: 'center', - borderRadius: 8, + borderRadius: props.theme.radius(1), padding: props.theme.spacing(5), - marginBottom: props.theme.spacing(6), + marginBottom: props.theme.space.s, })); function ConfirmOrdinalTransaction() { @@ -144,9 +144,11 @@ function ConfirmOrdinalTransaction() { }; useResetUserFlow('/confirm-ordinal-tx'); + const handleBackButtonClick = () => { - navigate(-1); + navigate(-1); // TODO: change this logic, also create a separate handler for cancel }; + const hideBackButton = location.key === 'default'; return ( diff --git a/src/app/screens/listRune/listRuneItem.tsx b/src/app/screens/listRune/listRuneItem.tsx index 5a9bf4c15..c846128f1 100644 --- a/src/app/screens/listRune/listRuneItem.tsx +++ b/src/app/screens/listRune/listRuneItem.tsx @@ -109,7 +109,7 @@ function ListRuneItem({ ( diff --git a/src/app/screens/listRune/setRunePriceItem.tsx b/src/app/screens/listRune/setRunePriceItem.tsx index cfb2c5b10..34e94c65f 100644 --- a/src/app/screens/listRune/setRunePriceItem.tsx +++ b/src/app/screens/listRune/setRunePriceItem.tsx @@ -78,7 +78,7 @@ function SetRunePriceItem({ floorPriceSats, handleShowCustomPriceModal, }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' }); + const { t } = useTranslation('translation'); const { btcFiatRate } = useCoinRates(); const { fiatCurrency } = useWalletSelector(); @@ -100,8 +100,8 @@ function SetRunePriceItem({ ( @@ -145,7 +145,7 @@ function SetRunePriceItem({ )} /> diff --git a/src/app/screens/ordinalDetail/index.styled.ts b/src/app/screens/ordinalDetail/index.styled.ts new file mode 100644 index 000000000..b9cb58569 --- /dev/null +++ b/src/app/screens/ordinalDetail/index.styled.ts @@ -0,0 +1,337 @@ +import { BetterBarLoader } from '@components/barLoader'; +import Separator from '@components/separator'; +import WebGalleryButton from '@components/webGalleryButton'; +import Callout from '@ui-library/callout'; +import { StyledP } from '@ui-library/common.styled'; +import { Tooltip } from 'react-tooltip'; +import styled from 'styled-components'; + +interface DetailSectionProps { + isGallery: boolean; +} + +export const GalleryScrollContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, + alignItems: 'center', +})); + +export const GalleryContainer = styled.div({ + marginLeft: 'auto', + marginRight: 'auto', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + maxWidth: 1224, +}); + +export const BackButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + width: 820, + marginTop: props.theme.spacing(40), +})); + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + position: 'relative', + flexDirection: 'row', + maxWidth: 400, + columnGap: props.theme.spacing(8), + marginBottom: props.theme.spacing(10.5), +})); + +export const ExtensionContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.spacing(4), + alignItems: 'center', + flex: 1, + paddingLeft: props.theme.spacing(4), + paddingRight: props.theme.spacing(4), +})); + +export const OrdinalsContainer = styled.div((props) => ({ + width: 376.5, + height: 376.5, + display: 'flex', + aspectRatio: '1', + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + borderRadius: 8, + marginBottom: props.theme.spacing(12), +})); + +export const ExtensionOrdinalsContainer = styled.div((props) => ({ + maxHeight: 136, + width: 136, + display: 'flex', + aspectRatio: '1', + justifyContent: 'center', + alignItems: 'center', + borderRadius: props.theme.radius(1), + marginTop: props.theme.spacing(12), + marginBottom: props.theme.space.m, +})); + +export const OrdinalTitleText = styled.h1((props) => ({ + ...props.theme.typography.headline_m, + color: props.theme.colors.white_0, + marginTop: props.theme.spacing(1), + textAlign: 'center', +})); + +export const OrdinalGalleryTitleText = styled.h1((props) => ({ + ...props.theme.typography.headline_l, + color: props.theme.colors.white_0, +})); + +export const DescriptionText = styled.h1((props) => ({ + ...props.theme.typography.headline_l, + color: props.theme.colors.white_0, + fontSize: 24, +})); + +export const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +export const RowContainer = styled.div<{ + withGap?: boolean; +}>((props) => ({ + display: 'flex', + alignItems: 'flex-start', + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(12), + flexDirection: 'row', + columnGap: props.withGap ? props.theme.spacing(20) : 0, +})); + +export const ColumnContainer = styled.div({ + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + width: '100%', +}); + +export const OrdinalDetailsContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', + width: props.isGallery ? 400 : '100%', + marginTop: props.theme.spacing(8), +})); + +export const Row = styled.div({ + display: 'flex', + justifyContent: 'space-between', + flexDirection: 'row', + width: '100%', +}); + +export const DescriptionContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginBottom: props.theme.spacing(30), +})); + +export const StyledWebGalleryButton = styled(WebGalleryButton)` + margintop: ${(props) => props.theme.space.s}; +`; + +export const ViewInExplorerButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'transparent', + width: props.isGallery ? 392 : 328, + height: 44, + padding: 12, + borderRadius: 12, + marginTop: props.theme.spacing(16), + marginBottom: props.theme.spacing(18), + border: `1px solid ${props.theme.colors.white_800}`, +})); + +export const ButtonText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, +})); + +export const ButtonHiglightedText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + marginLeft: props.theme.spacing(2), + marginRight: props.theme.spacing(2), +})); + +export const StyledTooltip = styled(Tooltip)` + &&& { + font-size: 12px; + background-color: #ffffff; + color: #12151e; + border-radius: 8px; + padding: 7px; + } +`; + +export const ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +export const Button = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.spacing(12), +})); + +export const AssetDeatilButtonText = styled.div((props) => ({ + ...props.theme.typography.body_s, + fontWeight: 400, + fontSize: 14, + color: props.theme.colors.white_0, + textAlign: 'center', +})); + +export const CollectibleText = styled.h1((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_400, + textAlign: 'center', +})); + +export const GalleryCollectibleText = styled.h1((props) => ({ + ...props.theme.typography.body_bold_l, + color: props.theme.colors.white_400, +})); + +export const GalleryButtonContainer = styled.div` + width: 190px; + border-radius: 12px; +`; + +export const RowButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + columnGap: props.theme.spacing(11), + marginBottom: props.theme.space.l, + marginTop: props.theme.space.m, + width: '100%', +})); + +export const Divider = styled.div((props) => ({ + width: '100%', + borderBottom: `1px solid ${props.theme.colors.elevation3}`, +})); + +export const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: !props.isGallery ? 'row' : 'column', + justifyContent: 'space-between', + columnGap: props.theme.space.m, + width: '100%', +})); + +export const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +export const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const StyledBarLoader = styled(BetterBarLoader)<{ + withMarginBottom?: boolean; +}>((props) => ({ + padding: 0, + borderRadius: props.theme.radius(1), + marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, +})); + +export const StyledSeparator = styled(Separator)` + width: 100%; +`; + +export const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; + +export const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +export const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.spacing(4), +})); + +export const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.spacing(8)}px`, +})); + +export const RareSatsBundleCallout = styled(Callout)((props) => ({ + width: props.isGallery ? 400 : '100%', + marginBottom: props.isGallery ? 0 : props.theme.space.l, + marginTop: props.isGallery ? props.theme.space.xs : 0, +})); + +export const SatributesIconsContainer = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + marginTop: props.isGallery ? props.theme.space.m : 0, +})); + +export const SatributesBadgeContainer = styled.div((props) => ({ + marginTop: props.isGallery ? 0 : props.theme.space.m, +})); +export const SatributesBadges = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + flexWrap: 'wrap', + maxWidth: props.isGallery ? 400 : '100%', + marginTop: props.theme.space.s, +})); +export const Badge = styled.div<{ backgroundColor?: string; isLastItem: boolean }>((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + backgroundColor: props.backgroundColor, + padding: `${props.theme.space.s} ${props.theme.space.s}`, + borderRadius: props.theme.radius(2), + border: `1px solid ${props.theme.colors.elevation3}`, + marginRight: props.isLastItem ? 0 : props.theme.space.xs, + marginBottom: props.theme.space.xs, +})); +export const SatributeBadgeLabel = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xs}; +`; +export const DataItemsContainer = styled.div` + margin-top: ${(props) => props.theme.space.l}; +`; diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index 926640b9e..ea8bd71a7 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -1,393 +1,69 @@ import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; import AlertMessage from '@components/alertMessage'; -import { BetterBarLoader } from '@components/barLoader'; import ActionButton from '@components/button'; import CollectibleDetailTile from '@components/collectibleDetailTile'; import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; -import Separator from '@components/separator'; import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import WebGalleryButton from '@components/webGalleryButton'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import { ArrowUp, Share } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; -import Callout from '@ui-library/callout'; import { StyledP } from '@ui-library/common.styled'; import { EMPTY_LABEL } from '@utils/constants'; import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; import { useTranslation } from 'react-i18next'; -import { Tooltip } from 'react-tooltip'; -import styled from 'styled-components'; +import { + ActionButtonLoader, + ActionButtonsLoader, + AssetDeatilButtonText, + BackButtonContainer, + Badge, + BottomBarContainer, + Button, + ButtonContainer, + ButtonHiglightedText, + ButtonImage, + ButtonText, + CollectibleText, + ColumnContainer, + DataItemsContainer, + DescriptionContainer, + DescriptionText, + DetailSection, + Divider, + ExtensionContainer, + ExtensionLoaderContainer, + ExtensionOrdinalsContainer, + GalleryButtonContainer, + GalleryCollectibleText, + GalleryContainer, + GalleryLoaderContainer, + GalleryScrollContainer, + InfoContainer, + OrdinalDetailsContainer, + OrdinalGalleryTitleText, + OrdinalsContainer, + OrdinalTitleText, + RareSatsBundleCallout, + Row, + RowButtonContainer, + RowContainer, + SatributeBadgeLabel, + SatributesBadgeContainer, + SatributesBadges, + SatributesIconsContainer, + StyledBarLoader, + StyledSeparator, + StyledTooltip, + StyledWebGalleryButton, + TitleLoader, + ViewInExplorerButton, +} from './index.styled'; import OrdinalAttributeComponent from './ordinalAttributeComponent'; import useOrdinalDetail from './useOrdinalDetail'; -interface DetailSectionProps { - isGallery: boolean; -} - -const GalleryScrollContainer = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - flex: 1, - alignItems: 'center', -})); - -const GalleryContainer = styled.div({ - marginLeft: 'auto', - marginRight: 'auto', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - width: '100%', - maxWidth: 1224, -}); - -const BackButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - width: 820, - marginTop: props.theme.spacing(40), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - position: 'relative', - flexDirection: 'row', - maxWidth: 400, - columnGap: props.theme.spacing(8), - marginBottom: props.theme.spacing(10.5), -})); - -const ExtensionContainer = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(4), - alignItems: 'center', - flex: 1, - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), -})); - -const OrdinalsContainer = styled.div((props) => ({ - width: 376.5, - height: 376.5, - display: 'flex', - aspectRatio: '1', - flexDirection: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start', - borderRadius: 8, - marginBottom: props.theme.spacing(12), -})); - -const ExtensionOrdinalsContainer = styled.div((props) => ({ - maxHeight: 136, - width: 136, - display: 'flex', - aspectRatio: '1', - justifyContent: 'center', - alignItems: 'center', - borderRadius: props.theme.radius(1), - marginTop: props.theme.spacing(12), - marginBottom: props.theme.space.m, -})); - -const OrdinalTitleText = styled.h1((props) => ({ - ...props.theme.typography.headline_m, - color: props.theme.colors.white_0, - marginTop: props.theme.spacing(1), - textAlign: 'center', -})); - -const OrdinalGalleryTitleText = styled.h1((props) => ({ - ...props.theme.typography.headline_l, - color: props.theme.colors.white_0, -})); - -const DescriptionText = styled.h1((props) => ({ - ...props.theme.typography.headline_l, - color: props.theme.colors.white_0, - fontSize: 24, -})); - -const NftOwnedByText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, -})); - -const OwnerAddressText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - marginLeft: props.theme.spacing(3), -})); - -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); - -const RowContainer = styled.div<{ - withGap?: boolean; -}>((props) => ({ - display: 'flex', - alignItems: 'flex-start', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(12), - flexDirection: 'row', - columnGap: props.withGap ? props.theme.spacing(20) : 0, -})); - -const ColumnContainer = styled.div({ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - width: '100%', -}); - -const OrdinalDetailsContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - wordBreak: 'break-all', - whiteSpace: 'pre-wrap', - width: props.isGallery ? 400 : '100%', - marginTop: props.theme.spacing(8), -})); - -const Row = styled.div({ - display: 'flex', - justifyContent: 'space-between', - flexDirection: 'row', - width: '100%', -}); - -const DescriptionContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginBottom: props.theme.spacing(30), -})); - -const StyledWebGalleryButton = styled(WebGalleryButton)` - margintop: ${(props) => props.theme.space.s}; -`; - -const ViewInExplorerButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'transparent', - width: props.isGallery ? 392 : 328, - height: 44, - padding: 12, - borderRadius: 12, - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(18), - border: `1px solid ${props.theme.colors.white_800}`, -})); - -const ButtonText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, -})); - -const ButtonHiglightedText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), -})); - -const StyledTooltip = styled(Tooltip)` - &&& { - font-size: 12px; - background-color: #ffffff; - color: #12151e; - border-radius: 8px; - padding: 7px; - } -`; - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDeatilButtonText = styled.div((props) => ({ - ...props.theme.typography.body_s, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const ButtonIcon = styled.img({ - width: 12, - height: 12, -}); - -const OrdinalsTag = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - marginLeft: 2, - background: 'rgba(39, 42, 68, 0.6)', - borderRadius: 40, - height: 22, - padding: '3px 6px', -}); - -const CollectibleText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_400, - textAlign: 'center', -})); - -const GalleryCollectibleText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_l, - color: props.theme.colors.white_400, -})); - -const Text = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - textTransform: 'uppercase', - color: props.theme.colors.white_0, - fontSize: 10, - marginLeft: props.theme.spacing(2), -})); - -const GalleryButtonContainer = styled.div` - width: 190px; - border-radius: 12px; -`; - -const RowButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - columnGap: props.theme.spacing(11), - marginBottom: props.theme.space.l, - marginTop: props.theme.space.m, - width: '100%', -})); - -const Divider = styled.div((props) => ({ - width: '100%', - borderBottom: `1px solid ${props.theme.colors.elevation3}`, -})); - -const DetailSection = styled.div((props) => ({ - display: 'flex', - flexDirection: !props.isGallery ? 'row' : 'column', - justifyContent: 'space-between', - columnGap: props.theme.space.m, - width: '100%', -})); - -const ExtensionLoaderContainer = styled.div({ - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-around', - alignItems: 'center', -}); - -const GalleryLoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const StyledBarLoader = styled(BetterBarLoader)<{ - withMarginBottom?: boolean; -}>((props) => ({ - padding: 0, - borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, -})); - -const StyledSeparator = styled(Separator)` - width: 100%; -`; - -const TitleLoader = styled.div` - display: flex; - flex-direction: column; -`; - -const ActionButtonsLoader = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - columnGap: props.theme.spacing(11), -})); - -const ActionButtonLoader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - rowGap: props.theme.spacing(4), -})); - -const InfoContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - padding: `0 ${props.theme.spacing(8)}px`, -})); - -const RareSatsBundleCallout = styled(Callout)((props) => ({ - width: props.isGallery ? 400 : '100%', - marginBottom: props.isGallery ? 0 : props.theme.space.l, - marginTop: props.isGallery ? props.theme.space.xs : 0, -})); - -const SatributesIconsContainer = styled.div((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - marginTop: props.isGallery ? props.theme.space.m : 0, -})); - -const SatributesBadgeContainer = styled.div((props) => ({ - marginTop: props.isGallery ? 0 : props.theme.space.m, -})); -const SatributesBadges = styled.div((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - flexWrap: 'wrap', - maxWidth: props.isGallery ? 400 : '100%', - marginTop: props.theme.space.s, -})); -const Badge = styled.div<{ backgroundColor?: string; isLastItem: boolean }>((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - alignItems: 'center', - backgroundColor: props.backgroundColor, - padding: `${props.theme.space.s} ${props.theme.space.s}`, - borderRadius: props.theme.radius(2), - border: `1px solid ${props.theme.colors.elevation3}`, - marginRight: props.isLastItem ? 0 : props.theme.space.xs, - marginBottom: props.theme.space.xs, -})); -const SatributeBadgeLabel = styled(StyledP)` - margin-left: ${(props) => props.theme.space.xs}; -`; -const DataItemsContainer = styled.div` - margin-top: ${(props) => props.theme.space.l}; -`; - function OrdinalDetailScreen() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); @@ -500,7 +176,15 @@ function OrdinalDetailScreen() { case 'mint': return ( - + + + + {!isGallery && ( props.theme.scrollbar}; + overflow-y: auto; + padding-bottom: ${(props) => props.theme.space.l}; +`; + +export const PageHeader = styled.div` + padding: ${(props) => (props.isGalleryOpen ? props.theme.space.m : 0)}; + padding-top: 0; + max-width: 1224px; + margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +// TODO tim: use media queries for this once we get responsive layouts with breakpoints +export const PageHeaderContent = styled.div` + display: flex; + flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; +`; + +export const AttributesContainer = styled.div((props) => ({ + maxWidth: props.isGalleryOpen ? '285px' : '100%', + padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, +})); + +export const StyledSeparator = styled(Separator)` + margin-bottom: ${(props) => props.theme.space.xxl}; +`; + +/* components */ + +export const StyledWebGalleryButton = styled(WebGalleryButton)` + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.xs}; +`; + +export const SendButtonContainer = styled.div` + margin-top: ${(props) => props.theme.space.l}; + width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; +`; + +export const BundleRarityLinkContainer = styled.button` + margin-top: ${(props) => props.theme.space.l}; + display: flex; + justify-content: flex-start; + align-items: center; + gap: ${(props) => props.theme.space.xxs}; + background-color: transparent; + color: ${(props) => props.theme.colors.white_200}; + :hover:enabled { + color: ${(props) => props.theme.colors.white_200}; + } + :active ; + :disabled { + color: ${(props) => props.theme.colors.white_400}; + } + svg { + flex-grow: 0; + flex-shrink: 0; + } +`; + +export const BackButtonContainer = styled.div` + display: flex; + flex-direction: row; + margin-bottom: ${(props) => props.theme.space.xxl}; +`; + +export const ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +export const BackButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.spacing(12), +})); + +export const AssetDetailButtonText = styled.div((props) => ({ + ...props.theme.body_xs, + fontWeight: 400, + fontSize: 14, + color: props.theme.colors.white['0'], + textAlign: 'center', +})); + +export const NoCollectiblesText = styled.p((props) => ({ + ...props.theme.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.spacing(16), + marginBottom: 'auto', + textAlign: 'center', +})); + +export const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ + display: props.isGalleryOpen ? 'block' : 'flex', + flexDirection: props.isGalleryOpen ? 'row' : 'column', + alignItems: props.isGalleryOpen ? 'flex-start' : 'center', +})); + +export const SatRangeContainer = styled.div((props) => ({ + marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, + maxWidth: '1224px', + marginLeft: 'auto', + marginRight: 'auto', + width: '100%', +})); + +export const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: props.isGalleryOpen ? 'column' : 'row', + justifyContent: 'space-between', + columnGap: props.theme.space.m, + width: '100%', +})); + +export const SeeRarityContainer = styled.div` + padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; +`; diff --git a/src/app/screens/rareSatsBundle/index.tsx b/src/app/screens/rareSatsBundle/index.tsx index b62f73251..a5eb7bd36 100644 --- a/src/app/screens/rareSatsBundle/index.tsx +++ b/src/app/screens/rareSatsBundle/index.tsx @@ -1,11 +1,8 @@ import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; import AlertMessage from '@components/alertMessage'; -import ActionButton from '@components/button'; -import Separator from '@components/separator'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import WebGalleryButton from '@components/webGalleryButton'; import usePendingOrdinalTxs from '@hooks/queries/usePendingOrdinalTx'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; @@ -14,6 +11,7 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp } from '@phosphor-icons/react'; import type { BundleSatRange } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; import { getBtcTxStatusUrl, @@ -24,143 +22,28 @@ import { import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import OrdinalAttributeComponent from '../ordinalDetail/ordinalAttributeComponent'; +import { + AssetDetailButtonText, + AttributesContainer, + BackButton, + BackButtonContainer, + BundleRarityLinkContainer, + ButtonImage, + Container, + DetailSection, + Header, + NoCollectiblesText, + PageHeader, + PageHeaderContent, + SatRangeContainer, + SeeRarityContainer, + SendButtonContainer, + StyledSeparator, + StyledWebGalleryButton, +} from './index.styled'; import { RareSatsBundleGridItem } from './rareSatsBundleGridItem'; -interface DetailSectionProps { - isGalleryOpen?: boolean; -} - -/* layout */ -const Container = styled.div` - ...${(props) => props.theme.scrollbar}; - overflow-y: auto; - padding-bottom: ${(props) => props.theme.space.l}; -`; - -const PageHeader = styled.div` - padding: ${(props) => (props.isGalleryOpen ? props.theme.space.m : 0)}; - padding-top: 0; - max-width: 1224px; - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - -// TODO tim: use media queries for this once we get responsive layouts with breakpoints -const PageHeaderContent = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; -`; - -const AttributesContainer = styled.div((props) => ({ - maxWidth: props.isGalleryOpen ? '285px' : '100%', - padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, -})); - -const StyledSeparator = styled(Separator)` - margin-bottom: ${(props) => props.theme.space.xxl}; -`; - -/* components */ - -const StyledWebGalleryButton = styled(WebGalleryButton)` - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.xs}; -`; - -const SendButtonContainer = styled.div` - margin-top: ${(props) => props.theme.space.l}; - width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; -`; - -const BundleRarityLinkContainer = styled.button` - margin-top: ${(props) => props.theme.space.l}; - display: flex; - justify-content: flex-start; - align-items: center; - gap: ${(props) => props.theme.space.xxs}; - background-color: transparent; - color: ${(props) => props.theme.colors.white_200}; - :hover:enabled { - color: ${(props) => props.theme.colors.white_200}; - } - :active ; - :disabled { - color: ${(props) => props.theme.colors.white_400}; - } - svg { - flex-grow: 0; - flex-shrink: 0; - } -`; - -const BackButtonContainer = styled.div` - display: flex; - flex-direction: row; - margin-bottom: ${(props) => props.theme.space.xxl}; -`; - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDetailButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const NoCollectiblesText = styled.p((props) => ({ - ...props.theme.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: 'auto', - textAlign: 'center', -})); - -const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ - display: props.isGalleryOpen ? 'block' : 'flex', - flexDirection: props.isGalleryOpen ? 'row' : 'column', - alignItems: props.isGalleryOpen ? 'flex-start' : 'center', -})); - -const SatRangeContainer = styled.div((props) => ({ - marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, - maxWidth: '1224px', - marginLeft: 'auto', - marginRight: 'auto', - width: '100%', -})); - -const DetailSection = styled.div((props) => ({ - display: 'flex', - flexDirection: props.isGalleryOpen ? 'column' : 'row', - justifyContent: 'space-between', - columnGap: props.theme.space.m, - width: '100%', -})); - -const SeeRarityContainer = styled.div` - padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; -`; - function RareSatsBundle() { const { t } = useTranslation('translation'); const navigate = useNavigate(); @@ -170,7 +53,7 @@ function RareSatsBundle() { const { network } = useWalletSelector(); const { selectedSatBundle: bundle } = useNftDataSelector(); const { isPending, pendingTxHash } = usePendingOrdinalTxs(bundle?.txid); - const [showSendOrdinalsAlert, setShowSendOrdinalsAlert] = useState(false); + const [showSendOrdinalsAlert, setShowSendOrdinalsAlert] = useState(false); const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); @@ -201,14 +84,16 @@ function RareSatsBundle() { return setShowSendOrdinalsAlert(true); } + const link = `/nft-dashboard/ordinal-detail/${bundle?.txid}/send-ordinal?isRareSat=true&vout=${bundle?.vout}`; + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard/send-rare-sat'), + url: chrome.runtime.getURL(`options.html#${link}`), }); return; } - navigate('/nft-dashboard/send-rare-sat'); + navigate(link); }; const handleRedirectToTx = () => { @@ -239,12 +124,12 @@ function RareSatsBundle() { {isGalleryOpen && ( - + )} @@ -257,10 +142,10 @@ function RareSatsBundle() { {!isGalleryOpen && } - } - text={t('COMMON.SEND')} - onPress={handleSendOrdinal} + title={t('COMMON.SEND')} + onClick={handleSendOrdinal} /> {isGalleryOpen && ( @@ -284,10 +169,10 @@ function RareSatsBundle() { )} {!isGalleryOpen && ( - )} diff --git a/src/app/screens/sendBrc20OneStep/index.tsx b/src/app/screens/sendBrc20OneStep/index.tsx index 5d42c4403..327df29ef 100644 --- a/src/app/screens/sendBrc20OneStep/index.tsx +++ b/src/app/screens/sendBrc20OneStep/index.tsx @@ -16,23 +16,24 @@ import { isDangerFeedback, type InputFeedbackProps } from '@ui-library/inputFeed import type { Brc20TransferEstimateFeesParams, ConfirmBrc20TransferState } from '@utils/brc20'; import { isInOptions, replaceCommaByDot } from '@utils/helper'; import { getFtTicker } from '@utils/tokens'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import Brc20TransferForm from './brc20TransferForm'; function SendBrc20Screen() { const { t } = useTranslation('translation', { keyPrefix: 'SEND_BRC20' }); const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const location = useLocation(); const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { network } = useWalletSelector(); const { data: brc20CoinsList } = useGetBrc20FungibleTokens(); const { data: feeRate } = useBtcFeeRate(); + const [amountToSend, setAmountToSend] = useState(location.state?.amount || ''); const [amountError, setAmountError] = useState(null); - const [amountToSend, setAmountToSend] = useState(''); + const [recipientAddress, setRecipientAddress] = useState(location.state?.recipientAddress || ''); const [recipientError, setRecipientError] = useState(null); - const [recipientAddress, setRecipientAddress] = useState(''); const [processing, setProcessing] = useState(false); const transactionContext = useTransactionContext(); @@ -42,10 +43,6 @@ function SendBrc20Screen() { const principal = searchParams.get('principal'); const fungibleToken = brc20CoinsList?.find((coin) => coin.principal === principal); - if (!fungibleToken) { - navigate('/'); - return null; - } const isNextEnabled = !isDangerFeedback(amountError) && @@ -54,7 +51,7 @@ function SendBrc20Screen() { amountToSend !== ''; const handleBackButtonClick = () => { - navigate(-1); + navigate(`/coinDashboard/FT?ftKey=${fungibleToken?.principal}&protocol=brc-20`); }; const validateAmount = (amountInput: string): boolean => { @@ -72,15 +69,6 @@ function SendBrc20Screen() { return true; }; - const onInputChange = (e: React.FormEvent) => { - const newValue = e.currentTarget.value; - const resultRegex = /^\d*\.?\d*$/; - if (resultRegex.test(newValue)) { - validateAmount(newValue); - setAmountToSend(newValue); - } - }; - const validateRecipientAddress = (address: string): boolean => { if (!address) { setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); @@ -103,6 +91,24 @@ function SendBrc20Screen() { return true; }; + useEffect(() => { + if (location.state?.amount) { + validateAmount(location.state.amount); + } + if (location.state?.recipientAddress) { + validateRecipientAddress(location.state.recipientAddress); + } + }, [location.state]); + + const onInputChange = (e: React.FormEvent) => { + const newValue = e.currentTarget.value; + const resultRegex = /^\d*\.?\d*$/; + if (resultRegex.test(newValue)) { + validateAmount(newValue); + setAmountToSend(newValue); + } + }; + const onAddressInputChange = (e: React.ChangeEvent) => { validateRecipientAddress(e.target.value); setRecipientAddress(e.target.value); @@ -111,6 +117,7 @@ function SendBrc20Screen() { const handleOnPressNext = async () => { try { if ( + !fungibleToken || !validateAmount(amountToSend) || !validateRecipientAddress(recipientAddress) || !feeRate @@ -151,6 +158,11 @@ function SendBrc20Screen() { } }; + if (!fungibleToken) { + navigate('/'); + return null; + } + return ( <> diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index 0ad9ebdf5..b0c8b29a0 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -21,14 +21,19 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import StepDisplay from './stepDisplay'; import { getPreviousStep, Step } from './steps'; -function SendRuneScreen() { +function SendOrdinalScreen() { const navigate = useNavigate(); const isInOption = isInOptions(); - const context = useTransactionContext(); const location = useLocation(); const { id } = useParams(); - const { data: selectedOrdinal } = useAddressInscription(id!); + const params = new URLSearchParams(location.search); + const isRareSatParam = params.get('isRareSat'); + const vout = params.get('vout'); + const isRareSat = isRareSatParam === 'true'; + + const context = useTransactionContext(); + const { data: selectedOrdinal } = useAddressInscription(isRareSat ? undefined : id); const selectedAccount = useSelectedAccount(); const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); const [currentStep, setCurrentStep] = useState(Step.SelectRecipient); @@ -49,7 +54,7 @@ function SendRuneScreen() { useResetUserFlow(RoutePaths.SendOrdinal); useEffect(() => { - if (!selectedOrdinal) { + if (!selectedOrdinal && !isRareSat) { return; } if (!feeRate && btcFeeRate && !feeRatesLoading) { @@ -70,11 +75,18 @@ function SendRuneScreen() { setIsLoading(true); setInsufficientFundsError(false); try { - const transactionDetails = await btcTransaction.sendOrdinalsWithSplit( - context, - [{ toAddress: recipientAddress, inscriptionId: id! }], - Number(feeRate), - ); + const transactionDetails = isRareSat + ? await btcTransaction.sendOrdinals( + context, + [{ toAddress: recipientAddress, outpoint: `${id}:${vout}` }], + Number(feeRate), + ) + : await btcTransaction.sendOrdinalsWithSplit( + context, + [{ toAddress: recipientAddress, inscriptionId: id! }], + Number(feeRate), + ); + if (!isActiveEffect) return; if (!transactionDetails) return; setTransaction(transactionDetails); @@ -111,7 +123,7 @@ function SendRuneScreen() { }; }, [context, recipientAddress, feeRate, id]); - if (!selectedOrdinal) { + if (!selectedOrdinal && !isRareSat) { navigate('/'); return null; } @@ -121,7 +133,11 @@ function SendRuneScreen() { window.close(); return; } - navigate(`/nft-dashboard/ordinal-detail/${selectedOrdinal?.id}`); + navigate( + isRareSat + ? `/nft-dashboard/rare-sats-bundle` + : `/nft-dashboard/ordinal-detail/${selectedOrdinal?.id}`, + ); }; const handleBackButtonClick = () => { @@ -133,11 +149,18 @@ function SendRuneScreen() { }; const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { - const transactionDetails = await btcTransaction.sendOrdinalsWithSplit( - context, - [{ toAddress: recipientAddress, inscriptionId: id! }], - desiredFeeRate, - ); + const transactionDetails = isRareSat + ? await btcTransaction.sendOrdinals( + context, + [{ toAddress: recipientAddress, outpoint: `${id}:${vout}` }], + desiredFeeRate, + ) + : await btcTransaction.sendOrdinalsWithSplit( + context, + [{ toAddress: recipientAddress, inscriptionId: id! }], + desiredFeeRate, + ); + if (!transactionDetails) return; const txSummary = await transactionDetails.getSummary(); if (txSummary) return Number(txSummary.fee); @@ -202,4 +225,4 @@ function SendRuneScreen() { ); } -export default SendRuneScreen; +export default SendOrdinalScreen; diff --git a/src/app/screens/sendOrdinal/stepDisplay.tsx b/src/app/screens/sendOrdinal/stepDisplay.tsx index 231d5ec97..b7be5811e 100644 --- a/src/app/screens/sendOrdinal/stepDisplay.tsx +++ b/src/app/screens/sendOrdinal/stepDisplay.tsx @@ -1,10 +1,16 @@ import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small_over_card.svg'; import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import RecipientSelector from '@components/recipientSelector'; +import useTextOrdinalContent from '@hooks/useTextOrdinalContent'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; -import type { Inscription, RuneSummary } from '@secretkeylabs/xverse-core'; +import { getBrc20Details, type Inscription, type RuneSummary } from '@secretkeylabs/xverse-core'; import Avatar from '@ui-library/avatar'; +import { + getInscriptionsCollectionGridItemSubText, + getInscriptionsCollectionGridItemSubTextColor, +} from '@utils/inscriptions'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import SendLayout from '../../layouts/sendLayout'; @@ -32,7 +38,7 @@ const Container = styled.div` type Props = { summary: TransactionSummary | undefined; runeSummary: RuneSummary | undefined; - ordinal: Inscription; + ordinal?: Inscription; currentStep: Step; setCurrentStep: (step: Step) => void; recipientAddress: string; @@ -68,12 +74,37 @@ function StepDisplay({ }: Props) { const { t } = useTranslation('translation'); - const header = ( + const textContent = useTextOrdinalContent(ordinal); + const contentType = ordinal?.content_type ?? ''; + const brc20Details = useMemo( + () => getBrc20Details(textContent!, contentType), + [textContent, contentType], + ); + const brc20Status = getInscriptionsCollectionGridItemSubText(ordinal); + const brc20StatusColor = getInscriptionsCollectionGridItemSubTextColor(ordinal); + const brc20Summary = brc20Details + ? { + ...brc20Details, + status: brc20Status, + statusColor: brc20StatusColor, + } + : undefined; + + let header: React.ReactNode = ( - } /> - {t('SEND.SEND')} Ordinal + {t('SEND.SEND_TO')} ); + + if (ordinal) { + header = ( + + } /> + {t('SEND.SEND')} Ordinal + + ); + } + switch (currentStep) { case Step.SelectRecipient: return ( @@ -100,6 +131,7 @@ function StepDisplay({ props.theme.space.l}; -`; - -const InputGroup = styled.div` - margin-top: ${(props) => props.theme.spacing(8)}px; -`; - -const Label = styled.label((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, - display: 'flex', - flex: 1, -})); - -const AmountInputContainer = styled.div<{ error: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.space.xs, - marginBottom: props.theme.space.xs, - border: props.error - ? `1px solid ${props.theme.colors.danger_dark_200}` - : `1px solid ${props.theme.colors.white_800}`, - backgroundColor: props.theme.colors.elevation_n1, - borderRadius: props.theme.radius(1), - paddingLeft: props.theme.space.s, - paddingRight: props.theme.space.s, - height: 44, -})); - -const InputFieldContainer = styled.div(() => ({ - flex: 1, -})); - -const InputField = styled.input((props) => ({ - ...props.theme.typography.body_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - width: '100%', - border: 'transparent', -})); - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.space.xs, - marginBottom: props.theme.space.l, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const StyledCallout = styled(Callout)` - margin-bottom: ${(props) => props.theme.spacing(14)}px; -`; - -function SendOrdinal() { - const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); - const navigate = useNavigate(); - const { selectedSatBundle } = useNftDataSelector(); - const btcClient = useBtcClient(); - const location = useLocation(); - const selectedAccount = useSelectedAccount(); - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - - const { getSeed } = useSeedVault(); - const [ordinalUtxo, setOrdinalUtxo] = useState(undefined); - const [recipientAddress, setRecipientAddress] = useState(''); - const [recipientError, setRecipientError] = useState(null); - useResetUserFlow('/send-rare-sat'); - - const { - isLoading, - data, - error: txError, - mutate, - } = useMutation({ - mutationFn: async (recipient) => { - const addressUtxos = await btcClient.getUnspentUtxos(selectedAccount.ordinalsAddress); - const ordUtxo = addressUtxos.find( - (utxo) => - `${utxo.txid}:${utxo.vout}` === `${selectedSatBundle?.txid}:${selectedSatBundle?.vout}`, - ); - setOrdinalUtxo(ordUtxo); - if (ordUtxo) { - const seedPhrase = await getSeed(); - const signedTx = await signOrdinalSendTransaction( - recipient, - ordUtxo, - selectedAccount.btcAddress, - Number(selectedAccount.id), - seedPhrase, - btcClient, - network.type, - [ordUtxo], - ); - return signedTx; - } - }, - }); - - useEffect(() => { - if (data) { - navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedSatBundle?.txid}`, { - state: { - signedTxHex: data.signedTx, - recipientAddress, - fee: data.fee, - feePerVByte: data.feePerVByte, - fiatFee: getBtcFiatEquivalent(data.fee, new BigNumber(btcFiatRate)), - total: data.total, - fiatTotal: getBtcFiatEquivalent(data.total, new BigNumber(btcFiatRate)), - ordinalUtxo, - isRareSat: true, - }, - }); - } - }, [data]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (txError) { - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setRecipientError({ variant: 'danger', message: t('ERRORS.INSUFFICIENT_BALANCE') }); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setRecipientError({ - variant: 'danger', - message: t('ERRORS.INSUFFICIENT_BALANCE_FEES'), - }); - } else setRecipientError({ variant: 'danger', message: txError.toString() }); - } - }, [txError]); // eslint-disable-line react-hooks/exhaustive-deps - - const handleBackButtonClick = () => { - navigate(-1); - }; - - const validateRecipientAddress = (address: string): boolean => { - if (!address) { - setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); - return false; - } - if ( - !validateBtcAddress({ - btcAddress: address, - network: network.type, - }) - ) { - setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_INVALID') }); - return false; - } - if (address === selectedAccount.ordinalsAddress || address === selectedAccount.btcAddress) { - setRecipientError({ variant: 'info', message: t('YOU_ARE_TRANSFERRING_TO_YOURSELF') }); - return true; - } - setRecipientError(null); - return true; - }; - - const onPressNext = async () => { - if (validateRecipientAddress(recipientAddress)) { - mutate(recipientAddress); - } - }; - - const handleAddressChange = (e: React.ChangeEvent) => { - validateRecipientAddress(e.target.value); - setRecipientAddress(e.target.value); - }; - - const isNextEnabled = !isDangerFeedback(recipientError) && !!recipientAddress; - - // hide back button if there is no history - const hideBackButton = location.key === 'default'; - - return ( - - -
- - {t('SEND_TO')} - - - - - - - - - - - - {recipientError && } - - - -
- - - -
-
- ); -} - -export default SendOrdinal; diff --git a/src/locales/en.json b/src/locales/en.json index f5d85e940..9691b86c8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -693,7 +693,8 @@ "VIEW_ADDRESS": "View address", "ADD_STACKS_ADDRESS": "Add a Stacks address", "NFTS": "NFTs", - "RARE_SATS": "Rare Sats", + "RARE_SAT": "Rare sat", + "RARE_SATS": "Rare sats", "NEW_FEATURE": "New Feature, Rare Sats", "NEW_FEAT_RARE_SATS_DESCRIPTION": "Enable rare sats to display rare, interesting and unique sats in your wallet. You can change this setting at any time.", "NEW_FEAT_RARE_SATS_ORDINALS_ENABLE": "Enable rare sats to display rare, interesting and unique sats in your wallet. Note than enabling rare sats will also enable Ordinals. You can change this setting at any time.", diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 5b9ccb5dd..d72781f88 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -160,7 +160,7 @@ export default class Wallet { readonly inputMemo: Locator; - readonly inputRecipientAdress: Locator; + readonly inputRecipientAddress: Locator; readonly inputSendAmount: Locator; @@ -172,7 +172,7 @@ export default class Wallet { readonly containerFeeRate: Locator; - readonly inputBTCAdress: Locator; + readonly inputBTCAddress: Locator; readonly coinBalance: Locator; @@ -472,7 +472,7 @@ export default class Wallet { // Send this.inputSendAmount = page.getByTestId('send-input'); - this.inputRecipientAdress = page.getByTestId('recipient-adress'); + this.inputRecipientAddress = page.getByTestId('recipient-address'); this.inputMemo = page.getByTestId('memo-input'); this.errorMessageAddressInvalid = page .locator('p') @@ -487,7 +487,7 @@ export default class Wallet { this.errorInsufficientBalance = page.locator('p').filter({ hasText: 'Insufficient balance' }); this.errorInsufficientFunds = page.locator('p').filter({ hasText: 'Insufficient funds' }); this.containerFeeRate = page.getByTestId('feerate-container'); - this.inputBTCAdress = page.locator('input[type="text"]'); + this.inputBTCAddress = page.locator('input[type="text"]'); this.inputBTCAmount = page.getByTestId('btc-amount'); this.buttonExpand = page.getByRole('button', { name: 'Inputs & Outputs Dropdown' }); this.confirmTotalAmount = page.getByTestId('confirm-total-amount'); @@ -547,13 +547,13 @@ export default class Wallet { await this.checkVisualsStartpage(); if (testnet) { await this.navigationSettings.click(); - await this.switchtoTestnetNetwork(); + await this.switchToTestnetNetwork(); await this.navigationDashboard.click(); - await this.checkVisualsStartpage('testnet'); + await this.checkVisualsStartpage(); } } - async checkVisualsStartpage(network?: string) { + async checkVisualsStartpage() { await expect(this.balance).toBeVisible(); await expect(this.manageTokenButton).toBeVisible(); @@ -562,23 +562,6 @@ export default class Wallet { await this.buttonDenyDataCollection.click(); } - /* -TODO: needs to be changed to be debending on network and feature enabled -const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); - - const featureFlags = await getXverseApiClient('Mainnet').getAppFeatures(); - const featureEnabled = featureFlags?.SWAPS?.enabled; - switch (true) { - case network === 'testnet': - // Check if all 3 buttons (send, receive, buy) are visible - await expect(this.allupperButtons).toHaveCount(3); - break; - default: - // Check if all 4 buttons (send, receive, swap, buy) are visible - await expect(this.allupperButtons).toHaveCount(4); - } -*/ - // await expect(this.allupperButtons).toHaveCount(3); await expect(this.labelAccountName).toBeVisible(); await expect(this.buttonMenu).toBeVisible(); await expect(await this.labelTokenSubtitle.count()).toBeGreaterThanOrEqual(2); @@ -628,7 +611,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await expect(this.buttonBack).toBeVisible(); } - async checkVisualsSendInscriptionsPage2(ordinalAddress, ordinalNumber) { + async checkVisualsSendInscriptionsPage2(ordinalAddress, ordinalNumber, collection) { await expect(this.confirmTotalAmount).toBeVisible(); await expect(this.confirmCurrencyAmount).toBeVisible(); await expect(this.buttonExpand).toBeVisible(); @@ -645,9 +628,12 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await expect(this.confirmBalance.first()).toBeVisible(); await expect(await this.receiveAddress.first().innerText()).toContain(ordinalAddress.slice(-4)); + // Collection Inscriptions don't have the ordinal number displayed in the Review // Check if the right ordinal number is shown - const reviewNumberOrdinal = await this.numberInscription.first().innerText(); - await expect(ordinalNumber).toMatch(reviewNumberOrdinal); + if (!collection) { + const reviewNumberOrdinal = await this.numberInscription.first().innerText(); + await expect(ordinalNumber).toMatch(reviewNumberOrdinal); + } } async checkVisualsSendPage1(url) { @@ -729,7 +715,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await expect(this.runeContainer.first()).toBeVisible(); } - async invalidAdressCheck(adressfield) { + async invalidAddressCheck(adressfield) { await adressfield.fill(`Test Address 123`); await this.buttonNext.click(); await expect(this.errorMessageAddressInvalid).toBeVisible(); @@ -897,7 +883,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await Promise.all(checks); } - async switchtoTestnetNetwork() { + async switchToTestnetNetwork() { await expect(this.buttonNetwork).toBeVisible(); await expect(this.buttonNetwork).toHaveText('NetworkMainnet'); await this.buttonNetwork.click(); @@ -920,7 +906,7 @@ const { getXverseApiClient } = require('@secretkeylabs/xverse-core'); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); } - async switchtoMainnetNetwork() { + async switchToMainnetNetwork() { await expect(this.buttonNetwork).toBeVisible(); await expect(this.buttonNetwork).toHaveText('NetworkTestnet'); await this.buttonNetwork.click(); diff --git a/tests/specs/runesSend.spec.ts b/tests/specs/runesSend.spec.ts index 4d8cf4785..5f65cb769 100644 --- a/tests/specs/runesSend.spec.ts +++ b/tests/specs/runesSend.spec.ts @@ -59,7 +59,7 @@ test.describe('Send runes', () => { await wallet.checkVisualsSendPage1('send-rune'); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); @@ -90,7 +90,7 @@ test.describe('Send runes', () => { await wallet.confirmSendTransaction(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await wallet.checkAndClickOnSpecificRune(runeName); const BalanceAmount = await wallet.checkVisualsRunesDashboard(runeName); diff --git a/tests/specs/runesSendCancel.spec.ts b/tests/specs/runesSendCancel.spec.ts index 45681e275..c6ebd9b61 100644 --- a/tests/specs/runesSendCancel.spec.ts +++ b/tests/specs/runesSendCancel.spec.ts @@ -25,7 +25,7 @@ test.describe('Send runes', () => { await wallet.checkVisualsSendPage1('send-rune'); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); @@ -57,7 +57,7 @@ test.describe('Send runes', () => { await wallet.buttonCancel.click(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await wallet.checkAndClickOnSpecificRune(runeName); const BalanceAmount = await wallet.checkVisualsRunesDashboard(runeName); diff --git a/tests/specs/tabCollectiblesInscriptions.spec.ts b/tests/specs/tabCollectiblesInscriptions.spec.ts index a96cfd558..9f15962d8 100644 --- a/tests/specs/tabCollectiblesInscriptions.spec.ts +++ b/tests/specs/tabCollectiblesInscriptions.spec.ts @@ -51,7 +51,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); // Check Info message await wallet.receiveAddress.fill(addressOrdinals); @@ -62,7 +62,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await wallet.buttonNext.click(); // Transaction Review Page - await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal); + await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal, true); // Cancel the transaction await wallet.buttonCancel.click(); @@ -135,14 +135,14 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); await wallet.receiveAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); // Transaction Review Page - await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal); + await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal, true); await wallet.confirmSendTransaction(); @@ -199,7 +199,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.invalidAdressCheck(wallet.receiveAddress); + await wallet.invalidAddressCheck(wallet.receiveAddress); // Check Info message await wallet.receiveAddress.fill(addressOrdinals); @@ -210,7 +210,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await wallet.buttonNext.click(); // Transaction Review Page - await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal); + await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal, false); // Cancel the transaction await wallet.buttonCancel.click(); @@ -274,7 +274,7 @@ test.describe('Collectibles Tab - Inscriptions', () => { await wallet.buttonNext.click(); // Transaction Review Page - await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal); + await wallet.checkVisualsSendInscriptionsPage2(TEST_ORDINALS_ADDRESS, orgNumberOrdinal, false); await wallet.confirmSendTransaction(); diff --git a/tests/specs/tabCollectiblesRareSats.spec.ts b/tests/specs/tabCollectiblesRareSats.spec.ts index e5a36dd4d..7eea7c1eb 100644 --- a/tests/specs/tabCollectiblesRareSats.spec.ts +++ b/tests/specs/tabCollectiblesRareSats.spec.ts @@ -30,6 +30,9 @@ test.describe('Collectibles Tab - Rare sats', () => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); + // get own Ordinals Address for address check on review page + const addressOrdinals = await wallet.getAddress(1); + // Navigate to Collectibles tab await wallet.navigateToCollectibles(); // Click on Rare Sats in Tab list @@ -49,42 +52,35 @@ test.describe('Collectibles Tab - Rare sats', () => { // Address invalid check const inputAddress = page.locator('input'); - await inputAddress.fill(`Test Address 123`); - await expect(wallet.errorMessageAddressInvalid).toBeVisible(); - await expect(wallet.buttonNext).toBeDisabled(); - await inputAddress.fill(TEST_ORDINALS_ADDRESS); + await wallet.invalidAddressCheck(inputAddress); + await inputAddress.fill(addressOrdinals); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); // Transaction Review Page - await expect(wallet.receiveAddress.first()).toBeVisible(); - await expect(await wallet.receiveAddress.first().innerText()).toContain( - TEST_ORDINALS_ADDRESS.slice(-4), + await wallet.checkVisualsSendTransactionReview( + 'send-ordinal', + addressOrdinals, + addressOrdinals, ); - await expect(wallet.buttonCancel).toBeEnabled(); - await expect(wallet.buttonConfirm).toBeEnabled(); // Cancel the transaction - // TODO: Enable the following code when this is fixed: https://linear.app/xverseapp/issue/ENG-4305/cancel-transaction-functionality-is-inconsistent-between-stx-and-btc - /* await wallet.buttonCancel.click(); - // TODO: Check where the cancel button leads the user - // Navigate to Collectibles tab - await wallet.navigateToCollectibles(); - // Click on Rare Sats in Tab list - await wallet.tabsCollectiblesItems.nth(2).click(); + await expect(wallet.buttonBack).toBeVisible(); + await wallet.buttonBack.click(); await expect(wallet.containerRareSats).toBeVisible(); - // at least two Rare Sats should be visible - const childCRareStats = await wallet.containerRareSats.getByRole('button').count(); - await expect(childCRareStats).toBeGreaterThan(2); - */ + const childCRareStats2 = await wallet.containerRareSats.getByRole('button').count(); + await expect(childCRareStats2).toBeGreaterThan(2); }); test('Send rare stats testnet #localexecution', async ({ page, extensionId }) => { const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); + // get own Ordinals Address for address check on review page + const addressOrdinals = await wallet.getAddress(1); + // Navigate to Collectibles tab await wallet.navigateToCollectibles(); // Click on Rare Sats in Tab list @@ -102,22 +98,19 @@ test.describe('Collectibles Tab - Rare sats', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); - // Address invalid check + // Address input const inputAddress = page.locator('input'); - await inputAddress.fill(`Test Address 123`); - await expect(wallet.errorMessageAddressInvalid).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); await inputAddress.fill(TEST_ORDINALS_ADDRESS); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); // Transaction Review Page - await expect(wallet.receiveAddress.first()).toBeVisible(); - await expect(await wallet.receiveAddress.first().innerText()).toContain( - TEST_ORDINALS_ADDRESS.slice(-4), + await wallet.checkVisualsSendTransactionReview( + 'send-ordinal', + addressOrdinals, + addressOrdinals, ); - await expect(wallet.buttonCancel).toBeEnabled(); - await expect(wallet.buttonConfirm).toBeEnabled(); await wallet.confirmSendTransaction(); diff --git a/tests/specs/tabSettings.spec.ts b/tests/specs/tabSettings.spec.ts index db35a22c3..c24057c56 100644 --- a/tests/specs/tabSettings.spec.ts +++ b/tests/specs/tabSettings.spec.ts @@ -43,7 +43,7 @@ test.describe('Settings Tab', () => { await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 2'); await page.goto(`chrome-extension://${extensionId}/popup.html#/settings`); - await wallet.switchtoTestnetNetwork(); + await wallet.switchToTestnetNetwork(); await wallet.navigationDashboard.click(); await expect(wallet.labelAccountName).toHaveText('Account 1'); @@ -51,10 +51,10 @@ test.describe('Settings Tab', () => { await wallet.labelAccountName.click(); await expect(page.url()).toContain('account-list'); await wallet.labelAccountName.last().click(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); await expect(wallet.labelAccountName).toHaveText('Account 2'); await page.goto(`chrome-extension://${extensionId}/popup.html#/settings`); - await wallet.switchtoMainnetNetwork(); + await wallet.switchToMainnetNetwork(); await wallet.navigationDashboard.click(); await expect(wallet.labelAccountName).toHaveText('Account 1'); }); diff --git a/tests/specs/transactionBTC.spec.ts b/tests/specs/transactionBTC.spec.ts index 35de66d07..32d9020b1 100644 --- a/tests/specs/transactionBTC.spec.ts +++ b/tests/specs/transactionBTC.spec.ts @@ -28,17 +28,17 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Address invalid check - await wallet.inputBTCAdress.fill(`Test Address 123`); + await wallet.inputBTCAddress.fill(`Test Address 123`); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.errorMessageAddressInvalid).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in own Address to check info message - await wallet.inputBTCAdress.fill(selfBTC); + await wallet.inputBTCAddress.fill(selfBTC); await expect(wallet.buttonNext).toBeEnabled(); await expect(wallet.infoMessageSendSelf).toBeVisible(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCMain); + await wallet.inputBTCAddress.fill(BTCMain); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); // Check visuals from 2 page (send BTC) @@ -73,7 +73,7 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCTest); + await wallet.inputBTCAddress.fill(BTCTest); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.containerFeeRate).toBeVisible(); @@ -107,7 +107,7 @@ test.describe('Transaction BTC', () => { await wallet.buttonCancel.click(); // Check Startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check BTC Balance after cancel the transaction const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); @@ -134,7 +134,7 @@ test.describe('Transaction BTC', () => { await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.buttonNext).toBeDisabled(); // Fill in correct Receiver Address - await wallet.inputBTCAdress.fill(BTCTest); + await wallet.inputBTCAddress.fill(BTCTest); await expect(wallet.buttonNext).toBeEnabled(); await wallet.buttonNext.click(); await expect(wallet.containerFeeRate).toBeVisible(); @@ -160,7 +160,7 @@ test.describe('Transaction BTC', () => { await wallet.checkAmountsSendingBTC(selfBTCTest, BTCTest, amountBTCSend); await wallet.confirmSendTransaction(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check BTC Balance after the transaction const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); diff --git a/tests/specs/transactionSTX.spec.ts b/tests/specs/transactionSTX.spec.ts index 19832bb7b..7f9633a73 100644 --- a/tests/specs/transactionSTX.spec.ts +++ b/tests/specs/transactionSTX.spec.ts @@ -96,7 +96,7 @@ test.describe('Transaction STX', () => { await wallet.buttonCancel.click(); // Check startpage - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Check STX Balance after cancel the transaction const balanceAfterCancel = await wallet.getTokenBalance('Stacks'); @@ -152,7 +152,7 @@ test.describe('Transaction STX', () => { // Confirm the transaction await wallet.confirmSendTransaction(); - await wallet.checkVisualsStartpage('testnet'); + await wallet.checkVisualsStartpage(); // Can't check amounts or transaction as E2E test is faster than the UI or API to how that transaction --> has to be checked manually }); From db14505bef9decafdbdf4bddc20a72d5a5836037 Mon Sep 17 00:00:00 2001 From: Christos Maris Date: Mon, 5 Aug 2024 23:38:56 +0300 Subject: [PATCH 152/219] [ENG-4828] Fix Swap Slippage Tolerance and Fee Rate Not Persisting on URL Change --- src/app/hooks/useSearchParamsState.ts | 20 ++++++++++++++++++++ src/app/screens/swap/quoteSummary/index.tsx | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 src/app/hooks/useSearchParamsState.ts diff --git a/src/app/hooks/useSearchParamsState.ts b/src/app/hooks/useSearchParamsState.ts new file mode 100644 index 000000000..5a88747e4 --- /dev/null +++ b/src/app/hooks/useSearchParamsState.ts @@ -0,0 +1,20 @@ +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +const useSearchParamsState = (key: string, defaultValue: T): [T, (newValue: T) => void] => { + const [searchParams, setSearchParams] = useSearchParams(); + + const paramValue = searchParams.get(key); + const initialValue = paramValue !== null ? (JSON.parse(paramValue) as T) : defaultValue; + + const [value, setValue] = useState(initialValue); + const customSetter = (newValue: T) => { + setValue(newValue); + searchParams.set(key, JSON.stringify(newValue)); + setSearchParams(searchParams); + }; + + return [value, customSetter]; +}; + +export default useSearchParamsState; diff --git a/src/app/screens/swap/quoteSummary/index.tsx b/src/app/screens/swap/quoteSummary/index.tsx index ba63b3009..67e757c80 100644 --- a/src/app/screens/swap/quoteSummary/index.tsx +++ b/src/app/screens/swap/quoteSummary/index.tsx @@ -3,6 +3,7 @@ import TopRow from '@components/topRow'; import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; import useCoinRates from '@hooks/queries/useCoinRates'; import useBtcFeeRate from '@hooks/useBtcFeeRate'; +import useSearchParamsState from '@hooks/useSearchParamsState'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowDown, ArrowRight } from '@phosphor-icons/react'; @@ -23,7 +24,6 @@ import Button from '@ui-library/button'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import { formatNumber } from '@utils/helper'; -import { trackMixPanel } from '@utils/mixpanel'; import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -178,7 +178,7 @@ export default function QuoteSummary({ }, [placeOrderError, placeUtxoOrderError]); const { data: recommendedFees } = useBtcFeeRate(); - const [feeRate, setFeeRate] = useState('0'); + const [feeRate, setFeeRate] = useSearchParamsState('feeRate', '0'); const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); useEffect(() => { @@ -198,7 +198,7 @@ export default function QuoteSummary({ toToken?.protocol === 'btc' ? 'Sats' : toToken?.symbol ?? RUNE_DISPLAY_DEFAULTS.symbol; const [showSlippageModal, setShowSlippageModal] = useState(false); - const [slippage, setSlippage] = useState(0.05); + const [slippage, setSlippage] = useSearchParamsState('slippage', 0.05); const handleSwap = async () => { if (!fromToken || !toToken) { From bff5828380496499382dc8171e1b607fafcd3896 Mon Sep 17 00:00:00 2001 From: Christos Maris Date: Mon, 5 Aug 2024 23:40:11 +0300 Subject: [PATCH 153/219] [ENG-4830] Fix app closes after clicking close button for swap transactions --- .../swap/components/psbtConfirmation/psbtConfirmation.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx index 6a406e8a8..aaa680125 100644 --- a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx +++ b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx @@ -65,7 +65,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop currency: 'BTC', errorTitle: '', error, - browserTx: true, + browserTx: false, }, }); } @@ -143,7 +143,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop txid: orderResponse.txid, currency: 'BTC', error: '', - browserTx: true, + browserTx: false, }, }); } catch (err) { @@ -155,7 +155,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop currency: 'BTC', errorTitle: '', error: err.message, - browserTx: true, + browserTx: false, }, }); } From eb37ada3b0257ad6d649d39a1be0a34cd78bc872 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Tue, 6 Aug 2024 19:07:58 +0800 Subject: [PATCH 154/219] Tx Review Screens - Bundle by output address (#440) * initial commit * fixes * remove log * commit core logic for send/transfer * remove log * touchup * touchup * touchup * touchup * fix bug when no output addresses type * touchup * fix logic * fix logic * remove outputs to own address logic * hack - fix spacing * fix spacing * remove export * fix some margins * Add a translation key --------- Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> Co-authored-by: Denys Hriaznov --- .../hooks/useParsedTxSummaryContext.ts | 116 ++++++++++---- .../confirmBtcTransaction/itemRow/amount.tsx | 2 + .../itemRow/inscriptionSatributeRow.tsx | 128 +++++++--------- .../itemRow/rareSats.tsx | 1 + .../itemRow/runeAmount.tsx | 23 ++- .../confirmBtcTransaction/receiveSection.tsx | 108 +++++++------- .../confirmBtcTransaction/sendSection.tsx | 141 +++++------------- .../transactionSummary.tsx | 10 +- .../confirmBtcTransaction/transferSection.tsx | 103 ++++++------- .../components/confirmBtcTransaction/utils.ts | 14 +- src/locales/en.json | 3 +- 11 files changed, 322 insertions(+), 327 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts b/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts index fec8b4bdd..e0bbf70cc 100644 --- a/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts +++ b/src/app/components/confirmBtcTransaction/hooks/useParsedTxSummaryContext.ts @@ -29,6 +29,15 @@ export const ParsedTxSummaryContext = createContext brc20Summary: undefined, }); +type BundledOutputs = Record< + string, + { + outputs: (btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput)[]; + netBtcAmount: number; + runeTransfers: RuneSummary['transfers']; + } +>; + export const useParsedTxSummaryContext = (): { summary: | (TransactionSummary & { dustFiltered?: boolean }) @@ -46,16 +55,22 @@ export const useParsedTxSummaryContext = (): { hasOutputScript: boolean; hasSigHashNone: boolean; validMintingRune: undefined | boolean; + showSendSection: boolean; + showTransferSection: boolean; + showReceiveSection: boolean; sendSection: { - showSendSection: boolean; + bundledOutputs: BundledOutputs; + inscriptionsFromPayment: btcTransaction.IOInscription[]; + satributesFromPayment: btcTransaction.IOSatribute[]; + }; + transferSection: { showBtcAmount: boolean; showRuneTransfers: boolean; - hasInscriptionsRareSatsInOrdinal: boolean; outputsFromOrdinal: ( | btcTransaction.TransactionOutput | btcTransaction.TransactionPubKeyOutput )[]; - inputFromOrdinal: btcTransaction.EnhancedInput[]; + inputsFromOrdinal: btcTransaction.EnhancedInput[]; inscriptionsFromPayment: btcTransaction.IOInscription[]; satributesFromPayment: btcTransaction.IOSatribute[]; }; @@ -75,7 +90,6 @@ export const useParsedTxSummaryContext = (): { const { summary, runeSummary, brc20Summary } = useContext(ParsedTxSummaryContext); const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { hasActivatedRareSatsKey } = useWalletSelector(); - const showCenotaphCallout = !!summary?.runeOp?.Cenotaph?.flaws; const hasInsufficientRunes = runeSummary?.transfers?.some((transfer) => !transfer.hasSufficientBalance) ?? false; @@ -93,6 +107,11 @@ export const useParsedTxSummaryContext = (): { (input) => !input.extendedUtxo.utxo.status.confirmed && input.walletWillSign, ) ?? false; + // defaults for non-psbt transactions + const transactionIsFinal = (summary as btcTransaction.PsbtSummary)?.isFinal ?? true; + const hasSigHashNone = (summary as btcTransaction.PsbtSummary)?.hasSigHashNone ?? false; + const hasSigHashSingle = (summary as btcTransaction.PsbtSummary)?.hasSigHashSingle ?? false; + const netBtcAmount = getNetAmount({ inputs: summary?.inputs, outputs: summary?.outputs, @@ -100,14 +119,9 @@ export const useParsedTxSummaryContext = (): { ordinalsAddress, }); - // defaults for non-psbt transactions - const transactionIsFinal = (summary as btcTransaction.PsbtSummary)?.isFinal ?? true; - const hasSigHashNone = (summary as btcTransaction.PsbtSummary)?.hasSigHashNone ?? false; - const hasSigHashSingle = (summary as btcTransaction.PsbtSummary)?.hasSigHashSingle ?? false; - - /* Send/Transfer section */ + /* Send/Transfer Section */ - const { inputFromPayment, inputFromOrdinal } = getInputsWithAssetsFromUserAddress({ + const { inputsFromPayment, inputsFromOrdinal } = getInputsWithAssetsFromUserAddress({ inputs: summary?.inputs, btcAddress, ordinalsAddress, @@ -119,10 +133,52 @@ export const useParsedTxSummaryContext = (): { ordinalsAddress, }); + const showSendBtc = netBtcAmount < 0; + // if transaction is not final, then runes will be delegated and will show up in the delegation section + const showRuneTransfers = transactionIsFinal && (runeSummary?.transfers ?? []).length > 0; + const hasInscriptionsRareSatsInOrdinal = + (!transactionIsFinal && inputsFromOrdinal.length > 0) || outputsFromOrdinal.length > 0; + + const showSendOrTransfer = showSendBtc || showRuneTransfers || hasInscriptionsRareSatsInOrdinal; + const showSendSection = showSendOrTransfer && transactionIsFinal && !hasExternalInputs; + const showTransferSection = showSendOrTransfer && (!transactionIsFinal || hasExternalInputs); + + const bundledOutputs: BundledOutputs = {}; + if (showSendSection) { + summary?.outputs.forEach((output) => { + if (output.type === 'script') return; + let destinationKey: string; + if (output.type === 'address') { + if ([ordinalsAddress, btcAddress].includes(output.address)) return; // ignore outputs to own address + destinationKey = output.address; + } else { + // TODO - we can have different destinations of the same type + destinationKey = output.type; + } + bundledOutputs[destinationKey] ||= { + outputs: [], + netBtcAmount: 0, + runeTransfers: [], + }; + if (output.inscriptions.length || (output.satributes.length && hasActivatedRareSatsKey)) { + bundledOutputs[destinationKey].outputs = + bundledOutputs[destinationKey].outputs.concat(output); + } else { + bundledOutputs[destinationKey].netBtcAmount += output.amount; + } + }); + runeSummary?.transfers.forEach((runeTransfer) => { + const outputAddress = runeTransfer.destinationAddresses[0]; + if (bundledOutputs[outputAddress]) { + bundledOutputs[outputAddress].runeTransfers = + bundledOutputs[outputAddress].runeTransfers.concat(runeTransfer); + } + }); + } // TODO move to utils const inscriptionsFromPayment: btcTransaction.IOInscription[] = []; const satributesFromPayment: btcTransaction.IOSatribute[] = []; - (transactionIsFinal ? outputsFromPayment : inputFromPayment).forEach( + (transactionIsFinal ? outputsFromPayment : inputsFromPayment).forEach( ( item: | btcTransaction.EnhancedInput @@ -134,17 +190,8 @@ export const useParsedTxSummaryContext = (): { }, ); - const showSendBtcAmount = netBtcAmount < 0; - - // if transaction is not final, then runes will be delegated and will show up in the delegation section - const showRuneTransfers = transactionIsFinal && (runeSummary?.transfers ?? []).length > 0; - - const hasInscriptionsRareSatsInOrdinal = - (!transactionIsFinal && inputFromOrdinal.length > 0) || outputsFromOrdinal.length > 0; - /* Receive section */ - const showReceiveBtcAmount = netBtcAmount > 0; const { outputsToPayment, outputsToOrdinal } = getOutputsWithAssetsToUserAddress({ outputs: summary?.outputs, btcAddress, @@ -179,6 +226,12 @@ export const useParsedTxSummaryContext = (): { // if transaction is not final, then runes will be delegated and will show up in the delegation section const showPaymentRunes = !!(transactionIsFinal && paymentRuneReceipts.length); + const showReceiveBtc = netBtcAmount > 0; + const showOrdinalSection = showOrdinalRunes || outputsToOrdinal.length > 0; + const showPaymentSection = + showReceiveBtc || showPaymentRunes || inscriptionsRareSatsInPayment.length > 0; + const showReceiveSection = showOrdinalSection || showPaymentSection; + return { summary, runeSummary, @@ -193,21 +246,26 @@ export const useParsedTxSummaryContext = (): { showCenotaphCallout, transactionIsFinal, validMintingRune, + showSendSection, + showTransferSection, + showReceiveSection, sendSection: { - showSendSection: showSendBtcAmount || showRuneTransfers || hasInscriptionsRareSatsInOrdinal, - showBtcAmount: showSendBtcAmount, + bundledOutputs, + inscriptionsFromPayment, + satributesFromPayment, + }, + transferSection: { + showBtcAmount: showSendBtc, showRuneTransfers, - hasInscriptionsRareSatsInOrdinal, + inputsFromOrdinal, outputsFromOrdinal, - inputFromOrdinal, inscriptionsFromPayment, satributesFromPayment, }, receiveSection: { - showOrdinalSection: showOrdinalRunes || outputsToOrdinal.length > 0, - showPaymentSection: - showReceiveBtcAmount || showPaymentRunes || inscriptionsRareSatsInPayment.length > 0, - showBtcAmount: showReceiveBtcAmount, + showOrdinalSection, + showPaymentSection, + showBtcAmount: showReceiveBtc, inscriptionsRareSatsInPayment, ordinalRuneReceipts, outputsToOrdinal, diff --git a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx index 1b8ca1805..709db90c9 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amount.tsx @@ -42,6 +42,8 @@ export default function Amount({ amount }: Props) { const { btcFiatRate } = useCoinRates(); const { t } = useTranslation('translation'); + if (!amount) return null; + return ( diff --git a/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx index 047911fb0..84f118497 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/inscriptionSatributeRow.tsx @@ -1,109 +1,95 @@ +import Link from '@assets/img/rareSats/link.svg'; import useWalletSelector from '@hooks/useWalletSelector'; import { btcTransaction } from '@secretkeylabs/xverse-core'; -import Divider from '@ui-library/divider'; +import { StyledP } from '@ui-library/common.styled'; +import { useTranslation } from 'react-i18next'; +import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import { getSatRangesWithInscriptions } from '../utils'; -import Amount from './amount'; import Inscription from './inscription'; import RareSats from './rareSats'; -import { StyledP } from '@ui-library/common.styled'; -import { NumericFormat } from 'react-number-format'; -import { useTranslation } from 'react-i18next'; -import Link from '@assets/img/rareSats/link.svg'; -const RowContainer = styled.div((props) => ({ - padding: `0 ${props.theme.space.m}`, -})); +const Container = styled.div` + margin-bottom: ${(props) => props.theme.space.s}; +`; -type Props = { - inscriptions: btcTransaction.IOInscription[]; - hasExternalInputs: boolean; - satributes: btcTransaction.IOSatribute[]; - amount: number; - showBottomDivider?: boolean; - showTopDivider?: boolean; - onShowInscription: (inscription: btcTransaction.IOInscription) => void; -}; +const LinkContainer = styled.div` + display: flex; + width: 32px; + justify-content: center; + margin-top: ${(props) => props.theme.space.xxxs}; + margin-bottom: -6px; +`; -const BundleHeader = styled.div((props) => ({ +const BundleHeader = styled.div<{ topMargin? }>((props) => ({ display: 'flex', flex: 1, flexDirection: 'row', justifyContent: 'space-between', - marginBottom: props.theme.space.m, + marginBottom: props.theme.space.s, + marginTop: props.topMargin ? props.theme.space.s : 0, })); -const LinkContainer = styled.div` - display: flex; - width: 32px; - justify-content: center; - margin: ${(props) => props.theme.space.xxxs} 0; -`; +type Props = { + inscriptions: btcTransaction.IOInscription[]; + satributes: btcTransaction.IOSatribute[]; + amount: number; + showAmount?: boolean; + topMargin?: boolean; + onShowInscription: (inscription: btcTransaction.IOInscription) => void; +}; function InscriptionSatributeRow({ inscriptions, - hasExternalInputs, satributes, amount, - showBottomDivider, - showTopDivider, + showAmount = false, + topMargin = false, onShowInscription, }: Props) { + const { t } = useTranslation('translation'); const { hasActivatedRareSatsKey } = useWalletSelector(); const satributesInfo = getSatRangesWithInscriptions({ satributes, inscriptions, amount, }); - const { t } = useTranslation('translation'); - // we only show rare sats if there are any and the user has enabled the feature const showRareSats = satributesInfo.totalExoticSats > 0 && hasActivatedRareSatsKey; return ( - <> - {showTopDivider && } - - {!hasExternalInputs && ( - -
+ + {showAmount && ( + + + {t('COMMON.BUNDLE')} + + ( - {t('COMMON.BUNDLE')} + {value} -
-
- {amount && ( - ( - - {value} - - )} - /> - )} -
-
- )} - {inscriptions.map((inscription, index) => ( -
- - {!!(inscriptions.length > index + 1 || showRareSats) && ( - - link - )} -
- ))} - {!hasActivatedRareSatsKey && } - {showRareSats && } -
- {showBottomDivider && } - + /> + + )} + {inscriptions.map((inscription, index) => ( +
+ + {!!(inscriptions.length > index + 1 || showRareSats) && ( + + link + + )} +
+ ))} + {showRareSats && } +
); } diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx index 361a79ada..e8e784767 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -16,6 +16,7 @@ const SatsBundleContainer = styled.div` display: flex; flex-direction: column; border-radius: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.s}; `; const SatsBundleButton = styled.button` diff --git a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx index 92e4a9573..c01ae5b74 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/runeAmount.tsx @@ -7,12 +7,14 @@ import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -const Container = styled.div` - width: 100%; - display: flex; - flex-direction: row; - align-items: 'center'; -`; +const Container = styled.div<{ topMargin?: boolean }>((props) => ({ + width: '100%', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + marginBottom: props.theme.space.s, + marginTop: props.topMargin ? props.theme.space.s : 0, +})); const AvatarContainer = styled.div` margin-right: ${(props) => props.theme.space.m}; @@ -42,14 +44,19 @@ const StyledPRight = styled(StyledP)` type Props = { rune: RuneBase; hasSufficientBalance?: boolean; + topMargin?: boolean; }; -export default function RuneAmount({ rune, hasSufficientBalance = true }: Props) { +export default function RuneAmount({ + rune, + hasSufficientBalance = true, + topMargin = false, +}: Props) { const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); const { runeName, amount, divisibility, symbol, inscriptionId } = rune; const amountWithDecimals = ftDecimals(String(amount), divisibility); return ( - + ({ flexDirection: 'column', background: props.theme.colors.elevation1, borderRadius: 12, - padding: `${props.theme.space.m} 0`, + paddingTop: props.theme.space.m, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -54,8 +54,8 @@ type Props = { function ReceiveSection({ onShowInscription }: Props) { const { t } = useTranslation('translation'); const { - hasExternalInputs, netBtcAmount, + showReceiveSection, receiveSection: { showOrdinalSection, showPaymentSection, @@ -69,20 +69,7 @@ function ReceiveSection({ onShowInscription }: Props) { }, } = useParsedTxSummaryContext(); - /** TODO - start bundling send/receive data by output addresses - * switch(output.type) === 'address' -> display `destinationAddress` - * script -> OP_RETURN - * ms -> nothing - * Each address can have 1:N bundles - * Challenge right now is how to to group the btc, runes, inscriptions data together by output address - */ - - /** Receive Data - * BTC: netBtcAmount - * Runes: paymentRuneReceipts - * Ordinals: ordinalRuneReceipts & outputsToOrdinal - * Rare Sats: inscriptionsRareSatsInPayment - */ + if (!showReceiveSection) return null; return ( <> @@ -102,34 +89,32 @@ function ReceiveSection({ onShowInscription }: Props) { {showOrdinalRunes && ordinalRuneReceipts.map((receipt) => ( - {hasExternalInputs && ( - -
- - {t('COMMON.BUNDLE')} - -
-
- ( - - {value} - - )} - /> -
-
- )} + +
+ + {t('COMMON.BUNDLE')} + +
+
+ ( + + {value} + + )} + /> +
+
))} {outputsToOrdinal.length > 0 && ( - + {outputsToOrdinal .sort((a, b) => b.inscriptions.length - a.inscriptions.length) .map((output, index) => ( @@ -137,12 +122,10 @@ function ReceiveSection({ onShowInscription }: Props) { // eslint-disable-next-line react/no-array-index-key key={index} inscriptions={output.inscriptions} - hasExternalInputs={hasExternalInputs} satributes={output.satributes} amount={output.amount} + showAmount onShowInscription={onShowInscription} - showTopDivider={Boolean(ordinalRuneReceipts.length) && index === 0} - showBottomDivider={outputsToOrdinal.length > index + 1} /> ))} @@ -162,19 +145,40 @@ function ReceiveSection({ onShowInscription }: Props) {
+ {showBtcAmount && ( + + + + )} {showPaymentRunes && paymentRuneReceipts.map((receipt) => ( + +
+ + {t('COMMON.BUNDLE')} + +
+
+ ( + + {value} + + )} + /> +
+
))} - {showBtcAmount && ( - - - - )} {inscriptionsRareSatsInPayment.length > 0 && ( - + {inscriptionsRareSatsInPayment .sort((a, b) => b.inscriptions.length - a.inscriptions.length) .map((output, index) => ( @@ -182,14 +186,10 @@ function ReceiveSection({ onShowInscription }: Props) { // eslint-disable-next-line react/no-array-index-key key={index} inscriptions={output.inscriptions} - hasExternalInputs={hasExternalInputs} satributes={output.satributes} amount={output.amount} + showAmount onShowInscription={onShowInscription} - showTopDivider={ - (Boolean(paymentRuneReceipts.length) || showBtcAmount) && index === 0 - } - showBottomDivider={inscriptionsRareSatsInPayment.length > index + 1} /> ))} diff --git a/src/app/components/confirmBtcTransaction/sendSection.tsx b/src/app/components/confirmBtcTransaction/sendSection.tsx index 10243caa9..dcb9bf4e0 100644 --- a/src/app/components/confirmBtcTransaction/sendSection.tsx +++ b/src/app/components/confirmBtcTransaction/sendSection.tsx @@ -2,9 +2,8 @@ import { useParsedTxSummaryContext } from '@components/confirmBtcTransaction/hoo import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; import type { btcTransaction } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; +import { getTruncatedAddress } from '@utils/helper'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import Amount from './itemRow/amount'; import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; @@ -22,7 +21,7 @@ const Container = styled.div((props) => ({ flexDirection: 'column', background: props.theme.colors.elevation1, borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, + paddingTop: props.theme.space.m, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -36,7 +35,7 @@ const BundleHeader = styled.div((props) => ({ flex: 1, flexDirection: 'row', justifyContent: 'space-between', - marginBottom: props.theme.space.m, + marginBottom: props.theme.space.s, })); function SendSection({ @@ -46,114 +45,56 @@ function SendSection({ }) { const { t } = useTranslation('translation'); const { - runeSummary, - netBtcAmount, - transactionIsFinal, - sendSection: { - showSendSection, - showBtcAmount, - showRuneTransfers, - hasInscriptionsRareSatsInOrdinal, - outputsFromOrdinal, - inputFromOrdinal, - inscriptionsFromPayment, - satributesFromPayment, - }, + showSendSection, + sendSection: { bundledOutputs, inscriptionsFromPayment, satributesFromPayment }, } = useParsedTxSummaryContext(); - /** TODO - start bundling send/receive data by output addresses - * switch(output.type) === 'address' -> display `destinationAddress` - * script -> OP_RETURN - * ms -> nothing - * Each address can have 1:N bundles - * Challenge right now is how to to group the btc, runes, inscriptions data together by output address - */ - - /** Send Data - * BTC: netBtcAmount - * Runes: runeSummary.transfers - * Ordinals: outputsFromOrdinal OR inputFromOrdinal (depending if tx is final) - */ - - if (!showSendSection) return null; + if (!showSendSection || !Object.entries(bundledOutputs).length) return null; return ( <> {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} - - {showBtcAmount && ( - - + {Object.entries(bundledOutputs).map(([address, bundle], index) => ( + + + + + {t('COMMON.TO')} + + + {getTruncatedAddress(address, 6)} + + + + {bundle.runeTransfers.map((runeTransfer, i) => ( + 0} + hasSufficientBalance={runeTransfer.hasSufficientBalance} + /> + ))} + {bundle.outputs.map((output) => ( + 0 || bundle.runeTransfers.length > 0} + showAmount + onShowInscription={onShowInscription} + /> + ))} - )} - {showRuneTransfers && - runeSummary?.transfers?.map((transfer, index) => ( - <> - {showBtcAmount && } - - -
- - {t('COMMON.BUNDLE')} - -
-
- ( - - {value} - - )} - /> -
-
- -
- {runeSummary?.transfers.length > index + 1 && } - - ))} - {hasInscriptionsRareSatsInOrdinal && ( - - {transactionIsFinal - ? outputsFromOrdinal.map((output, index) => ( - index + 1} - /> - )) - : inputFromOrdinal.map((input, index) => ( - index + 1} - /> - ))} - - )} -
+
+ ))} ); } diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index df5f3c811..1dff80d9b 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -69,11 +69,12 @@ function TransactionSummary({ isSubmitting, getFeeForFeeRate, onFeeRateSet, feeR const { summary, runeSummary, - hasExternalInputs, isUnconfirmedInput, hasOutputScript, showCenotaphCallout, transactionIsFinal, + showSendSection, + showTransferSection, } = useParsedTxSummaryContext(); const satsToFiat = (sats: string) => @@ -108,11 +109,8 @@ function TransactionSummary({ isSubmitting, getFeeForFeeRate, onFeeRateSet, feeR )} {hasRuneDelegation && } - {hasExternalInputs ? ( - - ) : ( - - )} + {showSendSection && } + {showTransferSection && } {!hasRuneDelegation && } diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx index 78778b0f1..44edfa9be 100644 --- a/src/app/components/confirmBtcTransaction/transferSection.tsx +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -23,7 +23,7 @@ const Container = styled.div((props) => ({ flexDirection: 'column', background: props.theme.colors.elevation1, borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, + paddingTop: props.theme.space.m, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -51,82 +51,83 @@ function TransferSection({ onShowInscription }: Props) { const { runeSummary, netBtcAmount, + showTransferSection, transactionIsFinal, - sendSection: { - showSendSection, + transferSection: { showBtcAmount, showRuneTransfers, - hasInscriptionsRareSatsInOrdinal, outputsFromOrdinal, - inputFromOrdinal, + inputsFromOrdinal, inscriptionsFromPayment, satributesFromPayment, }, } = useParsedTxSummaryContext(); - if (!showSendSection) return null; + if (!showTransferSection) return null; return ( <> {t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER')} - {showBtcAmount && ( - - - - - - - {t('CONFIRM_TRANSACTION.BTC_TRANSFER_WARNING')} - - - - )} - {showRuneTransfers && - runeSummary?.transfers?.map((transfer, index) => ( + + {showBtcAmount && ( <> - {showBtcAmount && } - - - - {runeSummary?.transfers.length > index + 1 && } + + + + + + {t('CONFIRM_TRANSACTION.BTC_TRANSFER_WARNING')} + + - ))} - {hasInscriptionsRareSatsInOrdinal && ( - - {!transactionIsFinal - ? inputFromOrdinal.map((input, index) => ( + )} + {showRuneTransfers && + runeSummary?.transfers?.map((transfer, index) => ( + <> + {showBtcAmount && index === 0 && } + + {runeSummary?.transfers.length > index + 1 && } + + ))} + {transactionIsFinal + ? outputsFromOrdinal.map((output, index) => ( + <> + {(showRuneTransfers || showBtcAmount) && index === 0 && ( + + )} index + 1} /> - )) - : outputsFromOrdinal.map((output, index) => ( + {outputsFromOrdinal.length > index + 1 && } + + )) + : inputsFromOrdinal.map((input, index) => ( + <> + {(showRuneTransfers || showBtcAmount) && index === 0 && ( + + )} index + 1} /> - ))} - - )} + {inputsFromOrdinal.length > index + 1 && } + + ))} + ); diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts index a562f9b2c..93147f812 100644 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -118,26 +118,26 @@ export const getInputsWithAssetsFromUserAddress = ({ ordinalsAddress, inputs, }: Omit): { - inputFromPayment: btcTransaction.EnhancedInput[]; - inputFromOrdinal: btcTransaction.EnhancedInput[]; + inputsFromPayment: btcTransaction.EnhancedInput[]; + inputsFromOrdinal: btcTransaction.EnhancedInput[]; } => { // we want to discard inputs that are not from user address and do not have inscriptions or satributes - const inputFromPayment: btcTransaction.EnhancedInput[] = []; - const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; + const inputsFromPayment: btcTransaction.EnhancedInput[] = []; + const inputsFromOrdinal: btcTransaction.EnhancedInput[] = []; inputs?.forEach((input) => { if (!input.inscriptions.length && !input.satributes.length) { return; } if (input.extendedUtxo.address === btcAddress) { - return inputFromPayment.push(input); + return inputsFromPayment.push(input); } if (input.extendedUtxo.address === ordinalsAddress) { - inputFromOrdinal.push(input); + inputsFromOrdinal.push(input); } }); - return { inputFromPayment, inputFromOrdinal }; + return { inputsFromPayment, inputsFromOrdinal }; }; export const getOutputsWithAssetsToUserAddress = ({ diff --git a/src/locales/en.json b/src/locales/en.json index 9691b86c8..0c93a01a0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -15,7 +15,8 @@ "CANCEL": "Cancel", "CONFIRM": "Confirm", "APPLY": "Apply", - "IN": "In" + "IN": "In", + "TO": "To" }, "UNITS": { "SATS_PER_VB": "sats/vB" From 6aca5ba8a2bf2965ef3b55fc358fac6accbea6cb Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Tue, 6 Aug 2024 14:50:47 +0200 Subject: [PATCH 155/219] Add helper function --- tests/fixtures/helpers.ts | 16 +++ tests/pages/wallet.ts | 2 +- tests/specs/swapExchange.spec.ts | 13 +-- tests/specs/swapExchangeCancel.spec.ts | 13 +-- tests/specs/swapME.spec.ts | 23 ++-- tests/specs/swapMECancel.spec.ts | 22 ++-- tests/specs/swapUSCancel.spec.ts | 143 +++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 46 deletions(-) create mode 100644 tests/fixtures/helpers.ts create mode 100644 tests/specs/swapUSCancel.spec.ts diff --git a/tests/fixtures/helpers.ts b/tests/fixtures/helpers.ts new file mode 100644 index 000000000..1c81fd2e2 --- /dev/null +++ b/tests/fixtures/helpers.ts @@ -0,0 +1,16 @@ +import { type Page } from '@playwright/test'; + +/** + * Enables cross-chain swaps for the test environment. + */ +export async function enableCrossChainSwaps(page: Page): Promise { + await page.route('https://api-3.xverse.app/v1/app-features', (route) => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + CROSS_CHAIN_SWAPS: { enabled: true }, + }), + }); + }); +} diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index b2d4bb3af..59befae44 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -811,7 +811,7 @@ export default class Wallet { } // Only 2 token should be visible await expect(await this.buttonSwapPlace.count()).toBe(2); - await expect(await this.imageToken.count()).toBe(2); + // await expect(await this.imageToken.count()).toBe(2); // Check Rune token name await expect(this.infoMessage.last()).toContainText(tokenName); diff --git a/tests/specs/swapExchange.spec.ts b/tests/specs/swapExchange.spec.ts index 0e3b0de63..d96cebe16 100644 --- a/tests/specs/swapExchange.spec.ts +++ b/tests/specs/swapExchange.spec.ts @@ -1,18 +1,11 @@ import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; import Wallet from '../pages/wallet'; test.describe('Swap Flow Exchange', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { - await page.route('https://api-3.xverse.app/v1/app-features', (route) => { - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - CROSS_CHAIN_SWAPS: { enabled: true }, - }), - }); - }); + await enableCrossChainSwaps(page); }); const marketplace = 'DotSwap'; @@ -21,7 +14,7 @@ test.describe('Swap Flow Exchange', () => { page, extensionId, }) => { - // Restore wallet and setup Testnet network + // Restore wallet const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); diff --git a/tests/specs/swapExchangeCancel.spec.ts b/tests/specs/swapExchangeCancel.spec.ts index 9bbbe1d93..846ab37b0 100644 --- a/tests/specs/swapExchangeCancel.spec.ts +++ b/tests/specs/swapExchangeCancel.spec.ts @@ -1,24 +1,17 @@ import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; import Wallet from '../pages/wallet'; test.describe('Swap Flow Exchange', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { - await page.route('https://api-3.xverse.app/v1/app-features', (route) => { - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - CROSS_CHAIN_SWAPS: { enabled: true }, - }), - }); - }); + await enableCrossChainSwaps(page); }); const marketplace = 'DotSwap'; test('Cancel exchange token via DotSwap', async ({ page, extensionId }) => { - // Restore wallet and setup Testnet network + // Restore wallet const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts index c6c864c56..828963275 100644 --- a/tests/specs/swapME.spec.ts +++ b/tests/specs/swapME.spec.ts @@ -1,27 +1,21 @@ import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; import Wallet from '../pages/wallet'; test.describe('Swap Flow ME', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { - await page.route('https://api-3.xverse.app/v1/app-features', (route) => { - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - CROSS_CHAIN_SWAPS: { enabled: true }, - }), - }); - }); + await enableCrossChainSwaps(page); }); const marketplace = 'Magic Eden'; + const token = 'MONEY'; test('Swap token via ME with standard fee mainnet #localexecution', async ({ page, extensionId, }) => { - // Restore wallet and setup Testnet network + // Restore wallet const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); @@ -49,7 +43,7 @@ test.describe('Swap Flow ME', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - await wallet.divTokenRow.first().click(); + await wallet.divTokenRow.filter({ hasText: token }).click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); @@ -72,7 +66,11 @@ test.describe('Swap Flow ME', () => { await expect(wallet.itemUTXO.first()).toBeVisible(); // click only on a UTXO with value from 1000 e(not enough funds for higher) - await wallet.itemUTXO.filter({ hasText: '1,000' }).first().locator('input').click(); + await wallet.itemUTXO + .filter({ hasText: /\b1,000\b/ }) + .first() + .locator('input') + .click(); await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.textUSD).toBeVisible(); await expect(wallet.quoteAmount).toBeVisible(); @@ -109,6 +107,5 @@ test.describe('Swap Flow ME', () => { await wallet.confirmSendTransaction(); await wallet.checkVisualsStartpage(); - // TODO: locally check if transaction was successful, might need to wait for transaction to be processed }); }); diff --git a/tests/specs/swapMECancel.spec.ts b/tests/specs/swapMECancel.spec.ts index 241bfaa71..a7ced11f4 100644 --- a/tests/specs/swapMECancel.spec.ts +++ b/tests/specs/swapMECancel.spec.ts @@ -1,24 +1,18 @@ import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; import Wallet from '../pages/wallet'; test.describe('Swap Flow ME', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { - await page.route('https://api-3.xverse.app/v1/app-features', (route) => { - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - CROSS_CHAIN_SWAPS: { enabled: true }, - }), - }); - }); + await enableCrossChainSwaps(page); }); const marketplace = 'Magic Eden'; + const token = 'MONEY'; test('Cancel swap token via ME', async ({ page, extensionId }) => { - // Restore wallet and setup Testnet network + // Restore wallet const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', false); @@ -49,7 +43,7 @@ test.describe('Swap Flow ME', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - await wallet.divTokenRow.first().click(); + await wallet.divTokenRow.filter({ hasText: token }).click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); @@ -72,7 +66,11 @@ test.describe('Swap Flow ME', () => { await expect(wallet.itemUTXO.first()).toBeVisible(); // click only on a UTXO with value from 1000 e(not enough funds for higher) - await wallet.itemUTXO.filter({ hasText: '1,000' }).first().locator('input').click(); + await wallet.itemUTXO + .filter({ hasText: /\b1,000\b/ }) + .first() + .locator('input') + .click(); await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.textUSD).toBeVisible(); await expect(wallet.quoteAmount).toBeVisible(); diff --git a/tests/specs/swapUSCancel.spec.ts b/tests/specs/swapUSCancel.spec.ts new file mode 100644 index 000000000..529ac3341 --- /dev/null +++ b/tests/specs/swapUSCancel.spec.ts @@ -0,0 +1,143 @@ +import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; +import Wallet from '../pages/wallet'; + +test.describe('Swap Flow ME', () => { + // Enables the feature flag for Swap + test.beforeEach(async ({ page }) => { + await enableCrossChainSwaps(page); + }); + + const marketplace = 'Unisat'; + const token = 'WONDERLANDWABBIT'; + + test('Cancel swap token via Unisat', async ({ page, extensionId }) => { + // Restore wallet + const wallet = new Wallet(page); + await wallet.setupTest(extensionId, 'SEED_WORDS1', false); + + // get own BTC Address + const selfBTC = await wallet.getAddress(0); + + await wallet.checkVisualsStartpage(); + + // Save initial Balance for later Balance checks + const initialBTCBalance = await wallet.getTokenBalance('Bitcoin'); + + await wallet.allUpperButtons.nth(2).click(); + await wallet.checkVisualsSwapPage(); + + // Select the first Coin + await wallet.buttonDownArrow.nth(0).click(); + + // Had problems with loading of all tokens so I check that 'Bitcoin' is loaded + await expect(wallet.labelTokenSubtitle.getByText('Bitcoin').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + await wallet.divTokenRow.first().click(); + await expect(wallet.nameToken.first()).not.toContainText('Select asset'); + await expect(wallet.imageToken.first()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // Select the second Coin + await wallet.buttonDownArrow.nth(1).click(); + // Had problems with loading of all tokens so I check that a 'DOG' is loaded + await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); + await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); + + const searchInput = page.getByRole('textbox', { name: 'Search by token name' }); + await expect(searchInput).toBeVisible(); + await searchInput.fill(token); + const tokenOption = page.getByRole('paragraph').getByText(token); + await tokenOption.first().click(); + + // await wallet.divTokenRow.filter({ hasText: token }).click(); + await expect(wallet.nameToken.last()).not.toContainText('Select asset'); + await expect(wallet.imageToken.last()).toBeVisible(); + await expect(wallet.buttonGetQuotes).toBeDisabled(); + + // tried a calculated value but had multiple problems with that, for now we stick to a specific value + const swapAmount = 0.000001; + + await wallet.fillSwapAmount(swapAmount); + + // Save rune token name + const tokenName1 = await wallet.nameToken.last().innerText(); + + await wallet.buttonGetQuotes.click(); + await expect(wallet.nameSwapPlace.last()).toBeVisible(); + await expect(wallet.quoteAmount.last()).toBeVisible(); + await expect(wallet.infoMessage.last()).toBeVisible(); + await expect(wallet.buttonSwapPlace.last()).toBeVisible(); + + await wallet.buttonSwapPlace.filter({ hasText: marketplace }).click(); + await expect(wallet.itemUTXO.first()).toBeVisible(); + + // click only on a UTXO with value from 1000 e(not enough funds for higher) + await wallet.itemUTXO.nth(1).locator('input').click(); + await expect(wallet.buttonNext).toBeVisible(); + await expect(wallet.textUSD).toBeVisible(); + await expect(wallet.quoteAmount).toBeVisible(); + + const quoteAmount = await wallet.quoteAmount.innerText(); + const numericQuoteValue = parseFloat(quoteAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toBeGreaterThan(0); + + const usdAmount = await wallet.textUSD.innerText(); + const numericUSDValueSwap = parseFloat(usdAmount.replace(/[^0-9.]/g, '')); + await expect(numericUSDValueSwap).toBeGreaterThan(0); + + await wallet.buttonNext.click(); + + await wallet.checkVisualsQuotePage(tokenName1, false, numericQuoteValue, numericUSDValueSwap); + + // We can only continue if the FeeRate is above 0 + await wallet.waitForTextAboveZero(wallet.feeAmount, 30000); + + // Save the current fee amount for comparison + const originalFee = await wallet.feeAmount.innerText(); + const numericOriginalFee = parseFloat(originalFee.replace(/[^0-9.]/g, '')); + await expect(numericOriginalFee).toBeGreaterThan(0); + + // Click on edit Fee button + await wallet.buttonEditFee.click(); + await expect(wallet.buttonSelectFee.first()).toBeVisible(); + await expect(wallet.labelTotalFee.first()).toBeVisible(); + + // Compare medium fee to previous saved fee + const mediumFee = await wallet.labelTotalFee.last().innerText(); + const numericMediumFee = parseFloat(mediumFee.replace(/[^0-9.]/g, '')); + await expect(numericMediumFee).toBe(numericOriginalFee); + + // Save high fee rate for comparison + const highFee = await wallet.labelTotalFee.first().innerText(); + const numericHighFee = parseFloat(highFee.replace(/[^0-9.]/g, '')); + + // Switch to high fee + await wallet.buttonSelectFee.first().click(); + + const newFee = await wallet.feeAmount.innerText(); + const numericNewFee = parseFloat(newFee.replace(/[^0-9.]/g, '')); + await expect(numericNewFee).toBe(numericHighFee); + + await wallet.buttonSwap.click(); + await wallet.checkVisualsSendTransactionReview('swap', false, selfBTC); + + const sendRuneAmount = await wallet.sendRuneAmount.innerText(); + const sendAmountNumerical = parseFloat(sendRuneAmount.replace(/[^0-9.]/g, '')); + await expect(numericQuoteValue).toEqual(sendAmountNumerical); + + // Check Rune token name + await expect(wallet.nameRune).toContainText(tokenName1); + + // Cancel the transaction + await expect(wallet.buttonCancel).toBeEnabled(); + await wallet.buttonCancel.click(); + + // Check Startpage + await wallet.checkVisualsStartpage(); + + // Check BTC Balance after cancel the transaction + const balanceAfterCancel = await wallet.getTokenBalance('Bitcoin'); + await expect(initialBTCBalance).toEqual(balanceAfterCancel); + }); +}); From 3d3439bc5452d73249d8f805d3ce95f450ccd60f Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Tue, 6 Aug 2024 15:22:28 +0200 Subject: [PATCH 156/219] Update Unisat Cancel flow --- tests/specs/swapUSCancel.spec.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/specs/swapUSCancel.spec.ts b/tests/specs/swapUSCancel.spec.ts index 529ac3341..3fd844495 100644 --- a/tests/specs/swapUSCancel.spec.ts +++ b/tests/specs/swapUSCancel.spec.ts @@ -2,14 +2,14 @@ import { expect, test } from '../fixtures/base'; import { enableCrossChainSwaps } from '../fixtures/helpers'; import Wallet from '../pages/wallet'; -test.describe('Swap Flow ME', () => { +test.describe('Swap Flow Unisat', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { await enableCrossChainSwaps(page); }); const marketplace = 'Unisat'; - const token = 'WONDERLANDWABBIT'; + const token = 'MAX•DOG•ONCHAIN'; test('Cancel swap token via Unisat', async ({ page, extensionId }) => { // Restore wallet @@ -43,12 +43,9 @@ test.describe('Swap Flow ME', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - - const searchInput = page.getByRole('textbox', { name: 'Search by token name' }); - await expect(searchInput).toBeVisible(); - await searchInput.fill(token); - const tokenOption = page.getByRole('paragraph').getByText(token); - await tokenOption.first().click(); + await expect(wallet.inputField).toBeVisible(); + await wallet.inputField.fill(token); + await wallet.divTokenRow.filter({ hasText: token }).first().click(); // await wallet.divTokenRow.filter({ hasText: token }).click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); @@ -73,7 +70,11 @@ test.describe('Swap Flow ME', () => { await expect(wallet.itemUTXO.first()).toBeVisible(); // click only on a UTXO with value from 1000 e(not enough funds for higher) - await wallet.itemUTXO.nth(1).locator('input').click(); + await wallet.itemUTXO + .filter({ hasText: /\b10,000\b/ }) + .first() + .locator('input') + .click(); await expect(wallet.buttonNext).toBeVisible(); await expect(wallet.textUSD).toBeVisible(); await expect(wallet.quoteAmount).toBeVisible(); From 30187aab9bf05c34b56bfef70cbd15d842ecc940 Mon Sep 17 00:00:00 2001 From: Christos Maris Date: Tue, 6 Aug 2024 14:49:35 +0300 Subject: [PATCH 157/219] [ENG-4766] Update Snackbar Styling per New Designs --- src/app/screens/home/index.tsx | 43 +++++++++++--------- src/app/screens/swap/index.tsx | 8 +--- src/app/screens/swap/utxoSelection/index.tsx | 4 +- src/app/ui-library/snackBar.tsx | 39 ++++++++++-------- 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index fd3f82e83..4e4372b71 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -147,26 +147,29 @@ function Home() { { - toast.remove(toastId); - - // set the visibility back to true - const payload = { - principal: spamToken.principal, - isEnabled: true, - }; - - if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setRunesManageTokensAction(payload)); - } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setSip10ManageTokensAction(payload)); - } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setBrc20ManageTokensAction(payload)); - } - - removeFromSpamTokens(spamToken.principal); - dispatch(setSpamTokenAction(null)); + dismissToast={() => toast.remove(toastId)} + action={{ + text: t('UNDO'), + onClick: () => { + toast.remove(toastId); + + // set the visibility back to true + const payload = { + principal: spamToken.principal, + isEnabled: true, + }; + + if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setRunesManageTokensAction(payload)); + } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setSip10ManageTokensAction(payload)); + } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setBrc20ManageTokensAction(payload)); + } + + removeFromSpamTokens(spamToken.principal); + dispatch(setSpamTokenAction(null)); + }, }} />, ); diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index 6926870d8..6f3d0f26a 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -324,13 +324,7 @@ export default function SwapScreen() { useEffect(() => { if (errorMessage) { const toastId = toast.custom( - { - toast.remove(toastId); - }} - />, + toast.remove(toastId)} />, { duration: 3000 }, ); // Reset diff --git a/src/app/screens/swap/utxoSelection/index.tsx b/src/app/screens/swap/utxoSelection/index.tsx index e785eed47..a9668e268 100644 --- a/src/app/screens/swap/utxoSelection/index.tsx +++ b/src/app/screens/swap/utxoSelection/index.tsx @@ -161,9 +161,7 @@ export default function UtxoSelection({ { - toast.remove(toastId); - }} + dismissToast={() => toast.remove(toastId)} />, { duration: 3000 }, ); diff --git a/src/app/ui-library/snackBar.tsx b/src/app/ui-library/snackBar.tsx index 4876fd9c0..18517ecd4 100644 --- a/src/app/ui-library/snackBar.tsx +++ b/src/app/ui-library/snackBar.tsx @@ -1,5 +1,4 @@ -import toast from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; +import { CheckCircle, XCircle } from '@phosphor-icons/react'; import styled from 'styled-components'; type ToastType = 'success' | 'error' | 'neutral'; @@ -7,14 +6,14 @@ type ToastType = 'success' | 'error' | 'neutral'; interface ToastProps { text: string; type: ToastType; - actionButtonText?: string; - actionButtonCallback?: () => void; + dismissToast?: () => void; + action?: { text: string; onClick: () => void }; } const getBackgroundColor = (type: ToastType, theme: any): string => { const colors = { success: theme.colors.feedback.success, - error: theme.colors.feedback.error, + error: theme.colors.danger_dark, neutral: theme.colors.white_0, }; return colors[type] || theme.colors.feedback.neutral; @@ -38,7 +37,7 @@ const ToastContainer = styled.div<{ type: ToastType }>` min-height: 44px; padding: 12px 20px; width: auto; - max-width: 306px; + max-width: 328px; align-items: center; justify-content: space-between; margin-bottom: 80px; @@ -47,7 +46,8 @@ const ToastContainer = styled.div<{ type: ToastType }>` const ToastMessage = styled.h1<{ type: ToastType }>` ${({ theme }) => theme.typography.body_medium_m}; color: ${(props) => getTextColor(props.type, props.theme)}; - margin-right: 24px; + margin-left: ${(props) => props.theme.space.s}; + margin-right: ${(props) => props.theme.space.l}; `; const ToastDismissButton = styled.h1<{ type: ToastType }>` @@ -57,20 +57,23 @@ const ToastDismissButton = styled.h1<{ type: ToastType }>` cursor: pointer; `; -function SnackBar({ text, type, actionButtonText, actionButtonCallback }: ToastProps) { - const { t } = useTranslation('translation'); - - const dismissToast = () => { - actionButtonCallback?.(); - toast.dismiss(); - }; - +function SnackBar({ text, type, dismissToast, action }: ToastProps) { return ( + {type !== 'neutral' && dismissToast && ( + + {type === 'error' && } + {type === 'success' && } + + )} + {text} - - {actionButtonText ?? t('OK')} - + + {action?.text && ( + + {action?.text} + + )} ); } From fd68eaee1257da80bde6e1400e80d847b236f1b4 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Wed, 7 Aug 2024 16:39:26 +0800 Subject: [PATCH 158/219] remove remaining legacy screens (#454) * [ENG-4347] SEND BRC-20 Transaction Review Screen - New Design * Update the brc-20 fees block * initial commit * fixes * remove log * Fix the SendOrdinalScreen screen name * Move OrdinalDetailScreen styles to a separate file, add Status column for Mint brc-20 inscriptions * Add brc20Summary, show the brc20 details on the send inscription review screen * commit core logic for send/transfer * remove log * touchup * touchup * touchup * touchup * fix bug when no output addresses type * Fix the go back logic for the brc20 sending flow * Remove the unused styled components * touchup * fix logic * [ENG-4396] SEND Rare Sats Transaction review screen - New Design * Move the styles from RareSatsBundle screen to a separate file, replace the old action buttons * fix logic * Make sendOrdinal screen handle rare sats bundle sending * Remove the outdated SendRareSat screen * Make some code tweaks * Trigger CI * remove outputs to own address logic * hack - fix spacing * fix spacing * remove export * fix some margins * Add a translation key * Update E2E test * update recovery ordinals flow * remove log * opps * touchup * remove old component/screens * commit work * commit work * remove unused path * Update the info message copy * remove unused exports --------- Co-authored-by: Denys Hriaznov Co-authored-by: Den <36603049+dhriaznov@users.noreply.github.com> Co-authored-by: Christine Pinto --- .../amountWithInscriptionSatribute.tsx | 2 +- .../itemRow}/bundleItem.tsx | 0 .../itemRow/rareSats.tsx | 2 +- .../confirmBtcTransactionComponent/bundle.tsx | 123 ----- .../confirmBtcTransactionComponent/index.tsx | 497 ------------------ src/app/hooks/useResetUserFlow.ts | 3 - src/app/routes/index.tsx | 30 +- src/app/screens/btcSendRequest/index.tsx | 229 ++++++++ .../useBtcSendRequestPayload.ts | 46 ++ src/app/screens/btcSendScreen/index.tsx | 138 ----- .../btcSendScreen/useSendBtcRequest.ts | 109 ---- .../screens/confirmBtcTransaction/index.tsx | 292 ---------- .../confirmInscriptionRequest/index.tsx | 467 ---------------- .../confirmOrdinalTransaction/index.tsx | 189 ------- .../restoreFunds/restoreOrdinals/index.tsx | 231 +++++--- .../restoreOrdinals/ordinalRow.tsx | 16 +- src/app/screens/sendBtc/index.tsx | 28 +- src/app/screens/sendBtc/stepDisplay.tsx | 8 +- src/app/screens/sendBtc/steps.tsx | 13 +- .../index.tsx} | 0 .../useSendInscriptions.ts | 0 src/app/screens/sendOrdinal/index.tsx | 2 +- src/app/utils/localStorage.ts | 11 +- src/common/types/ledger.ts | 18 +- src/locales/en.json | 2 +- 25 files changed, 454 insertions(+), 2002 deletions(-) rename src/app/components/{confirmBtcTransactionComponent => confirmBtcTransaction/itemRow}/bundleItem.tsx (100%) delete mode 100644 src/app/components/confirmBtcTransactionComponent/bundle.tsx delete mode 100644 src/app/components/confirmBtcTransactionComponent/index.tsx create mode 100644 src/app/screens/btcSendRequest/index.tsx create mode 100644 src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts delete mode 100644 src/app/screens/btcSendScreen/index.tsx delete mode 100644 src/app/screens/btcSendScreen/useSendBtcRequest.ts delete mode 100644 src/app/screens/confirmBtcTransaction/index.tsx delete mode 100644 src/app/screens/confirmInscriptionRequest/index.tsx delete mode 100644 src/app/screens/confirmOrdinalTransaction/index.tsx rename src/app/screens/{confirmOrdinalTransaction/SendInscriptionsRequest.tsx => sendInscriptionsRequest/index.tsx} (100%) rename src/app/screens/{confirmOrdinalTransaction => sendInscriptionsRequest}/useSendInscriptions.ts (100%) diff --git a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx index b7f790d4c..cf76fe908 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/amountWithInscriptionSatribute.tsx @@ -1,5 +1,4 @@ import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; import useWalletSelector from '@hooks/useWalletSelector'; import { WarningOctagon } from '@phosphor-icons/react'; import { animated, config, useSpring } from '@react-spring/web'; @@ -11,6 +10,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import Theme from 'theme'; import { mapTxSatributeInfoToBundleInfo } from '../utils'; +import BundleItem from './bundleItem'; import Inscription from './inscription'; const WarningContainer = styled.div` diff --git a/src/app/components/confirmBtcTransactionComponent/bundleItem.tsx b/src/app/components/confirmBtcTransaction/itemRow/bundleItem.tsx similarity index 100% rename from src/app/components/confirmBtcTransactionComponent/bundleItem.tsx rename to src/app/components/confirmBtcTransaction/itemRow/bundleItem.tsx diff --git a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx index e8e784767..3fdf25e27 100644 --- a/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx +++ b/src/app/components/confirmBtcTransaction/itemRow/rareSats.tsx @@ -1,5 +1,4 @@ import DropDownIcon from '@assets/img/transactions/dropDownIcon.svg'; -import BundleItem from '@components/confirmBtcTransactionComponent/bundleItem'; import { Butterfly } from '@phosphor-icons/react'; import { animated, config, useSpring } from '@react-spring/web'; import { StyledP } from '@ui-library/common.styled'; @@ -11,6 +10,7 @@ import styled from 'styled-components'; import Theme from 'theme'; import Avatar from '../../../ui-library/avatar'; import { mapTxSatributeInfoToBundleInfo, type SatRangeTx } from '../utils'; +import BundleItem from './bundleItem'; const SatsBundleContainer = styled.div` display: flex; diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx deleted file mode 100644 index 0b4c95222..000000000 --- a/src/app/components/confirmBtcTransactionComponent/bundle.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import AssetModal from '@components/assetModal'; -import { Butterfly, CaretDown } from '@phosphor-icons/react'; -import { animated, config, useSpring } from '@react-spring/web'; -import type { Bundle, BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import Theme from 'theme'; -import BundleItem from './bundleItem'; - -const BundleItemsContainer = styled.div<{ - $withMargin: boolean; -}>` - margin-top: ${(props) => (props.$withMargin ? props.theme.space.m : 0)}; -`; - -const SatsBundleContainer = styled.div` - display: flex; - flex-direction: column; - margin-bottom: ${(props) => props.theme.space.s}; - border-radius: ${(props) => props.theme.space.s}; - padding: ${(props) => props.theme.space.m}; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const SatsBundleButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const BundleTitle = styled(StyledP)` - margin-left: ${(props) => props.theme.space.s}; -`; - -const Title = styled(StyledP)((props) => ({ - marginBottom: props.theme.space.xs, -})); - -const IconContainer = styled.div` - display: flex; - align-items: center; - justify-content: center; - background-color: ${(props) => props.theme.colors.white_0}; - border-radius: 50%; - padding: 6px; -`; - -function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { - const { t } = useTranslation('translation'); - - const [showBundleDetail, setShowBundleDetail] = useState(false); - const [inscriptionToShow, setInscriptionToShow] = useState( - undefined, - ); - - const arrowRotation = useSpring({ - transform: showBundleDetail ? 'rotate(180deg)' : 'rotate(0deg)', - config: { ...config.stiff }, - }); - - return ( - <> - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={inscriptionToShow} - /> - )} - - {title && {title}} - setShowBundleDetail((prevState) => !prevState)} - > - - - - - - {`${bundle.totalExoticSats} ${t( - bundle.totalExoticSats > 1 - ? 'NFT_DASHBOARD_SCREEN.RARE_SATS' - : 'NFT_DASHBOARD_SCREEN.RARE_SAT', - )}`} - - - - - - - - - - {showBundleDetail && - bundle.satRanges.map((item: BundleSatRange, index: number) => ( - - { - // show ordinal modal to show asset - setInscriptionToShow(inscription); - }} - /> - {bundle.satRanges.length > index + 1 && } - - ))} - - - ); -} - -export default SatsBundle; diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx deleted file mode 100644 index 66a4c47d6..000000000 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import AssetIcon from '@assets/img/transactions/Assets.svg'; -import RecipientComponent from '@components/recipientComponent'; -import TransactionSettingAlert from '@components/transactionSetting'; -import TransferFeeView from '@components/transferFeeView'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; -import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import useSeedVault from '@hooks/useSeedVault'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { PencilSimple } from '@phosphor-icons/react'; -import { - ErrorCodes, - ResponseError, - getBtcFiatEquivalent, - satsToBtc, - signBtcTransaction, - signNonOrdinalBtcSendTransaction, - signOrdinalSendTransaction, - type Bundle, - type Recipient, - type SignedBtcTx, - type UTXO, -} from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import Button from '@ui-library/button'; -import Callout from '@ui-library/callout'; -import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'; -import type { CurrencyTypes } from '@utils/constants'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState, type ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import TransactionDetailComponent from '../transactionDetailComponent'; -import SatsBundle from './bundle'; - -const OuterContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const Subtitle = styled.p` - ${(props) => props.theme.typography.body_medium_m}; - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.s}; - margin-bottom: ${(props) => props.theme.space.xs}; -`; - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(24), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const ErrorText = styled.p((props) => ({ - ...props.theme.typography.body_s, - color: props.theme.colors.danger_medium, -})); - -const ReviewTransactionText = styled.span<{ - centerAligned: boolean; -}>((props) => ({ - ...props.theme.typography.headline_s, - color: props.theme.colors.white_0, - marginBottom: props.theme.space.l, - textAlign: props.centerAligned ? 'center' : 'left', -})); - -const CalloutContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(8), - marginhorizontal: props.theme.spacing(8), -})); - -const FeeContainer = styled.div` - position: relative; -`; - -const Label = styled.span` - ${(props) => props.theme.typography.body_medium_m}; - position: absolute; - top: 36px; - left: ${(props) => props.theme.space.m}; - color: ${(props) => props.theme.colors.tangerine}; - transition: color 0.1s ease; - cursor: pointer; - user-select: none; - display: inline-flex; - flex-direction: row; - align-items: center; - gap: ${(props) => props.theme.space.xxs}; - - &:hover { - color: ${(props) => props.theme.colors.tangerine_200}; - } -`; - -type Props = { - currentFee: BigNumber; - feePerVByte: BigNumber; // TODO tim: is this the same as currentFeeRate? refactor to be clear - loadingBroadcastedTx: boolean; - signedTxHex: string; - ordinalTxUtxo?: UTXO; - recipients: Recipient[]; - children?: ReactNode; - assetDetail?: string; - assetDetailValue?: string; - isRestoreFundFlow?: boolean; - nonOrdinalUtxos?: UTXO[]; - currencyType?: CurrencyTypes; - isPartOfBundle?: boolean; - ordinalBundle?: Bundle; - holdsRareSats?: boolean; - currentFeeRate: BigNumber; - setCurrentFee: (feeRate: BigNumber) => void; - setCurrentFeeRate: (feeRate: BigNumber) => void; - onConfirmClick: (signedTxHex: string) => void; - onCancelClick: () => void; -}; - -/** - * @deprecated should use ConfirmBtcTransaction master component - */ -function ConfirmBtcTransactionComponent({ - currentFee, - feePerVByte, - loadingBroadcastedTx, - signedTxHex, - ordinalTxUtxo, - recipients, - children, - assetDetail, - assetDetailValue, - isRestoreFundFlow, - nonOrdinalUtxos, - isPartOfBundle, - currencyType, - ordinalBundle, - holdsRareSats, - currentFeeRate, - setCurrentFee, - setCurrentFeeRate, - onConfirmClick, - onCancelClick, -}: Props) { - const { t } = useTranslation('translation'); - const [loading, setLoading] = useState(false); - const selectedAccount = useSelectedAccount(); - const { btcAddress } = selectedAccount; - const { network, feeMultipliers } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { selectedSatBundle } = useNftDataSelector(); - const { getSeed } = useSeedVault(); - const [showFeeSettings, setShowFeeSettings] = useState(false); - const [error, setError] = useState(''); - const [signedTx, setSignedTx] = useState(signedTxHex); - const [total, setTotal] = useState(new BigNumber(0)); - const [showFeeWarning, setShowFeeWarning] = useState(false); - const btcClient = useBtcClient(); - - const bundle = selectedSatBundle ?? ordinalBundle ?? undefined; - const { - isLoading, - data, - error: txError, - mutate, - } = useMutation< - SignedBtcTx, - ResponseError, - { - txRecipients: Recipient[]; - txFee: string; - seedPhrase: string; - } - >({ - mutationFn: async ({ txRecipients: newRecipients, txFee, seedPhrase }) => - signBtcTransaction( - newRecipients, - btcAddress, - selectedAccount?.id ?? 0, - seedPhrase, - btcClient, - network.type, - new BigNumber(txFee), - ), - }); - - if (typeof feePerVByte !== 'string' && !BigNumber.isBigNumber(feePerVByte)) { - Object.setPrototypeOf(feePerVByte, BigNumber.prototype); - } - - recipients.forEach((recipient) => { - if (typeof recipient.amountSats !== 'string' && !BigNumber.isBigNumber(recipient.amountSats)) { - Object.setPrototypeOf(recipient.amountSats, BigNumber.prototype); - } - }); - - const { - isLoading: isLoadingNonOrdinalBtcSend, - error: errorSigningNonOrdial, - data: signedNonOrdinalBtcSend, - mutate: mutateSignNonOrdinalBtcTransaction, - } = useMutation({ - mutationFn: async ({ txFee, seedPhrase }) => { - const signedNonOrdinalBtcTx = await signNonOrdinalBtcSendTransaction( - btcAddress, - nonOrdinalUtxos!, - selectedAccount?.id ?? 0, - seedPhrase, - network.type, - new BigNumber(txFee), - ); - return signedNonOrdinalBtcTx; - }, - }); - - const { ordinals, isLoading: ordinalsLoading } = useOrdinalsByAddress(btcAddress); - - const { - isLoading: isLoadingOrdData, - data: ordinalData, - error: ordinalError, - mutate: ordinalMutate, - } = useMutation({ - mutationFn: async ({ txFee, seedPhrase }) => { - const ordinalsUtxos = ordinals!.map((ord) => ord.utxo); - const newSignedTx = await signOrdinalSendTransaction( - recipients[0]?.address, - ordinalTxUtxo!, - btcAddress, - Number(selectedAccount?.id), - seedPhrase, - btcClient, - network.type, - ordinalsUtxos, - new BigNumber(txFee), - ); - return newSignedTx; - }, - }); - - useEffect(() => { - if (data) { - setCurrentFee(data.fee); - setSignedTx(data.signedTx); - setShowFeeSettings(false); - } - }, [data]); - - useEffect(() => { - if (ordinalData) { - setCurrentFee(ordinalData.fee); - setSignedTx(ordinalData.signedTx); - setShowFeeSettings(false); - } - }, [ordinalData]); - - useEffect(() => { - let sum: BigNumber = new BigNumber(0); - if (recipients?.length) { - recipients.forEach((recipient) => { - sum = sum.plus(recipient.amountSats); - }); - sum = sum?.plus(currentFee); - } - setTotal(sum); - }, [recipients, currentFee]); - - useEffect(() => { - if (signedNonOrdinalBtcSend) { - setCurrentFee(signedNonOrdinalBtcSend.fee); - setSignedTx(signedNonOrdinalBtcSend.signedTx); - setShowFeeSettings(false); - } - }, [signedNonOrdinalBtcSend]); - - useEffect(() => { - const isFeeHigh = - feeMultipliers && - currentFee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighSatsFee)); - setShowFeeWarning(!!isFeeHigh); - }, [currentFee, feeMultipliers]); - - const showEditFeesModal = () => { - setShowFeeSettings(true); - }; - - const hideEditFeesModal = () => { - setShowFeeSettings(false); - }; - - const handleApplyClick = async ({ - fee: modifiedFee, - feeRate, - }: { - fee: string; - feeRate?: string; - nonce?: string; - }) => { - const newFee = new BigNumber(modifiedFee); - setCurrentFee(newFee); - const seed = await getSeed(); - setCurrentFeeRate(new BigNumber(feeRate!)); - if (ordinalTxUtxo) ordinalMutate({ txFee: modifiedFee, seedPhrase: seed }); - else if (isRestoreFundFlow) { - mutateSignNonOrdinalBtcTransaction({ txFee: modifiedFee, seedPhrase: seed }); - } else { - mutate({ txRecipients: recipients, txFee: modifiedFee, seedPhrase: seed }); - } - setLoading(true); - }; - - const handleConfirmClick = () => { - onConfirmClick(signedTx); - }; - - const getAmountString = (amount: BigNumber, currency: string) => ( - - ); - - useEffect(() => { - if (recipients && txError) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(txError.toString()); - } - }, [txError]); - - useEffect(() => { - if (recipients && ordinalError) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(ordinalError.toString()); - } - }, [ordinalError]); - - useEffect(() => { - if (recipients && errorSigningNonOrdial) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(errorSigningNonOrdial.toString()); - } - }, [errorSigningNonOrdial]); - - return ( - <> - - {showFeeWarning && ( - - - - )} - {/* TODO tim: refactor this not to use children. it should be just another prop */} - {children} - - {t('CONFIRM_TRANSACTION.REVIEW_TRANSACTION')} - - {isPartOfBundle && ( - - - - )} - {holdsRareSats && ( - - - - )} - - {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} - - {ordinalTxUtxo ? ( - - ) : ( - recipients?.map((recipient, index) => ( - - )) - )} - - {currencyType !== 'BTC' && bundle && } - - {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} - - - - {t('CONFIRM_TRANSACTION.FEES')} - - - - - - - {!ordinalTxUtxo && recipients.length > 1 && ( - - )} - - {!!error && ( - - {error} - - )} - - - - - - - - - - - - - - - - ); -} - -export default ConfirmInscriptionRequest; diff --git a/src/app/screens/confirmOrdinalTransaction/index.tsx b/src/app/screens/confirmOrdinalTransaction/index.tsx deleted file mode 100644 index e3da4514d..000000000 --- a/src/app/screens/confirmOrdinalTransaction/index.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import type { ConfirmOrdinalsTransactionState, LedgerTransactionType } from '@common/types/ledger'; -import ConfirmBtcTransactionComponent from '@components/confirmBtcTransactionComponent'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useAddressInscription from '@hooks/queries/ordinals/useAddressInscription'; -import { useGetUtxoOrdinalBundle } from '@hooks/queries/ordinals/useAddressRareSats'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; -import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import OrdinalImage from '@screens/ordinals/ordinalImage'; -import { AnalyticsEvents, type BtcTransactionBroadcastResponse } from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import { isLedgerAccount } from '@utils/helper'; -import { trackMixPanel } from '@utils/mixpanel'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import SendLayout from '../../layouts/sendLayout'; - -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', -}); - -const NftContainer = styled.div((props) => ({ - maxWidth: 150, - maxHeight: 150, - width: '60%', - display: 'flex', - aspectRatio: 1, - justifyContent: 'center', - alignItems: 'center', - borderRadius: props.theme.radius(1), - padding: props.theme.spacing(5), - marginBottom: props.theme.space.s, -})); - -function ConfirmOrdinalTransaction() { - const selectedAccount = useSelectedAccount(); - const { hasActivatedRareSatsKey } = useWalletSelector(); - const navigate = useNavigate(); - const btcClient = useBtcClient(); - const [recipientAddress, setRecipientAddress] = useState(''); - const location = useLocation(); - - // TODO tim: refactor to not use location.state. - const { feePerVByte, signedTxHex, ordinalUtxo, isRareSat } = location.state; - // this hack is necessary because the browser back/forward buttons - // serialize BigNumber objects into plain objects - let { fee } = location.state; - if (!BigNumber.isBigNumber(fee)) { - fee = BigNumber(fee); - } - const { id } = useParams(); - const { data: selectedOrdinal } = useAddressInscription(id!); - const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); - const { refetch } = useBtcWalletData(); - const [currentFee, setCurrentFee] = useState(fee); - const [currentFeeRate, setCurrentFeeRate] = useState(feePerVByte); - - const { - isLoading, - error: txError, - data: btcTxBroadcastData, - mutate, - } = useMutation({ - mutationFn: async ({ signedTx }) => btcClient.sendRawTransaction(signedTx), - }); - - useEffect(() => { - setRecipientAddress(location.state.recipientAddress); - }, [location]); - - useEffect(() => { - if (btcTxBroadcastData) { - setSelectedSatBundleDetails(null); - navigate('/tx-status', { - state: { - txid: btcTxBroadcastData.tx.hash, - currency: 'BTC', - error: '', - isRareSat, - isOrdinal: !isRareSat, - }, - }); - setTimeout(() => { - refetch(); - }, 1000); - } - }, [btcTxBroadcastData]); - - useEffect(() => { - if (txError) { - setSelectedSatBundleDetails(null); - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: txError.toString(), - isOrdinal: true, - }, - }); - } - }, [txError]); - - const { - bundle: ordinalBundle, - isPartOfABundle, - ordinalSatributes, - } = useGetUtxoOrdinalBundle( - selectedOrdinal?.output, - hasActivatedRareSatsKey, - selectedOrdinal?.number, - ); - - const holdsRareSats = ordinalSatributes?.length > 0; - - const handleConfirmClick = (txHex: string) => { - if (isLedgerAccount(selectedAccount)) { - const txType: LedgerTransactionType = 'ORDINALS'; - const state: ConfirmOrdinalsTransactionState = { - recipients: [{ address: recipientAddress, amountSats: new BigNumber(ordinalUtxo.value) }], - type: txType, - ordinalUtxo, - feeRateInput: currentFeeRate, - fee: currentFee, - }; - - navigate('/confirm-ledger-tx', { state }); - return; - } - - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: isRareSat ? 'rare-sats' : 'ordinals', - action: 'transfer', - wallet_type: selectedAccount?.accountType || 'software', - }); - - mutate({ signedTx: txHex }); - }; - - useResetUserFlow('/confirm-ordinal-tx'); - - const handleBackButtonClick = () => { - navigate(-1); // TODO: change this logic, also create a separate handler for cancel - }; - - const hideBackButton = location.key === 'default'; - - return ( - - - {!isRareSat && selectedOrdinal && ( - - - - - - )} - - - ); -} -export default ConfirmOrdinalTransaction; diff --git a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx index 21afd5566..aa20e9d27 100644 --- a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx +++ b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx @@ -1,23 +1,21 @@ import ActionButton from '@components/button'; +import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import useSeedVault from '@hooks/useSeedVault'; import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useTransactionContext from '@hooks/useTransactionContext'; +import type { TransactionSummary } from '@screens/sendBtc/helpers'; import { - ErrorCodes, - getBtcFiatEquivalent, - signOrdinalSendTransaction, + AnalyticsEvents, type BtcOrdinal, - type SignedBtcTx, + btcTransaction, + type Transport, } from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; import Spinner from '@ui-library/spinner'; -import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; +import { trackMixPanel } from '@utils/mixpanel'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -52,7 +50,7 @@ const LoaderContainer = styled.div({ }); const ErrorText = styled.h1((props) => ({ - ...props.theme.body_xs, + ...props.theme.typography.body_s, marginBottom: 20, color: props.theme.colors.feedback.error, })); @@ -68,52 +66,27 @@ function RestoreOrdinals() { const { t } = useTranslation('translation'); const selectedAccount = useSelectedAccount(); const { ordinalsAddress, btcAddress } = selectedAccount; - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { getSeed } = useSeedVault(); const navigate = useNavigate(); const ordinalsQuery = useOrdinalsByAddress(btcAddress); - const [error, setError] = useState(''); - const [transferringOrdinalId, setTransferringOrdinalId] = useState(null); const location = useLocation(); - const btcClient = useBtcClient(); - - const isRestoreFundFlow = location.state?.isRestoreFundFlow; + const context = useTransactionContext(); + const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); - const ordinalsUtxos = useMemo( - () => ordinalsQuery.ordinals?.map((ord) => ord.utxo), - [ordinalsQuery.ordinals], - ); + const [transaction, setTransaction] = useState(); + const [summary, setSummary] = useState(); + const [error, setError] = useState(''); + const [feeRate, setFeeRate] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [selectedOrdinal, setSelectedOrdinal] = useState(null); - const { - isLoading, - error: transactionError, - mutateAsync, - } = useMutation({ - mutationFn: async ({ ordinal, seedPhrase }) => { - const tx = await signOrdinalSendTransaction( - ordinalsAddress, - ordinal.utxo, - btcAddress, - Number(selectedAccount?.id), - seedPhrase, - btcClient, - network.type, - ordinalsUtxos!, - ); - return tx; - }, - }); + const isRestoreFundFlow = location.state?.isRestoreFundFlow; useEffect(() => { - if (transactionError) { - if (Number(transactionError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(transactionError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(transactionError.toString()); + if (!feeRate && btcFeeRate && !feeRatesLoading) { + setFeeRate(btcFeeRate.regular.toString()); } - }, [transactionError]); + }, [feeRate, btcFeeRate, feeRatesLoading]); const handleOnCancelClick = () => { if (isRestoreFundFlow) { @@ -123,33 +96,119 @@ function RestoreOrdinals() { } }; - const onClickTransfer = async (selectedOrdinal: BtcOrdinal) => { - setTransferringOrdinalId(selectedOrdinal.id); - const seedPhrase = await getSeed(); - const signedTx = await mutateAsync({ ordinal: selectedOrdinal, seedPhrase }); - navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedOrdinal.id}`, { - state: { - signedTxHex: signedTx.signedTx, - recipientAddress: ordinalsAddress, - fee: signedTx.fee, - feePerVByte: signedTx.feePerVByte, - fiatFee: getBtcFiatEquivalent(signedTx.fee, BigNumber(btcFiatRate)), - total: signedTx.total, - fiatTotal: getBtcFiatEquivalent(signedTx.total, BigNumber(btcFiatRate)), - ordinalUtxo: selectedOrdinal.utxo, - }, - }); + const onClickTransfer = async (ordinal: BtcOrdinal, desiredFeeRate: string) => { + setSelectedOrdinal(ordinal); + setFeeRate(desiredFeeRate); + try { + setIsLoading(true); + setError(''); + const txSummary = await btcTransaction.sendOrdinals( + context, + [{ toAddress: ordinalsAddress, inscriptionId: ordinal.id }], + Number(desiredFeeRate), + ); + setTransaction(txSummary); + setSummary(await txSummary.getSummary()); + } catch (err) { + setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); + setSelectedOrdinal(null); + setTransaction(undefined); + setSummary(undefined); + } finally { + setIsLoading(false); + } + }; + + const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { + const transactionDetail = await btcTransaction.sendOrdinals( + context, + [{ toAddress: ordinalsAddress, inscriptionId: selectedOrdinal?.id! }], + desiredFeeRate, + ); + if (!transactionDetail) return; + const txSummary = await transactionDetail.getSummary(); + if (txSummary) return Number(txSummary.fee); + return undefined; }; + const handleBack = () => { + setSelectedOrdinal(null); + setFeeRate(''); + setTransaction(undefined); + setSummary(undefined); + }; + + const handleSubmit = async (ledgerTransport?: Transport) => { + try { + setIsSubmitting(true); + const txnId = await transaction?.broadcast({ ledgerTransport, rbfEnabled: true }); + trackMixPanel(AnalyticsEvents.TransactionConfirmed, { + protocol: 'ordinals', + action: 'transfer', + wallet_type: selectedAccount?.accountType || 'software', + }); + navigate('/tx-status', { + state: { + txid: txnId, + currency: 'BTC', + error: '', + browserTx: false, + }, + }); + } catch (e) { + console.error(e); + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: `${e}`, + browserTx: false, + }, + }); + } finally { + setIsSubmitting(false); + } + }; + + if (ordinalsQuery.isLoading || feeRatesLoading) { + return ( + <> + + + + + + + + ); + } + + if (summary) { + return ( + onClickTransfer(selectedOrdinal!, newFeeRate.toString())} + feeRate={+feeRate} + isSubmitting={isSubmitting} + hideBottomBar + isBroadcast + /> + ); + } + return ( <> - {ordinalsQuery.isLoading ? ( - - - - ) : ordinalsQuery.ordinals?.length === 0 ? ( + {t('RESTORE_ORDINAL_SCREEN.DESCRIPTION')} + {ordinalsQuery.ordinals?.length === 0 ? ( <> {t('RESTORE_ORDINAL_SCREEN.NO_FUNDS')} @@ -157,21 +216,21 @@ function RestoreOrdinals() { ) : ( - <> - {t('RESTORE_ORDINAL_SCREEN.DESCRIPTION')} - {ordinalsQuery.ordinals?.map((ordinal) => ( - - ))} - - {error} - - + ordinalsQuery.ordinals?.map((ordinal) => ( + + )) + )} + {error && ( + + {error} + )} diff --git a/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx b/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx index 30a0a76f7..90faec8dc 100644 --- a/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx +++ b/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx @@ -60,12 +60,19 @@ const LoaderContainer = styled.div({ interface Props { ordinal: BtcOrdinal; + feeRate: string; isLoading: boolean; disableTransfer: boolean; - handleOrdinalTransfer: (ordinal: BtcOrdinal) => Promise; + handleOrdinalTransfer: (ordinal: BtcOrdinal, feeRate: string) => Promise; } -function OrdinalRow({ ordinal, isLoading, disableTransfer, handleOrdinalTransfer }: Props) { +function OrdinalRow({ + ordinal, + feeRate, + isLoading, + disableTransfer, + handleOrdinalTransfer, +}: Props) { const { t } = useTranslation('translation'); const { data: ordinalData, isLoading: isQuerying } = useInscriptionDetails(ordinal.id); @@ -80,7 +87,10 @@ function OrdinalRow({ ordinal, isLoading, disableTransfer, handleOrdinalTransfer Ordinal - handleOrdinalTransfer(ordinal)} disabled={disableTransfer}> + handleOrdinalTransfer(ordinal, feeRate)} + disabled={disableTransfer} + > {isLoading ? ( diff --git a/src/app/screens/sendBtc/index.tsx b/src/app/screens/sendBtc/index.tsx index 109171f3d..958e6127d 100644 --- a/src/app/screens/sendBtc/index.tsx +++ b/src/app/screens/sendBtc/index.tsx @@ -1,7 +1,6 @@ import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useDebounce from '@hooks/useDebounce'; import useHasFeature from '@hooks/useHasFeature'; -import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; @@ -16,7 +15,7 @@ import { import { isInOptions, isLedgerAccount } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { generateSendMaxTransaction, generateTransaction, @@ -32,31 +31,19 @@ function SendBtcScreen() { useResetUserFlow('/send-btc'); - const location = useLocation(); - const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); const selectedAccount = useSelectedAccount(); const transactionContext = useTransactionContext(); - const [recipientAddress, setRecipientAddress] = useState( - location.state?.recipientAddress || '', - ); + const [recipientAddress, setRecipientAddress] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const [amountSats, setAmountSats] = useState(location.state?.amount || ''); + const [amountSats, setAmountSats] = useState(''); const [feeRate, setFeeRate] = useState(''); const [sendMax, setSendMax] = useState(false); - const amountEditable = location.state?.disableAmountEdit ?? true; - const addressEditable = location.state?.disableAddressEdit ?? true; const debouncedRecipient = useDebounce(recipientAddress, 500); - const initialStep = addressEditable - ? Step.SelectRecipient - : amountEditable - ? Step.SelectRecipient - : Step.Confirm; - - const [currentStep, setCurrentStep] = useState(initialStep); + const [currentStep, setCurrentStep] = useState(Step.SelectRecipient); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); @@ -150,7 +137,7 @@ function SendBtcScreen() { const handleBackButtonClick = () => { if (currentStep > 0) { - setCurrentStep(getPreviousStep(currentStep, addressEditable, amountEditable)); + setCurrentStep(getPreviousStep(currentStep)); } else { handleCancel(); } @@ -159,7 +146,6 @@ function SendBtcScreen() { const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { const { summary: tempSummary } = await generateTransactionAndSummary(desiredFeeRate); if (tempSummary) return Number(tempSummary.fee); - return undefined; }; @@ -222,11 +208,9 @@ function SendBtcScreen() { setAmountSats={setAmountSatsSafe} feeRate={feeRate} setFeeRate={handleFeeRateChange} + getFeeForFeeRate={calculateFeeForFeeRate} sendMax={sendMax} setSendMax={setSendMax} - getFeeForFeeRate={calculateFeeForFeeRate} - addressEditable={addressEditable} - amountEditable={amountEditable} onBack={handleBackButtonClick} onCancel={handleCancel} onConfirm={handleSubmit} diff --git a/src/app/screens/sendBtc/stepDisplay.tsx b/src/app/screens/sendBtc/stepDisplay.tsx index 00d7be006..a43a6c5b6 100644 --- a/src/app/screens/sendBtc/stepDisplay.tsx +++ b/src/app/screens/sendBtc/stepDisplay.tsx @@ -42,8 +42,6 @@ type Props = { sendMax: boolean; setSendMax: (sendMax: boolean) => void; getFeeForFeeRate: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; - addressEditable: boolean; - amountEditable: boolean; onConfirm: () => void; onBack: () => void; onCancel: () => void; @@ -65,8 +63,6 @@ function StepDisplay({ sendMax, setSendMax, getFeeForFeeRate, - addressEditable, - amountEditable, onConfirm, onBack, onCancel, @@ -89,7 +85,7 @@ function StepDisplay({ header={header} recipientAddress={recipientAddress} setRecipientAddress={setRecipientAddress} - onNext={() => setCurrentStep(getNextStep(Step.SelectRecipient, amountEditable))} + onNext={() => setCurrentStep(getNextStep(Step.SelectRecipient))} isLoading={isLoading} />
@@ -110,7 +106,7 @@ function StepDisplay({ fee={(summary as TransactionSummary)?.fee.toString()} getFeeForFeeRate={getFeeForFeeRate} dustFiltered={(summary as TransactionSummary)?.dustFiltered ?? false} - onNext={() => setCurrentStep(getNextStep(Step.SelectAmount, amountEditable))} + onNext={() => setCurrentStep(getNextStep(Step.SelectAmount))} hasSufficientFunds={!!summary || isLoading} isLoading={isLoading} /> diff --git a/src/app/screens/sendBtc/steps.tsx b/src/app/screens/sendBtc/steps.tsx index 87c859581..2d464b7b1 100644 --- a/src/app/screens/sendBtc/steps.tsx +++ b/src/app/screens/sendBtc/steps.tsx @@ -4,10 +4,10 @@ export enum Step { Confirm = 2, } -export const getNextStep = (currentStep: Step, amountEditable: boolean) => { +export const getNextStep = (currentStep: Step) => { switch (currentStep) { case Step.SelectRecipient: - return amountEditable ? Step.SelectAmount : Step.Confirm; + return Step.SelectAmount; case Step.SelectAmount: return Step.Confirm; case Step.Confirm: @@ -17,19 +17,14 @@ export const getNextStep = (currentStep: Step, amountEditable: boolean) => { } }; -export const getPreviousStep = ( - currentStep: Step, - addressEditable: boolean, - amountEditable: boolean, -) => { +export const getPreviousStep = (currentStep: Step) => { switch (currentStep) { case Step.SelectRecipient: return Step.SelectRecipient; case Step.SelectAmount: - return addressEditable ? Step.SelectRecipient : Step.SelectAmount; return Step.SelectRecipient; case Step.Confirm: - return amountEditable ? Step.SelectAmount : Step.SelectRecipient; + return Step.SelectAmount; default: throw new Error(`Unknown step: ${currentStep}`); } diff --git a/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx b/src/app/screens/sendInscriptionsRequest/index.tsx similarity index 100% rename from src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx rename to src/app/screens/sendInscriptionsRequest/index.tsx diff --git a/src/app/screens/confirmOrdinalTransaction/useSendInscriptions.ts b/src/app/screens/sendInscriptionsRequest/useSendInscriptions.ts similarity index 100% rename from src/app/screens/confirmOrdinalTransaction/useSendInscriptions.ts rename to src/app/screens/sendInscriptionsRequest/useSendInscriptions.ts diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index b0c8b29a0..0869e5b22 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -173,7 +173,7 @@ function SendOrdinalScreen() { const txnId = await transaction?.broadcast({ ledgerTransport, rbfEnabled: true }); trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: 'runes', + protocol: 'ordinals', action: 'transfer', wallet_type: selectedAccount?.accountType || 'software', }); diff --git a/src/app/utils/localStorage.ts b/src/app/utils/localStorage.ts index 80f3aef3a..7c2d57a0c 100644 --- a/src/app/utils/localStorage.ts +++ b/src/app/utils/localStorage.ts @@ -1,5 +1,4 @@ const isTermsAccepted = 'isTermsAccepted'; -const nonOrdinalTransferTime = 'nonOrdinalTransferTime'; export function saveIsTermsAccepted(termsDisplayed: boolean) { localStorage.setItem(isTermsAccepted, termsDisplayed.toString()); @@ -7,13 +6,5 @@ export function saveIsTermsAccepted(termsDisplayed: boolean) { export function getIsTermsAccepted(): boolean { const accepted = localStorage.getItem(isTermsAccepted); - if (accepted !== null) { - return true; - } - return false; -} - -export async function saveTimeForNonOrdinalTransferTransaction(ordinalAddress: string) { - const currentTime = new Date().getTime().toString(); - return localStorage.setItem(nonOrdinalTransferTime + ordinalAddress, currentTime); + return accepted !== null; } diff --git a/src/common/types/ledger.ts b/src/common/types/ledger.ts index d6e14987b..2ed0fe32f 100644 --- a/src/common/types/ledger.ts +++ b/src/common/types/ledger.ts @@ -1,4 +1,4 @@ -import type { Recipient, StacksRecipient, UTXO } from '@secretkeylabs/xverse-core'; +import type { StacksRecipient } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; export type LedgerTransactionType = 'BTC' | 'STX' | 'ORDINALS' | 'BRC-20'; @@ -12,19 +12,3 @@ export type ConfirmStxTransactionState = ConfirmLedgerTransactionState & { recipients: StacksRecipient[]; unsignedTx: Buffer; }; - -export type ConfirmBtcTransactionState = ConfirmLedgerTransactionState & { - recipients: Recipient[]; - feeRateInput: string; -}; - -export type ConfirmOrdinalsTransactionState = ConfirmLedgerTransactionState & { - recipients: Recipient[]; - feeRateInput: string; - ordinalUtxo: UTXO; -}; - -export type ConfirmBrc20TransactionState = ConfirmLedgerTransactionState & { - recipients: Recipient[]; - amount: BigNumber; -}; diff --git a/src/locales/en.json b/src/locales/en.json index 0c93a01a0..52959bd50 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -409,7 +409,7 @@ "RUNES_CENOTAPH_WARNING": "This transaction will burn all input Runes. Make sure you trust the requesting app.", "YOU_WILL_SEND": "You will send", "TRANSACTION_DETAILS": "Transaction details", - "BTC_TRANSFER_WARNING": "This amount may include the transaction fees & bundle size of the transferred assets.", + "BTC_TRANSFER_WARNING": "This amount may include the tx fee & postage associated to transferred ordinals, runes & BRC-20.", "LEDGER": { "CONNECT": { "TITLE": "Connect your hardware wallet", From b6edbdb804e0eb9be837d0ce51309d6091c385f2 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Wed, 7 Aug 2024 10:50:27 +0200 Subject: [PATCH 159/219] adjust helper --- tests/specs/swapVisuals.spec.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/specs/swapVisuals.spec.ts b/tests/specs/swapVisuals.spec.ts index 7ab34dfa9..023d3cc1a 100644 --- a/tests/specs/swapVisuals.spec.ts +++ b/tests/specs/swapVisuals.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '../fixtures/base'; +import { enableCrossChainSwaps } from '../fixtures/helpers'; import Onboarding from '../pages/onboarding'; import Wallet from '../pages/wallet'; @@ -7,15 +8,7 @@ const strongPW = Onboarding.generateSecurePasswordCrypto(); test.describe('Swap Flow Visuals', () => { // Enables the feature flag for Swap test.beforeEach(async ({ page }) => { - await page.route('https://api-3.xverse.app/v1/app-features', (route) => { - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - CROSS_CHAIN_SWAPS: { enabled: true }, - }), - }); - }); + await enableCrossChainSwaps(page); }); test('Check swap page', async ({ page, extensionId }) => { From 25bb8fcda3fcce33ef852b620486c11f817445b9 Mon Sep 17 00:00:00 2001 From: fede erbes Date: Wed, 7 Aug 2024 13:53:42 +0300 Subject: [PATCH 160/219] feat: add ledger support into swaps --- src/app/screens/coinDashboard/coinHeader.tsx | 3 +-- src/app/screens/home/index.tsx | 3 +-- .../psbtConfirmation/psbtConfirmation.tsx | 13 ++++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/screens/coinDashboard/coinHeader.tsx b/src/app/screens/coinDashboard/coinHeader.tsx index 98e0bf6ea..4edaf13b7 100644 --- a/src/app/screens/coinDashboard/coinHeader.tsx +++ b/src/app/screens/coinDashboard/coinHeader.tsx @@ -169,8 +169,7 @@ export default function CoinHeader({ currency, fungibleToken }: Props) { const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS); const showRunesSwap = (currency === 'FT' && fungibleToken?.protocol === 'runes') || currency === 'BTC'; - // ledger is disabled for now - const showSwaps = isCrossChainSwapsEnabled && showRunesSwap && !isLedgerAccount(selectedAccount); + const showSwaps = isCrossChainSwapsEnabled && showRunesSwap; const navigateToSwaps = () => { if (!showSwaps) { diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index 4e4372b71..b063bcc6e 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -411,8 +411,7 @@ function Home() { }; const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS); - // ledger is disabled for now - const showSwaps = isCrossChainSwapsEnabled && !isLedgerAccount(selectedAccount); + const showSwaps = isCrossChainSwapsEnabled; return ( <> diff --git a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx index aaa680125..f222eebfa 100644 --- a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx +++ b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx @@ -11,6 +11,7 @@ import { type PlaceOrderResponse, type PlaceUtxoOrderResponse, type RuneSummary, + type Transport, } from '@secretkeylabs/xverse-core'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -119,11 +120,13 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop return executeOrderResponse; }; - const handleConfirm = async () => { + const handleConfirm = async (ledgerTransport?: Transport) => { setIsSigning(true); try { - // TODO: add ledger support - const signedPsbt = await parsedPsbt?.getSignedPsbtBase64({ finalize: false }); + const signedPsbt = await parsedPsbt?.getSignedPsbtBase64({ + finalize: false, + ledgerTransport, + }); if (!signedPsbt) { throw new Error(t('PSBT_CANT_SIGN_ERROR_TITLE')); @@ -135,6 +138,10 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop return setIsSigning(false); } + if (ledgerTransport) { + await ledgerTransport.close(); + } + onConfirm(); setIsSigning(false); From dacdc8b1ab871f3351a8d6e7dbf1bfaa77c018d2 Mon Sep 17 00:00:00 2001 From: fede erbes Date: Wed, 7 Aug 2024 15:09:32 +0300 Subject: [PATCH 161/219] chore: add quote expiry callout for dotswap --- src/app/components/confirmBtcTransaction/index.tsx | 5 ++++- .../swap/components/psbtConfirmation/psbtConfirmation.tsx | 8 ++++++++ src/locales/en.json | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx index 83c017100..14c88c573 100644 --- a/src/app/components/confirmBtcTransaction/index.tsx +++ b/src/app/components/confirmBtcTransaction/index.tsx @@ -10,7 +10,7 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import TransportFactory from '@ledgerhq/hw-transport-webusb'; import type { Transport } from '@secretkeylabs/xverse-core'; import Button from '@ui-library/button'; -import Callout from '@ui-library/callout'; +import Callout, { type CalloutProps } from '@ui-library/callout'; import { StickyHorizontalSplitButtonContainer, StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; @@ -72,6 +72,7 @@ type Props = { feeRate?: number; title?: string; selectedBottomTab?: Tab; + customCallout?: CalloutProps; }; function ConfirmBtcTransaction({ @@ -95,6 +96,7 @@ function ConfirmBtcTransaction({ title, selectedBottomTab, brc20Summary, + customCallout, }: Props) { const parsedTxSummaryContextValue = useMemo( () => ({ summary, runeSummary, brc20Summary }), @@ -198,6 +200,7 @@ function ConfirmBtcTransaction({ /> )} {!isBroadcast && } + {customCallout && } ); } diff --git a/src/locales/en.json b/src/locales/en.json index 52959bd50..a4f9b292a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -457,7 +457,8 @@ "RUNE_IS_CLOSED": "This rune is closed and cannot be minted.", "UNKNOWN_RUNE_RECIPIENTS": "Undetermined Runes recipients ", "RUNE_DELEGATION_DESCRIPTION": "This is a partial transaction with undetermined Runes recipients. You are delegating your Runes to the requesting app. The requesting app can burn your runes or transfer them to any recipient", - "YOU_WILL_DELEGATE": "You will delegate" + "YOU_WILL_DELEGATE": "You will delegate", + "QUOTE_EXPIRES_IN": "Note that this swap quote will expire after {{seconds}} seconds" }, "TX_ERRORS": { "INSUFFICIENT_BALANCE": "The requested transaction cannot be created due to insufficient balance", From 5321c80bdc572c1bd503f5399d9792b62093bfc4 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Wed, 7 Aug 2024 15:00:18 +0200 Subject: [PATCH 162/219] Adjust after merge --- src/app/components/tokenImage/index.tsx | 6 ++--- src/app/screens/ordinals/brc20Tile.tsx | 6 ++++- src/app/screens/ordinals/ordinalImage.tsx | 7 +++--- tests/pages/wallet.ts | 26 +++++++++++++-------- tests/specs/swapME.spec.ts | 5 ++-- tests/specs/swapMECancel.spec.ts | 5 ++-- tests/specs/tabCollectiblesRareSats.spec.ts | 4 ++++ tests/specs/transactionBTC.spec.ts | 4 ++-- 8 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/app/components/tokenImage/index.tsx b/src/app/components/tokenImage/index.tsx index e7cedf3e7..3243ed5f5 100644 --- a/src/app/components/tokenImage/index.tsx +++ b/src/app/components/tokenImage/index.tsx @@ -118,7 +118,7 @@ export default function TokenImage({ if (imageError) { return ( - {ticker.substring(0, 4)} + {ticker.substring(0, 4)} ); } @@ -156,14 +156,14 @@ export default function TokenImage({ if (fungibleToken.runeSymbol) { return ( - {fungibleToken.runeSymbol} + {fungibleToken.runeSymbol} ); } return ( - {ticker.substring(0, 4)} + {ticker.substring(0, 4)} ); }; diff --git a/src/app/screens/ordinals/brc20Tile.tsx b/src/app/screens/ordinals/brc20Tile.tsx index f0b3f2eed..1dba1cecf 100644 --- a/src/app/screens/ordinals/brc20Tile.tsx +++ b/src/app/screens/ordinals/brc20Tile.tsx @@ -169,7 +169,11 @@ export default function Brc20Tile(props: Brc20TileProps) { isGalleryOpen={isGalleryOpen} isSmallImage={isSmallImage} > - + {ticker} diff --git a/src/app/screens/ordinals/ordinalImage.tsx b/src/app/screens/ordinals/ordinalImage.tsx index 1f34ee5a9..37fc407b7 100644 --- a/src/app/screens/ordinals/ordinalImage.tsx +++ b/src/app/screens/ordinals/ordinalImage.tsx @@ -208,6 +208,7 @@ function OrdinalImage({ const renderImage = (tag: string, src?: string) => ( + + @@ -326,7 +327,7 @@ function OrdinalImage({ } return ( - + ordinal ); diff --git a/tests/pages/wallet.ts b/tests/pages/wallet.ts index 59befae44..c4a59405d 100644 --- a/tests/pages/wallet.ts +++ b/tests/pages/wallet.ts @@ -630,8 +630,6 @@ export default class Wallet { } async checkVisualsSendInscriptionsPage2(ordinalAddress, ordinalNumber, collection) { - await expect(this.confirmTotalAmount).toBeVisible(); - await expect(this.confirmCurrencyAmount).toBeVisible(); await expect(this.buttonExpand).toBeVisible(); await expect(this.buttonCancel).toBeEnabled(); await expect(this.buttonConfirm).toBeEnabled(); @@ -711,16 +709,28 @@ export default class Wallet { editableFees?: boolean, sendAddress?: string, receiverAddress?: string, + totalAmountShown: boolean = true, + tokenImageShown: boolean = true, ) { await expect(this.page.url()).toContain(url); - await expect(this.confirmTotalAmount).toBeVisible(); - await expect(this.confirmCurrencyAmount).toBeVisible(); await expect(this.buttonExpand).toBeVisible(); await expect(this.buttonCancel).toBeEnabled(); await expect(this.buttonConfirm).toBeEnabled(); - await expect(this.feeAmount).toBeVisible(); - await expect(this.imageToken.first()).toBeVisible(); + + // Not all TX Screens show a total amount + if (totalAmountShown) { + await expect(this.confirmTotalAmount).toBeVisible(); + await expect(this.confirmCurrencyAmount).toBeVisible(); + } + + if (tokenImageShown) { + await expect(this.imageToken.first()).toBeVisible(); + } + + if (editableFees) { + await expect(this.buttonEditFee).toBeVisible(); + } await this.buttonExpand.click(); await expect(this.sendAddress.first()).toBeVisible(); @@ -728,10 +738,6 @@ export default class Wallet { await expect(this.confirmAmount.first()).toBeVisible(); await expect(this.confirmBalance.first()).toBeVisible(); - if (editableFees) { - await expect(this.buttonEditFee).toBeVisible(); - } - // Execute these checks only if sendAddress is provided if (sendAddress) { await expect(await this.sendAddress.first().innerText()).toContain(sendAddress.slice(-4)); diff --git a/tests/specs/swapME.spec.ts b/tests/specs/swapME.spec.ts index 828963275..a9d5b918a 100644 --- a/tests/specs/swapME.spec.ts +++ b/tests/specs/swapME.spec.ts @@ -9,7 +9,7 @@ test.describe('Swap Flow ME', () => { }); const marketplace = 'Magic Eden'; - const token = 'MONEY'; + const token = 'THE•MONEY•BEES'; test('Swap token via ME with standard fee mainnet #localexecution', async ({ page, @@ -43,7 +43,8 @@ test.describe('Swap Flow ME', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - await wallet.divTokenRow.filter({ hasText: token }).click(); + await wallet.inputField.fill(token); + await wallet.divTokenRow.filter({ hasText: token }).first().click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); diff --git a/tests/specs/swapMECancel.spec.ts b/tests/specs/swapMECancel.spec.ts index a7ced11f4..15bdf53f2 100644 --- a/tests/specs/swapMECancel.spec.ts +++ b/tests/specs/swapMECancel.spec.ts @@ -9,7 +9,7 @@ test.describe('Swap Flow ME', () => { }); const marketplace = 'Magic Eden'; - const token = 'MONEY'; + const token = 'THE•MONEY•BEES'; test('Cancel swap token via ME', async ({ page, extensionId }) => { // Restore wallet @@ -43,7 +43,8 @@ test.describe('Swap Flow ME', () => { // Had problems with loading of all tokens so I check that a 'DOG' is loaded await expect(wallet.labelTokenSubtitle.getByText('DOG').first()).toBeVisible(); await expect(await wallet.divTokenRow.count()).toBeGreaterThan(0); - await wallet.divTokenRow.filter({ hasText: token }).click(); + await wallet.inputField.fill(token); + await wallet.divTokenRow.filter({ hasText: token }).first().click(); await expect(wallet.nameToken.last()).not.toContainText('Select asset'); await expect(wallet.imageToken.last()).toBeVisible(); await expect(wallet.buttonGetQuotes).toBeDisabled(); diff --git a/tests/specs/tabCollectiblesRareSats.spec.ts b/tests/specs/tabCollectiblesRareSats.spec.ts index d74ca1f93..8dfd61cfa 100644 --- a/tests/specs/tabCollectiblesRareSats.spec.ts +++ b/tests/specs/tabCollectiblesRareSats.spec.ts @@ -63,6 +63,8 @@ test.describe('Collectibles Tab - Rare sats', () => { false, addressOrdinals, addressOrdinals, + false, + false, ); // Cancel the transaction @@ -112,6 +114,8 @@ test.describe('Collectibles Tab - Rare sats', () => { false, addressOrdinals, addressOrdinals, + false, + false, ); await wallet.confirmSendTransaction(); diff --git a/tests/specs/transactionBTC.spec.ts b/tests/specs/transactionBTC.spec.ts index 0b4716f48..9b2d3d632 100644 --- a/tests/specs/transactionBTC.spec.ts +++ b/tests/specs/transactionBTC.spec.ts @@ -53,7 +53,7 @@ test.describe('Transaction BTC', () => { await expect(initialBTCBalance).toEqual(displayBalanceNumerical); }); - test('Send BTC - Cancel transaction testnet', async ({ page, extensionId }) => { + test('Cancel BTC transaction testnet', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); @@ -114,7 +114,7 @@ test.describe('Transaction BTC', () => { await expect(initialBTCBalance).toEqual(balanceAfterCancel); }); - test('Send BTC - confirm transaction testnet #localexecution', async ({ page, extensionId }) => { + test('Confirm BTC transaction testnet #localexecution', async ({ page, extensionId }) => { // Restore wallet and setup Testnet network const wallet = new Wallet(page); await wallet.setupTest(extensionId, 'SEED_WORDS1', true); From 36594b4a979349eec5d74e7c2077caa0311d8541 Mon Sep 17 00:00:00 2001 From: Christine Pinto Date: Wed, 7 Aug 2024 16:09:53 +0200 Subject: [PATCH 163/219] Optimize Visual Check for Transaction Review Page Add FeeChange to transaction flows Add stabilisation for FeeSelection and copyAddress function --- .../components/receiveCardComponent/index.tsx | 2 +- src/app/ui-components/selectFeeRate/index.tsx | 2 +- tests/pages/onboarding.ts | 1 - tests/pages/wallet.ts | 93 +++++++++++++------ tests/specs/createWallet.spec.ts | 12 +-- tests/specs/runesList.spec.ts | 8 +- tests/specs/runesListCancel.spec.ts | 4 +- tests/specs/runesSend.spec.ts | 6 +- tests/specs/runesSendCancel.spec.ts | 7 +- tests/specs/swapExchange.spec.ts | 2 +- tests/specs/swapExchangeCancel.spec.ts | 28 +----- tests/specs/swapME.spec.ts | 2 +- tests/specs/swapMECancel.spec.ts | 28 +----- tests/specs/swapUSCancel.spec.ts | 28 +----- .../specs/tabCollectiblesInscriptions.spec.ts | 44 +++++++-- tests/specs/tabCollectiblesRareSats.spec.ts | 6 +- tests/specs/transactionBTC.spec.ts | 10 +- tests/specs/transactionSTX.spec.ts | 7 +- 18 files changed, 144 insertions(+), 146 deletions(-) diff --git a/src/app/components/receiveCardComponent/index.tsx b/src/app/components/receiveCardComponent/index.tsx index b138c4dc7..8e3e3348f 100644 --- a/src/app/components/receiveCardComponent/index.tsx +++ b/src/app/components/receiveCardComponent/index.tsx @@ -114,7 +114,7 @@ function ReceiveCardComponent({ }; return ( - + {children} {title} diff --git a/src/app/ui-components/selectFeeRate/index.tsx b/src/app/ui-components/selectFeeRate/index.tsx index 400d87605..68df5ec8a 100644 --- a/src/app/ui-components/selectFeeRate/index.tsx +++ b/src/app/ui-components/selectFeeRate/index.tsx @@ -147,7 +147,7 @@ function SelectFeeRate({ {feeRateSpeed && ( -
+ {showBtcAmount && ( + + + + )} {showPaymentRunes && paymentRuneReceipts.map((receipt) => ( + +
+ + {t('COMMON.BUNDLE')} + +
+
+ ( + + {value} + + )} + /> +
+
))} - {showBtcAmount && ( - - - - )} {inscriptionsRareSatsInPayment.length > 0 && ( - + {inscriptionsRareSatsInPayment .sort((a, b) => b.inscriptions.length - a.inscriptions.length) .map((output, index) => ( @@ -182,14 +186,10 @@ function ReceiveSection({ onShowInscription }: Props) { // eslint-disable-next-line react/no-array-index-key key={index} inscriptions={output.inscriptions} - hasExternalInputs={hasExternalInputs} satributes={output.satributes} amount={output.amount} + showAmount onShowInscription={onShowInscription} - showTopDivider={ - (Boolean(paymentRuneReceipts.length) || showBtcAmount) && index === 0 - } - showBottomDivider={inscriptionsRareSatsInPayment.length > index + 1} /> ))} diff --git a/src/app/components/confirmBtcTransaction/sendSection.tsx b/src/app/components/confirmBtcTransaction/sendSection.tsx index 670bef849..dcb9bf4e0 100644 --- a/src/app/components/confirmBtcTransaction/sendSection.tsx +++ b/src/app/components/confirmBtcTransaction/sendSection.tsx @@ -2,9 +2,8 @@ import { useParsedTxSummaryContext } from '@components/confirmBtcTransaction/hoo import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount'; import type { btcTransaction } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; +import { getTruncatedAddress } from '@utils/helper'; import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; import Amount from './itemRow/amount'; import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute'; @@ -22,7 +21,7 @@ const Container = styled.div((props) => ({ flexDirection: 'column', background: props.theme.colors.elevation1, borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, + paddingTop: props.theme.space.m, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -36,7 +35,7 @@ const BundleHeader = styled.div((props) => ({ flex: 1, flexDirection: 'row', justifyContent: 'space-between', - marginBottom: props.theme.space.m, + marginBottom: props.theme.space.s, })); function SendSection({ @@ -46,117 +45,56 @@ function SendSection({ }) { const { t } = useTranslation('translation'); const { - runeSummary, - netBtcAmount, - transactionIsFinal, - sendSection: { - showSendSection, - showBtcAmount, - showRuneTransfers, - hasInscriptionsRareSatsInOrdinal, - outputsFromOrdinal, - inputFromOrdinal, - inscriptionsFromPayment, - satributesFromPayment, - }, + showSendSection, + sendSection: { bundledOutputs, inscriptionsFromPayment, satributesFromPayment }, } = useParsedTxSummaryContext(); - /** TODO - start bundling send/receive data by output addresses - * switch(output.type) === 'address' -> display `destinationAddress` - * script -> OP_RETURN - * ms -> nothing - * Each address can have 1:N bundles - * Challenge right now is how to to group the btc, runes, inscriptions data together by output address - */ - - /** Send Data - * BTC: netBtcAmount - * Runes: runeSummary.transfers - * Ordinals: outputsFromOrdinal OR inputFromOrdinal (depending if tx is final) - */ - - if (!showSendSection) return null; + if (!showSendSection || !Object.entries(bundledOutputs).length) return null; return ( <> {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} - - {showBtcAmount && ( - - + {Object.entries(bundledOutputs).map(([address, bundle], index) => ( + + + + + {t('COMMON.TO')} + + + {getTruncatedAddress(address, 6)} + + + + {bundle.runeTransfers.map((runeTransfer, i) => ( + 0} + hasSufficientBalance={runeTransfer.hasSufficientBalance} + /> + ))} + {bundle.outputs.map((output) => ( + 0 || bundle.runeTransfers.length > 0} + showAmount + onShowInscription={onShowInscription} + /> + ))} - )} - {showRuneTransfers && - runeSummary?.transfers?.map((transfer, index) => ( - <> - {showBtcAmount && } - - -
- - {t('COMMON.BUNDLE')} - -
-
- ( - - {value} - - )} - /> -
-
- -
- {/* { - // unnecessary divider, re-add if needed - runeSummary?.transfers.length > index + 1 && - } */} - - ))} - {hasInscriptionsRareSatsInOrdinal && ( - - {transactionIsFinal - ? outputsFromOrdinal.map((output, index) => ( - index + 1} - /> - )) - : inputFromOrdinal.map((input, index) => ( - index + 1} - /> - ))} - - )} -
+
+ ))} ); } diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index df5f3c811..80fbdf390 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -69,7 +69,6 @@ function TransactionSummary({ isSubmitting, getFeeForFeeRate, onFeeRateSet, feeR const { summary, runeSummary, - hasExternalInputs, isUnconfirmedInput, hasOutputScript, showCenotaphCallout, @@ -108,11 +107,8 @@ function TransactionSummary({ isSubmitting, getFeeForFeeRate, onFeeRateSet, feeR )} {hasRuneDelegation && } - {hasExternalInputs ? ( - - ) : ( - - )} + + {!hasRuneDelegation && } diff --git a/src/app/components/confirmBtcTransaction/transferSection.tsx b/src/app/components/confirmBtcTransaction/transferSection.tsx index 78778b0f1..44edfa9be 100644 --- a/src/app/components/confirmBtcTransaction/transferSection.tsx +++ b/src/app/components/confirmBtcTransaction/transferSection.tsx @@ -23,7 +23,7 @@ const Container = styled.div((props) => ({ flexDirection: 'column', background: props.theme.colors.elevation1, borderRadius: props.theme.radius(2), - padding: `${props.theme.space.m} 0 20px`, + paddingTop: props.theme.space.m, justifyContent: 'center', marginBottom: props.theme.space.s, })); @@ -51,82 +51,83 @@ function TransferSection({ onShowInscription }: Props) { const { runeSummary, netBtcAmount, + showTransferSection, transactionIsFinal, - sendSection: { - showSendSection, + transferSection: { showBtcAmount, showRuneTransfers, - hasInscriptionsRareSatsInOrdinal, outputsFromOrdinal, - inputFromOrdinal, + inputsFromOrdinal, inscriptionsFromPayment, satributesFromPayment, }, } = useParsedTxSummaryContext(); - if (!showSendSection) return null; + if (!showTransferSection) return null; return ( <> {t('CONFIRM_TRANSACTION.YOU_WILL_TRANSFER')} - {showBtcAmount && ( - - - - - - - {t('CONFIRM_TRANSACTION.BTC_TRANSFER_WARNING')} - - - - )} - {showRuneTransfers && - runeSummary?.transfers?.map((transfer, index) => ( + + {showBtcAmount && ( <> - {showBtcAmount && } - - - - {runeSummary?.transfers.length > index + 1 && } + + + + + + {t('CONFIRM_TRANSACTION.BTC_TRANSFER_WARNING')} + + - ))} - {hasInscriptionsRareSatsInOrdinal && ( - - {!transactionIsFinal - ? inputFromOrdinal.map((input, index) => ( + )} + {showRuneTransfers && + runeSummary?.transfers?.map((transfer, index) => ( + <> + {showBtcAmount && index === 0 && } + + {runeSummary?.transfers.length > index + 1 && } + + ))} + {transactionIsFinal + ? outputsFromOrdinal.map((output, index) => ( + <> + {(showRuneTransfers || showBtcAmount) && index === 0 && ( + + )} index + 1} /> - )) - : outputsFromOrdinal.map((output, index) => ( + {outputsFromOrdinal.length > index + 1 && } + + )) + : inputsFromOrdinal.map((input, index) => ( + <> + {(showRuneTransfers || showBtcAmount) && index === 0 && ( + + )} index + 1} /> - ))} - - )} + {inputsFromOrdinal.length > index + 1 && } + + ))} + ); diff --git a/src/app/components/confirmBtcTransaction/utils.ts b/src/app/components/confirmBtcTransaction/utils.ts index a562f9b2c..93147f812 100644 --- a/src/app/components/confirmBtcTransaction/utils.ts +++ b/src/app/components/confirmBtcTransaction/utils.ts @@ -118,26 +118,26 @@ export const getInputsWithAssetsFromUserAddress = ({ ordinalsAddress, inputs, }: Omit): { - inputFromPayment: btcTransaction.EnhancedInput[]; - inputFromOrdinal: btcTransaction.EnhancedInput[]; + inputsFromPayment: btcTransaction.EnhancedInput[]; + inputsFromOrdinal: btcTransaction.EnhancedInput[]; } => { // we want to discard inputs that are not from user address and do not have inscriptions or satributes - const inputFromPayment: btcTransaction.EnhancedInput[] = []; - const inputFromOrdinal: btcTransaction.EnhancedInput[] = []; + const inputsFromPayment: btcTransaction.EnhancedInput[] = []; + const inputsFromOrdinal: btcTransaction.EnhancedInput[] = []; inputs?.forEach((input) => { if (!input.inscriptions.length && !input.satributes.length) { return; } if (input.extendedUtxo.address === btcAddress) { - return inputFromPayment.push(input); + return inputsFromPayment.push(input); } if (input.extendedUtxo.address === ordinalsAddress) { - inputFromOrdinal.push(input); + inputsFromOrdinal.push(input); } }); - return { inputFromPayment, inputFromOrdinal }; + return { inputsFromPayment, inputsFromOrdinal }; }; export const getOutputsWithAssetsToUserAddress = ({ diff --git a/src/app/components/confirmBtcTransactionComponent/bundle.tsx b/src/app/components/confirmBtcTransactionComponent/bundle.tsx deleted file mode 100644 index 480a2c585..000000000 --- a/src/app/components/confirmBtcTransactionComponent/bundle.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import BundleIcon from '@assets/img/rareSats/satBundle.svg'; -import AssetModal from '@components/assetModal'; -import { CaretDown } from '@phosphor-icons/react'; -import type { Bundle, BundleSatRange, SatRangeInscription } from '@secretkeylabs/xverse-core'; -import { StyledP } from '@ui-library/common.styled'; -import Divider from '@ui-library/divider'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import Theme from 'theme'; -import BundleItem from './bundleItem'; - -interface BundleItemContainerProps { - addMargin: boolean; -} - -const BundleItemsContainer = styled.div` - margin-top: ${(props) => (props.addMargin ? props.theme.space.m : 0)}; -`; - -const SatsBundleContainer = styled.div` - display: flex; - flex-direction: column; - margin-bottom: ${(props) => props.theme.space.s}; - border-radius: ${(props) => props.theme.space.s}; - padding: ${(props) => props.theme.space.m}; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const SatsBundleButton = styled.button` - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background-color: ${(props) => props.theme.colors.elevation1}; -`; - -const Row = styled.div` - display: flex; - flex-direction: row; - align-items: center; -`; - -const BundleTitle = styled(StyledP)` - margin-left: ${(props) => props.theme.space.s}; -`; - -const BundleValue = styled(StyledP)` - margin-right: ${(props) => props.theme.space.xs}; -`; - -const Title = styled(StyledP)((props) => ({ - marginBottom: props.theme.space.xs, -})); - -function SatsBundle({ bundle, title }: { bundle: Bundle; title?: string }) { - const [showBundleDetail, setShowBundleDetail] = useState(false); - const [inscriptionToShow, setInscriptionToShow] = useState( - undefined, - ); - - const { t } = useTranslation('translation'); - - return ( - <> - {inscriptionToShow && ( - setInscriptionToShow(undefined)} - inscription={inscriptionToShow} - /> - )} - - {title && {title}} - setShowBundleDetail((prevState) => !prevState)} - > - - bundle - - {t('RARE_SATS.SATS_BUNDLE')} - - - - {`${bundle.totalExoticSats} ${t( - 'NFT_DASHBOARD_SCREEN.RARE_SATS', - )}`} - - - - - {showBundleDetail && - bundle.satRanges.map((item: BundleSatRange, index: number) => ( - - { - // show ordinal modal to show asset - setInscriptionToShow(inscription); - }} - /> - {bundle.satRanges.length > index + 1 && } - - ))} - - - ); -} - -export default SatsBundle; diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx deleted file mode 100644 index 762e9f113..000000000 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ /dev/null @@ -1,496 +0,0 @@ -import AssetIcon from '@assets/img/transactions/Assets.svg'; -import RecipientComponent from '@components/recipientComponent'; -import TransactionSettingAlert from '@components/transactionSetting'; -import TransferFeeView from '@components/transferFeeView'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useNftDataSelector from '@hooks/stores/useNftDataSelector'; -import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import useSeedVault from '@hooks/useSeedVault'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { PencilSimple } from '@phosphor-icons/react'; -import { - ErrorCodes, - ResponseError, - getBtcFiatEquivalent, - satsToBtc, - signBtcTransaction, - signNonOrdinalBtcSendTransaction, - signOrdinalSendTransaction, - type Bundle, - type Recipient, - type SignedBtcTx, - type UTXO, -} from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import Button from '@ui-library/button'; -import Callout from '@ui-library/callout'; -import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'; -import type { CurrencyTypes } from '@utils/constants'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState, type ReactNode } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; -import TransactionDetailComponent from '../transactionDetailComponent'; -import SatsBundle from './bundle'; - -const OuterContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const Subtitle = styled.p` - ${(props) => props.theme.typography.body_medium_m}; - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.s}; - margin-bottom: ${(props) => props.theme.space.xs}; -`; - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(24), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), -})); - -const ErrorText = styled.p((props) => ({ - ...props.theme.typography.body_s, - color: props.theme.colors.danger_medium, -})); - -const ReviewTransactionText = styled.span<{ - centerAligned: boolean; -}>((props) => ({ - ...props.theme.typography.headline_s, - color: props.theme.colors.white_0, - marginBottom: props.theme.space.l, - textAlign: props.centerAligned ? 'center' : 'left', -})); - -const CalloutContainer = styled.div((props) => ({ - marginBottom: props.theme.spacing(8), - marginhorizontal: props.theme.spacing(8), -})); - -const FeeContainer = styled.div` - position: relative; -`; - -const Label = styled.span` - ${(props) => props.theme.typography.body_medium_m}; - position: absolute; - top: 36px; - left: ${(props) => props.theme.space.m}; - color: ${(props) => props.theme.colors.tangerine}; - transition: color 0.1s ease; - cursor: pointer; - user-select: none; - display: inline-flex; - flex-direction: row; - align-items: center; - gap: ${(props) => props.theme.space.xxs}; - - &:hover { - color: ${(props) => props.theme.colors.tangerine_200}; - } -`; - -type Props = { - currentFee: BigNumber; - feePerVByte: BigNumber; // TODO tim: is this the same as currentFeeRate? refactor to be clear - loadingBroadcastedTx: boolean; - signedTxHex: string; - ordinalTxUtxo?: UTXO; - recipients: Recipient[]; - children?: ReactNode; - assetDetail?: string; - assetDetailValue?: string; - isRestoreFundFlow?: boolean; - nonOrdinalUtxos?: UTXO[]; - currencyType?: CurrencyTypes; - isPartOfBundle?: boolean; - ordinalBundle?: Bundle; - holdsRareSats?: boolean; - currentFeeRate: BigNumber; - setCurrentFee: (feeRate: BigNumber) => void; - setCurrentFeeRate: (feeRate: BigNumber) => void; - onConfirmClick: (signedTxHex: string) => void; - onCancelClick: () => void; -}; - -/** - * @deprecated should use ConfirmBtcTransaction master component - */ -function ConfirmBtcTransactionComponent({ - currentFee, - feePerVByte, - loadingBroadcastedTx, - signedTxHex, - ordinalTxUtxo, - recipients, - children, - assetDetail, - assetDetailValue, - isRestoreFundFlow, - nonOrdinalUtxos, - isPartOfBundle, - currencyType, - ordinalBundle, - holdsRareSats, - currentFeeRate, - setCurrentFee, - setCurrentFeeRate, - onConfirmClick, - onCancelClick, -}: Props) { - const { t } = useTranslation('translation'); - const [loading, setLoading] = useState(false); - const selectedAccount = useSelectedAccount(); - const { btcAddress } = selectedAccount; - const { network, feeMultipliers } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { selectedSatBundle } = useNftDataSelector(); - const { getSeed } = useSeedVault(); - const [showFeeSettings, setShowFeeSettings] = useState(false); - const [error, setError] = useState(''); - const [signedTx, setSignedTx] = useState(signedTxHex); - const [total, setTotal] = useState(new BigNumber(0)); - const [showFeeWarning, setShowFeeWarning] = useState(false); - const btcClient = useBtcClient(); - - const bundle = selectedSatBundle ?? ordinalBundle ?? undefined; - const { - isLoading, - data, - error: txError, - mutate, - } = useMutation< - SignedBtcTx, - ResponseError, - { - txRecipients: Recipient[]; - txFee: string; - seedPhrase: string; - } - >({ - mutationFn: async ({ txRecipients: newRecipients, txFee, seedPhrase }) => - signBtcTransaction( - newRecipients, - btcAddress, - selectedAccount?.id ?? 0, - seedPhrase, - btcClient, - network.type, - new BigNumber(txFee), - ), - }); - - if (typeof feePerVByte !== 'string' && !BigNumber.isBigNumber(feePerVByte)) { - Object.setPrototypeOf(feePerVByte, BigNumber.prototype); - } - - recipients.forEach((recipient) => { - if (typeof recipient.amountSats !== 'string' && !BigNumber.isBigNumber(recipient.amountSats)) { - Object.setPrototypeOf(recipient.amountSats, BigNumber.prototype); - } - }); - - const { - isLoading: isLoadingNonOrdinalBtcSend, - error: errorSigningNonOrdial, - data: signedNonOrdinalBtcSend, - mutate: mutateSignNonOrdinalBtcTransaction, - } = useMutation({ - mutationFn: async ({ txFee, seedPhrase }) => { - const signedNonOrdinalBtcTx = await signNonOrdinalBtcSendTransaction( - btcAddress, - nonOrdinalUtxos!, - selectedAccount?.id ?? 0, - seedPhrase, - network.type, - new BigNumber(txFee), - ); - return signedNonOrdinalBtcTx; - }, - }); - - const { ordinals, isLoading: ordinalsLoading } = useOrdinalsByAddress(btcAddress); - - const { - isLoading: isLoadingOrdData, - data: ordinalData, - error: ordinalError, - mutate: ordinalMutate, - } = useMutation({ - mutationFn: async ({ txFee, seedPhrase }) => { - const ordinalsUtxos = ordinals!.map((ord) => ord.utxo); - const newSignedTx = await signOrdinalSendTransaction( - recipients[0]?.address, - ordinalTxUtxo!, - btcAddress, - Number(selectedAccount?.id), - seedPhrase, - btcClient, - network.type, - ordinalsUtxos, - new BigNumber(txFee), - ); - return newSignedTx; - }, - }); - - useEffect(() => { - if (data) { - setCurrentFee(data.fee); - setSignedTx(data.signedTx); - setShowFeeSettings(false); - } - }, [data]); - - useEffect(() => { - if (ordinalData) { - setCurrentFee(ordinalData.fee); - setSignedTx(ordinalData.signedTx); - setShowFeeSettings(false); - } - }, [ordinalData]); - - useEffect(() => { - let sum: BigNumber = new BigNumber(0); - if (recipients?.length) { - recipients.forEach((recipient) => { - sum = sum.plus(recipient.amountSats); - }); - sum = sum?.plus(currentFee); - } - setTotal(sum); - }, [recipients, currentFee]); - - useEffect(() => { - if (signedNonOrdinalBtcSend) { - setCurrentFee(signedNonOrdinalBtcSend.fee); - setSignedTx(signedNonOrdinalBtcSend.signedTx); - setShowFeeSettings(false); - } - }, [signedNonOrdinalBtcSend]); - - useEffect(() => { - const isFeeHigh = - feeMultipliers && - currentFee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighSatsFee)); - setShowFeeWarning(!!isFeeHigh); - }, [currentFee, feeMultipliers]); - - const showEditFeesModal = () => { - setShowFeeSettings(true); - }; - - const hideEditFeesModal = () => { - setShowFeeSettings(false); - }; - - const handleApplyClick = async ({ - fee: modifiedFee, - feeRate, - }: { - fee: string; - feeRate?: string; - nonce?: string; - }) => { - const newFee = new BigNumber(modifiedFee); - setCurrentFee(newFee); - const seed = await getSeed(); - setCurrentFeeRate(new BigNumber(feeRate!)); - if (ordinalTxUtxo) ordinalMutate({ txFee: modifiedFee, seedPhrase: seed }); - else if (isRestoreFundFlow) { - mutateSignNonOrdinalBtcTransaction({ txFee: modifiedFee, seedPhrase: seed }); - } else { - mutate({ txRecipients: recipients, txFee: modifiedFee, seedPhrase: seed }); - } - setLoading(true); - }; - - const handleConfirmClick = () => { - onConfirmClick(signedTx); - }; - - const getAmountString = (amount: BigNumber, currency: string) => ( - - ); - - useEffect(() => { - if (recipients && txError) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(txError.toString()); - } - }, [txError]); - - useEffect(() => { - if (recipients && ordinalError) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(ordinalError.toString()); - } - }, [ordinalError]); - - useEffect(() => { - if (recipients && errorSigningNonOrdial) { - setShowFeeSettings(false); - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(errorSigningNonOrdial.toString()); - } - }, [errorSigningNonOrdial]); - - return ( - <> - - {showFeeWarning && ( - - - - )} - {/* TODO tim: refactor this not to use children. it should be just another prop */} - {children} - - {t('CONFIRM_TRANSACTION.REVIEW_TRANSACTION')} - - {isPartOfBundle && ( - - - - )} - {holdsRareSats && ( - - - - )} - {currencyType !== 'BTC' && bundle && } - - {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} - - {ordinalTxUtxo ? ( - - ) : ( - recipients?.map((recipient, index) => ( - - )) - )} - - {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} - - - - {t('CONFIRM_TRANSACTION.FEES')} - - - - - - - {!ordinalTxUtxo && recipients.length > 1 && ( - - )} - - {!!error && ( - - {error} - - )} - - -
diff --git a/src/app/components/sendForm/index.tsx b/src/app/components/sendForm/index.tsx index 87389a14b..2abbd8774 100644 --- a/src/app/components/sendForm/index.tsx +++ b/src/app/components/sendForm/index.tsx @@ -277,7 +277,7 @@ function SendForm({ ((props) => ({ - width: props.isSquare ? 18 : 22, - height: props.isSquare ? 18 : 22, - borderRadius: props.isSquare ? 0 : 22, + width: props.isSquare ? 18 : 20, + height: props.isSquare ? 18 : 20, + borderRadius: props.isSquare ? 0 : 20, position: 'absolute', - right: props.isSquare ? -9 : -11, + right: props.isSquare ? -9 : -10, bottom: -2, backgroundColor: props.theme.colors.elevation0, padding: 2, diff --git a/src/app/components/transferFeeView/index.tsx b/src/app/components/transferFeeView/index.tsx index 02a9f526a..31996f41d 100644 --- a/src/app/components/transferFeeView/index.tsx +++ b/src/app/components/transferFeeView/index.tsx @@ -69,7 +69,7 @@ function TransferFeeView({ - + {t('NETWORK_FEE')} diff --git a/src/app/hooks/queries/ordinals/useAddressInscription.ts b/src/app/hooks/queries/ordinals/useAddressInscription.ts index 9202dbb4b..d9537ede9 100644 --- a/src/app/hooks/queries/ordinals/useAddressInscription.ts +++ b/src/app/hooks/queries/ordinals/useAddressInscription.ts @@ -7,7 +7,7 @@ import { handleRetries, InvalidParamsError } from '@utils/query'; /** * Get inscription details belonging to an address by ordinalId */ -const useAddressInscription = (ordinalId: string, ordinal?: Inscription | null) => { +const useAddressInscription = (ordinalId?: string, ordinal?: Inscription | null) => { const { ordinalsAddress } = useSelectedAccount(); const { network } = useWalletSelector(); const fetchOrdinals = async (): Promise => { diff --git a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts index 850286a17..39525660d 100644 --- a/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts +++ b/src/app/hooks/queries/runes/useRuneFloorPriceQuery.ts @@ -9,18 +9,16 @@ export default function useRuneFloorPriceQuery(runeName: string, backgroundRefet const runesApi = useRunesApi(); const queryFn = useCallback( async () => - network.type === 'Mainnet' - ? runesApi - .getRuneMarketData(runeName) - .then((res) => Number(res.floorUnitPrice?.formatted ?? 0)) - : undefined, - [network.type, runeName, runesApi], + runesApi + .getRuneMarketData(runeName) + .then((res) => Number(res.floorUnitPrice?.formatted ?? 0)), + [runeName, runesApi], ); return useQuery({ refetchOnWindowFocus: backgroundRefetch, refetchOnReconnect: backgroundRefetch, queryKey: ['get-rune-floor-price', runeName], - enabled: Boolean(runeName), + enabled: Boolean(runeName) && network.type === 'Mainnet', queryFn, }); } diff --git a/src/app/hooks/useResetUserFlow.ts b/src/app/hooks/useResetUserFlow.ts index ebe39e2f6..7ecc488c8 100644 --- a/src/app/hooks/useResetUserFlow.ts +++ b/src/app/hooks/useResetUserFlow.ts @@ -11,21 +11,17 @@ const resetUserFlowChannel = 'resetUserFlow'; */ const userFlowConfig: Record = { '/send-btc': { resetTo: '/send-btc' }, - '/confirm-btc-tx': { resetTo: '/send-btc' }, '/send-brc20-one-step': { resetTo: '/' }, '/confirm-brc20-tx': { resetTo: '/' }, - '/confirm-inscription-request': { resetTo: '/' }, '/ordinals-collection': { resetTo: '/nft-dashboard?tab=inscriptions' }, '/ordinal-detail': { resetTo: '/nft-dashboard?tab=inscriptions' }, [RoutePaths.SendOrdinal]: { resetTo: '/nft-dashboard?tab=inscriptions' }, - '/confirm-ordinal-tx': { resetTo: '/nft-dashboard?tab=inscriptions' }, '/nft-collection': { resetTo: '/nft-dashboard?tab=nfts' }, '/nft-detail': { resetTo: '/nft-dashboard?tab=nfts' }, '/send-nft': { resetTo: '/nft-dashboard?tab=nfts' }, '/confirm-nft-tx': { resetTo: '/nft-dashboard?tab=nfts' }, '/rare-sats-detail': { resetTo: '/nft-dashboard?tab=rareSats' }, '/rare-sats-bundle': { resetTo: '/nft-dashboard?tab=rareSats' }, - '/send-rare-sat': { resetTo: '/nft-dashboard?tab=rareSats' }, '/verify-ledger': { resetTo: '/verify-ledger?mismatch=true' }, '/add-stx-address-ledger': { resetTo: '/add-stx-address-ledger?mismatch=true' }, '/send-rune': { resetTo: '/' }, diff --git a/src/app/hooks/useSearchParamsState.ts b/src/app/hooks/useSearchParamsState.ts new file mode 100644 index 000000000..5a88747e4 --- /dev/null +++ b/src/app/hooks/useSearchParamsState.ts @@ -0,0 +1,20 @@ +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +const useSearchParamsState = (key: string, defaultValue: T): [T, (newValue: T) => void] => { + const [searchParams, setSearchParams] = useSearchParams(); + + const paramValue = searchParams.get(key); + const initialValue = paramValue !== null ? (JSON.parse(paramValue) as T) : defaultValue; + + const [value, setValue] = useState(initialValue); + const customSetter = (newValue: T) => { + setValue(newValue); + searchParams.set(key, JSON.stringify(newValue)); + setSearchParams(searchParams); + }; + + return [value, customSetter]; +}; + +export default useSearchParamsState; diff --git a/src/app/hooks/useTextOrdinalContent.ts b/src/app/hooks/useTextOrdinalContent.ts index 00a6040ac..998749382 100644 --- a/src/app/hooks/useTextOrdinalContent.ts +++ b/src/app/hooks/useTextOrdinalContent.ts @@ -6,11 +6,15 @@ import useWalletSelector from './useWalletSelector'; const queue = new PQueue({ concurrency: 1 }); -const useTextOrdinalContent = (ordinal: Inscription | CondensedInscription) => { +const useTextOrdinalContent = (ordinal?: Inscription | CondensedInscription) => { const { network } = useWalletSelector(); const { data: textContent } = useQuery({ + enabled: !!ordinal?.id, queryKey: ['ordinal-text', ordinal?.id, network.type], - queryFn: async () => queue.add(() => getTextOrdinalContent(network.type, ordinal?.id)), + queryFn: async () => { + if (!ordinal?.id) return; + return queue.add(() => getTextOrdinalContent(network.type, ordinal?.id)); + }, staleTime: 5 * 60 * 1000, // 5 min }); diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 749ad7e6e..636f1f0b7 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -7,16 +7,11 @@ import ScreenContainer from '@components/screenContainer'; import AccountList from '@screens/accountList'; import BackupWallet from '@screens/backupWallet'; import BackupWalletSteps from '@screens/backupWalletSteps'; -import BtcSendScreen from '@screens/btcSendScreen'; import Buy from '@screens/buy'; import CoinDashboard from '@screens/coinDashboard'; import ConfirmBrc20Transaction from '@screens/confirmBrc20Transaction'; -import ConfirmBtcTransaction from '@screens/confirmBtcTransaction'; import ConfirmFtTransaction from '@screens/confirmFtTransaction'; -import ConfirmInscriptionRequest from '@screens/confirmInscriptionRequest'; import ConfirmNftTransaction from '@screens/confirmNftTransaction'; -import ConfirmOrdinalTransaction from '@screens/confirmOrdinalTransaction'; -import SendInscriptionsRequest from '@screens/confirmOrdinalTransaction/SendInscriptionsRequest'; import ConfirmStxTransaction from '@screens/confirmStxTransaction'; import AuthenticationRequest from '@screens/connect/authenticationRequest'; import BtcSelectAddressScreen from '@screens/connect/btcSelectAddressScreen'; @@ -57,9 +52,9 @@ import RestoreWallet from '@screens/restoreWallet'; import SendBrc20OneStepScreen from '@screens/sendBrc20OneStep'; import SendBtcScreen from '@screens/sendBtc'; import SendSip10Screen from '@screens/sendFt'; +import SendInscriptionsRequest from '@screens/sendInscriptionsRequest'; import SendNft from '@screens/sendNft'; import SendOrdinal from '@screens/sendOrdinal'; -import SendRareSat from '@screens/sendRareSat'; import SendRuneScreen from '@screens/sendRune'; import SendStxScreen from '@screens/sendStx'; import Setting from '@screens/settings'; @@ -85,6 +80,7 @@ import TransactionStatus from '@screens/transactionStatus'; import TransferRunesRequest from '@screens/transferRunesRequest'; import UnlistRuneScreen from '@screens/unlistRune'; import WalletExists from '@screens/walletExists'; +import BtcSendRequest from 'app/screens/btcSendRequest'; import ListRuneScreen from 'app/screens/listRune'; import { createHashRouter } from 'react-router-dom'; import RoutePaths from './paths'; @@ -279,7 +275,7 @@ const router = createHashRouter([ path: RequestsRoutes.SendBtcTx, element: ( - + ), }, @@ -455,14 +451,6 @@ const router = createHashRouter([ ), }, - { - path: 'confirm-inscription-request', - element: ( - - - - ), - }, { path: 'lockCountdown', element: ( @@ -520,11 +508,6 @@ const router = createHashRouter([ path: 'send-stx', element: , }, - // TODO can we kill this one? - { - path: 'confirm-btc-tx', - element: , - }, { path: 'send-rune', element: , @@ -585,22 +568,6 @@ const router = createHashRouter([ ), }, - { - path: 'nft-dashboard/send-rare-sat', - element: ( - - - - ), - }, - { - path: 'nft-dashboard/confirm-ordinal-tx/:id', - element: ( - - - - ), - }, { path: 'nft-dashboard/supported-rarity-scale', element: , diff --git a/src/app/screens/btcSendRequest/index.tsx b/src/app/screens/btcSendRequest/index.tsx new file mode 100644 index 000000000..a2746aeb2 --- /dev/null +++ b/src/app/screens/btcSendRequest/index.tsx @@ -0,0 +1,229 @@ +import { makeRPCError, sendRpcResponse } from '@common/utils/rpc/helpers'; +import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; +import useBtcFeeRate from '@hooks/useBtcFeeRate'; +import useHasFeature from '@hooks/useHasFeature'; +import useSelectedAccount from '@hooks/useSelectedAccount'; +import useTransactionContext from '@hooks/useTransactionContext'; +import useWalletSelector from '@hooks/useWalletSelector'; +import { RpcErrorCode } from '@sats-connect/core'; +import type { TransactionSummary } from '@screens/sendBtc/helpers'; +import { + AnalyticsEvents, + btcTransaction, + FeatureId, + parseSummaryForRunes, + type RuneSummary, + type Transport, +} from '@secretkeylabs/xverse-core'; +import Spinner from '@ui-library/spinner'; +import { BITCOIN_DUST_AMOUNT_SATS } from '@utils/constants'; +import { trackMixPanel } from '@utils/mixpanel'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; +import useBtcSendRequestPayload from './useBtcSendRequestPayload'; + +const OuterContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + align-items: center; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; + +function BtcSendRequest() { + const { network } = useWalletSelector(); + const navigate = useNavigate(); + const selectedAccount = useSelectedAccount(); + const { payload, tabId, requestId } = useBtcSendRequestPayload( + selectedAccount.btcAddress, + network, + ); + const transactionContext = useTransactionContext(); + const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); + const { t } = useTranslation('translation'); + const [feeRate, setFeeRate] = useState(''); + const [transaction, setTransaction] = useState(); + const [summary, setSummary] = useState(); + const [runeSummary, setRuneSummary] = useState(); + const hasRunesSupport = useHasFeature(FeatureId.RUNES_SUPPORT); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + + const generateTx = (desiredFeeRate: number): Promise => + btcTransaction.sendBtc( + transactionContext, + payload.recipients.map((value) => ({ + toAddress: value.address, + amount: BigInt(value.amountSats.toString()), + })), + +desiredFeeRate, + ); + + useEffect(() => { + if (!feeRate && btcFeeRate && !feeRatesLoading) { + setFeeRate(btcFeeRate.regular.toString()); + } + }, [btcFeeRate, feeRatesLoading]); + + useEffect(() => { + if (!payload || !transactionContext || !feeRate) { + setSummary(undefined); + setRuneSummary(undefined); + return; + } + const generateTxnAndSummary = async () => { + try { + setIsLoading(true); + const newTransaction = await generateTx(+feeRate); + const newSummary = await newTransaction.getSummary(); + setTransaction(newTransaction); + setSummary(newSummary); + if (newSummary && hasRunesSupport) { + setRuneSummary( + await parseSummaryForRunes(transactionContext, newSummary, transactionContext.network), + ); + } + } catch (e) { + setTransaction(undefined); + setSummary(undefined); + let error; + let errorTitle; + if (e instanceof Error && e.message.includes('Insufficient funds')) { + const errorResponse = makeRPCError(requestId, { + code: RpcErrorCode.INTERNAL_ERROR, + message: 'Insufficient balance', + }); + errorTitle = t('ERRORS.INVALID_TRANSACTION'); + error = t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES'); + sendRpcResponse(+tabId, errorResponse); + } + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + errorTitle, + error, + browserTx: true, + }, + }); + } finally { + setIsLoading(false); + } + }; + generateTxnAndSummary(); + }, [transactionContext, payload, feeRate]); + + useEffect(() => { + const checkIfMismatch = () => { + if (payload.senderAddress !== selectedAccount.btcAddress) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'STX', + error: t('CONFIRM_TRANSACTION.ADDRESS_MISMATCH'), + browserTx: true, + }, + }); + } + if (payload.network.type !== network.type) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('CONFIRM_TRANSACTION.NETWORK_MISMATCH'), + browserTx: true, + }, + }); + } + }; + + checkIfMismatch(); + }, [payload]); + + useEffect(() => { + const checkIfValidAmount = () => { + payload.recipients.forEach((txRecipient) => { + if (txRecipient.amountSats < BITCOIN_DUST_AMOUNT_SATS) { + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: t('SEND.ERRORS.BELOW_MINIMUM_AMOUNT'), + browserTx: true, + }, + }); + } + }); + }; + checkIfValidAmount(); + }, [payload]); + + const handleSubmit = async (ledgerTransport?: Transport) => { + try { + setIsSubmitting(true); + const txnId = await transaction?.broadcast({ ledgerTransport, rbfEnabled: true }); + trackMixPanel(AnalyticsEvents.TransactionConfirmed, { + protocol: 'bitcoin', + action: 'transfer', + wallet_type: selectedAccount.accountType || 'software', + }); + navigate('/tx-status', { + state: { + txid: txnId, + currency: 'BTC', + error: '', + browserTx: true, + }, + }); + } catch (e) { + console.error(e); + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: `${e}`, + browserTx: true, + }, + }); + } finally { + setIsSubmitting(false); + } + }; + + const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { + const tempTx = await generateTx(desiredFeeRate); + const tempSummary = await tempTx.getSummary(); + if (tempSummary) return Number(tempSummary.fee); + return undefined; + }; + + if (feeRatesLoading || !payload || !summary) { + return {isLoading && }; + } + + return ( + window.close()} + onConfirm={handleSubmit} + getFeeForFeeRate={calculateFeeForFeeRate} + onFeeRateSet={(newFeeRate) => setFeeRate(newFeeRate.toString())} + feeRate={+feeRate} + isSubmitting={isSubmitting} + isBroadcast + hideBottomBar + /> + ); +} + +export default BtcSendRequest; diff --git a/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts new file mode 100644 index 000000000..3486a7e7d --- /dev/null +++ b/src/app/screens/btcSendRequest/useBtcSendRequestPayload.ts @@ -0,0 +1,46 @@ +import { + BitcoinNetworkType, + type SendBtcTransactionOptions, + type SendBtcTransactionPayload, +} from '@sats-connect/core'; +import { type SettingsNetwork } from '@secretkeylabs/xverse-core'; +import { decodeToken } from 'jsontokens'; +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +const useBtcSendRequestPayload = (btcAddress: string, network: SettingsNetwork) => { + const { search } = useLocation(); + const params = new URLSearchParams(search); + const tabId = params.get('tabId') ?? '0'; + const requestId = params.get('requestId') ?? ''; + + const { payload, requestToken } = useMemo(() => { + const token = params.get('sendBtcRequest') ?? ''; + if (token) { + const request = decodeToken(token) as any as SendBtcTransactionOptions; + return { + payload: request.payload, + requestToken: token, + }; + } + const recipients = JSON.parse(params.get('recipients')!); + const transferRecipients = + recipients?.map((value) => ({ + address: value.address, + amountSats: BigInt(value.amount), + })) ?? []; + const rpcPayload: SendBtcTransactionPayload = { + senderAddress: btcAddress, + recipients: transferRecipients, + network: { type: BitcoinNetworkType[network.type] }, + }; + return { + payload: rpcPayload, + requestToken: null, + }; + }, []); + + return { payload, tabId, requestToken, requestId }; +}; + +export default useBtcSendRequestPayload; diff --git a/src/app/screens/btcSendScreen/index.tsx b/src/app/screens/btcSendScreen/index.tsx deleted file mode 100644 index 8615e11a0..000000000 --- a/src/app/screens/btcSendScreen/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { makeRPCError, sendRpcResponse } from '@common/utils/rpc/helpers'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { RpcErrorCode } from '@sats-connect/core'; -import { ErrorCodes, getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; -import Spinner from '@ui-library/spinner'; -import { BITCOIN_DUST_AMOUNT_SATS } from '@utils/constants'; -import BigNumber from 'bignumber.js'; -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; -import useSendBtcRequest from './useSendBtcRequest'; - -const OuterContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - justify-content: center; - align-items: center; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -function BtcSendScreen() { - const { payload, signedTx, isLoading, tabId, requestToken, requestId, error, recipient } = - useSendBtcRequest(); - const navigate = useNavigate(); - const { btcAddress } = useSelectedAccount(); - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { t } = useTranslation('translation'); - - useEffect(() => { - const checkIfMismatch = () => { - if (payload.senderAddress !== btcAddress) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'STX', - error: t('CONFIRM_TRANSACTION.ADDRESS_MISMATCH'), - browserTx: true, - }, - }); - } - if (payload.network.type !== network.type) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('CONFIRM_TRANSACTION.NETWORK_MISMATCH'), - browserTx: true, - }, - }); - } - }; - - checkIfMismatch(); - }, [payload]); - - useEffect(() => { - const checkIfValidAmount = () => { - recipient.forEach((txRecipient) => { - if (txRecipient.amountSats.lt(BITCOIN_DUST_AMOUNT_SATS)) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: t('SEND.ERRORS.BELOW_MINIMUM_AMOUNT'), - browserTx: true, - }, - }); - } - }); - }; - checkIfValidAmount(); - }, [recipient]); - - useEffect(() => { - if (error) { - if ( - Number(error) === ErrorCodes.InSufficientBalanceWithTxFee || - Number(error) === ErrorCodes.InSufficientBalance - ) { - const errorResponse = makeRPCError(requestId, { - code: RpcErrorCode.INTERNAL_ERROR, - message: 'Insufficient balance', - }); - sendRpcResponse(+tabId, errorResponse); - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - errorTitle: t('TX_ERRORS.INVALID_TRANSACTION'), - error: t('TX_ERRORS.INSUFFICIENT_BALANCE'), - browserTx: true, - }, - }); - } else { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error, - browserTx: true, - }, - }); - } - } - }, [error]); - - useEffect(() => { - if (signedTx) { - navigate('/confirm-btc-tx', { - state: { - signedTxHex: signedTx.signedTx, - recipient, - fee: signedTx.fee, - feePerVByte: signedTx.feePerVByte, - fiatFee: getBtcFiatEquivalent(signedTx.fee, BigNumber(btcFiatRate)), - total: signedTx.total, - fiatTotal: getBtcFiatEquivalent(signedTx.total, BigNumber(btcFiatRate)), - btcSendBrowserTx: true, - requestToken, - tabId, - requestId, - }, - }); - } - }, [signedTx]); - - return {isLoading && }; -} - -export default BtcSendScreen; diff --git a/src/app/screens/btcSendScreen/useSendBtcRequest.ts b/src/app/screens/btcSendScreen/useSendBtcRequest.ts deleted file mode 100644 index df9330788..000000000 --- a/src/app/screens/btcSendScreen/useSendBtcRequest.ts +++ /dev/null @@ -1,109 +0,0 @@ -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { - BitcoinNetworkType, - type SendBtcTransactionOptions, - type SendBtcTransactionPayload, -} from '@sats-connect/core'; -import { - signBtcTransaction, - type Recipient, - type SettingsNetwork, -} from '@secretkeylabs/xverse-core'; -import { useQuery } from '@tanstack/react-query'; -import BigNumber from 'bignumber.js'; -import { decodeToken } from 'jsontokens'; -import { useMemo, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import useBtcClient from '../../hooks/apiClients/useBtcClient'; -import useSeedVault from '../../hooks/useSeedVault'; -import useWalletSelector from '../../hooks/useWalletSelector'; - -const useSendBtcRequestParams = (btcAddress: string, network: SettingsNetwork) => { - const { search } = useLocation(); - const params = new URLSearchParams(search); - const tabId = params.get('tabId') ?? '0'; - const requestId = params.get('requestId') ?? ''; - - const { payload, requestToken } = useMemo(() => { - const token = params.get('sendBtcRequest') ?? ''; - if (token) { - const request = decodeToken(token) as any as SendBtcTransactionOptions; - return { - payload: request.payload, - requestToken: token, - }; - } - const recipients = JSON.parse(params.get('recipients')!); - const transferRecipients = recipients?.map((value) => ({ - address: value.address, - amountSats: BigInt(value.amount), - })); - const rpcPayload: SendBtcTransactionPayload = { - senderAddress: btcAddress, - recipients: transferRecipients, - network: { type: BitcoinNetworkType[network.type] }, - }; - return { - payload: rpcPayload, - requestToken: null, - }; - }, []); - - return { payload, tabId, requestToken, requestId }; -}; - -function useSendBtcRequest() { - const btcClient = useBtcClient(); - const { getSeed } = useSeedVault(); - const selectedAccount = useSelectedAccount(); - const { network } = useWalletSelector(); - const { payload, tabId, requestToken, requestId } = useSendBtcRequestParams( - selectedAccount.btcAddress, - network, - ); - const [recipient, setRecipient] = useState([]); - - const generateSignedTransaction = async () => { - const seedPhrase = await getSeed(); - const recipients: Recipient[] = []; - payload.recipients?.forEach(async (value) => { - const txRecipient: Recipient = { - address: value.address, - amountSats: new BigNumber(value.amountSats.toString()), - }; - recipients.push(txRecipient); - }); - setRecipient(recipients); - const signedTx = await signBtcTransaction( - recipients, - payload.senderAddress || selectedAccount.btcAddress, - selectedAccount?.id ?? 0, - seedPhrase, - btcClient, - network.type, - ); - return signedTx; - }; - - const { - data: signedTx, - isLoading, - error, - } = useQuery({ - queryKey: ['generate-signed-transaction'], - queryFn: generateSignedTransaction, - }); - - return { - payload, - tabId, - requestToken, - requestId, - signedTx, - isLoading, - error, - recipient, - }; -} - -export default useSendBtcRequest; diff --git a/src/app/screens/confirmBrc20Transaction/amountRow.tsx b/src/app/screens/confirmBrc20Transaction/amountRow.tsx index 0fc722753..075bfb3e3 100644 --- a/src/app/screens/confirmBrc20Transaction/amountRow.tsx +++ b/src/app/screens/confirmBrc20Transaction/amountRow.tsx @@ -1,5 +1,5 @@ -import styled from 'styled-components'; import { StyledP } from '@ui-library/common.styled'; +import styled from 'styled-components'; const RowContainer = styled.div` display: flex; @@ -10,45 +10,53 @@ const RowContainer = styled.div` const LabelContainer = styled.div` display: flex; align-items: center; - gap: ${(props) => props.theme.spacing(5)}px; + gap: ${(props) => props.theme.space.m}; `; -const SubTextContainer = styled.div` - margin-top: -5px; - min-height: 16px; - width: 100%; +const TextContainer = styled.div` text-align: end; `; +const StyledSubtext = styled(StyledP)` + margin-top: ${(props) => props.theme.space.xxxs}; +`; + function AmountRow({ icon, amountLabel, amount, amountSubText, + ticker, }: { icon: React.ReactNode; amountLabel: string; amount: React.ReactNode; amountSubText: React.ReactNode; + ticker: string; }) { return (
{icon} - - {amountLabel} - +
+ + {amountLabel} + + + {ticker} Token + +
- - {amount} - + + + {amount} + + + {amountSubText} + +
- - - {amountSubText} - -
); } diff --git a/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx b/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx index c53b97732..9178a9573 100644 --- a/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx +++ b/src/app/screens/confirmBrc20Transaction/brc20FeesComponent.tsx @@ -1,33 +1,23 @@ -import { currencySymbolMap, type SupportedCurrency } from '@secretkeylabs/xverse-core'; +import { StyledFiatAmountText } from '@components/fiatAmountText'; +import { type SupportedCurrency } from '@secretkeylabs/xverse-core'; import BigNumber from 'bignumber.js'; -import { useTranslation } from 'react-i18next'; import { NumericFormat } from 'react-number-format'; import styled from 'styled-components'; -const Card = styled.div` - background: ${(props) => props.theme.colors.elevation1}; - border-radius: ${(props) => props.theme.radius(2)}px; - padding: ${(props) => props.theme.spacing(8)}px; -`; - -const CardTitle = styled.div` - margin-bottom: ${(props) => props.theme.spacing(8)}px; -`; - const TextLabel = styled.label` - ${(props) => props.theme.body_medium_m} - color: ${(props) => props.theme.colors.white_200}; + ${(props) => props.theme.typography.body_medium_m} + color: ${(props) => props.theme.colors.white_0}; `; const TextValue = styled.p` - ${(props) => props.theme.body_medium_m} + ${(props) => props.theme.typography.body_medium_m} color: ${(props) => props.theme.colors.white_0}; `; const Rows = styled.div` display: flex; flex-direction: column; - gap: ${(props) => props.theme.spacing(8)}px; + gap: ${(props) => props.theme.space.m}; `; const RowItem = styled.div` @@ -35,59 +25,48 @@ const RowItem = styled.div` justify-content: space-between; `; -const FiatAmountText = styled.p` - ${(props) => props.theme.body_medium_s} +const FeeRateText = styled.p` + ${(props) => props.theme.typography.body_medium_s} color: ${(props) => props.theme.colors.white_400}; display: flex; justify-content: flex-end; + margin-top: ${(props) => props.theme.space.xxs}; `; -function Brc20FeesComponent({ - fees, -}: { - fees: { - label: string; - value: BigNumber; - suffix: string; - fiatValue?: BigNumber; - fiatCurrency?: SupportedCurrency; - }[]; -}) { - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); +type Props = { + label: string; + value: BigNumber; + suffix: string; + fiatValue?: BigNumber; + fiatCurrency?: SupportedCurrency; + feeRate?: number; +}; + +function Brc20FeesComponent({ label, value, suffix, fiatValue, fiatCurrency, feeRate }: Props) { return ( - - - {t('FEES')} - - - {fees.map(({ label, value, suffix, fiatValue, fiatCurrency }) => ( -
- - {label} - - - - - {fiatValue && fiatCurrency && ( - - - - )} -
- ))} -
-
+ +
+ + {label} + + + + + {feeRate && ( + + + + )} + {fiatValue && fiatCurrency && ( + + )} +
+
); } diff --git a/src/app/screens/confirmBrc20Transaction/editFees.tsx b/src/app/screens/confirmBrc20Transaction/editFees.tsx deleted file mode 100644 index 2908f33d3..000000000 --- a/src/app/screens/confirmBrc20Transaction/editFees.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import { BetterBarLoader } from '@components/barLoader'; -import BottomModal from '@components/bottomModal'; -import ActionButton from '@components/button'; -import FiatAmountText from '@components/fiatAmountText'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useBtcFeeRate from '@hooks/useBtcFeeRate'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent } from '@secretkeylabs/xverse-core'; -import { InputFeedback } from '@ui-library/inputFeedback'; -import { handleKeyDownFeeRateInput } from '@utils/helper'; -import BigNumber from 'bignumber.js'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(2), -})); - -const DetailText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(8), -})); - -const Text = styled.h1((props) => ({ - ...props.theme.body_medium_m, - marginTop: props.theme.spacing(8), -})); - -// TODO create input component in ui-library -const InputContainer = styled.div<{ withError?: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(6), - border: `1px solid ${ - props.withError ? props.theme.colors.danger_dark_200 : props.theme.colors.white_800 - }`, - backgroundColor: props.theme.colors.elevation1, - borderRadius: props.theme.radius(1), - padding: props.theme.spacing(5), -})); - -const InputField = styled.input((props) => ({ - ...props.theme.body_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - border: 'transparent', - width: '50%', - '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - margin: 0, - }, - '&[type=number]': { - '-moz-appearance': 'textfield', - }, -})); - -const FeeText = styled.h1((props) => ({ - ...props.theme.body_m, - color: props.theme.colors.white_0, -})); - -const ButtonContainer = styled.div` - display: flex; - flex-direction: row; - margin-top: ${(props) => props.theme.spacing(6)}px; - gap: ${(props) => props.theme.spacing(4)}px; -`; - -const FeeButton = styled.button<{ - isSelected: boolean; -}>((props) => ({ - ...props.theme.body_medium_m, - color: `${props.isSelected ? props.theme.colors.elevation2 : props.theme.colors.white_400}`, - background: `${props.isSelected ? props.theme.colors.white : 'transparent'}`, - border: `1px solid ${props.isSelected ? 'transparent' : props.theme.colors.elevation6}`, - borderRadius: 40, - height: 40, - display: 'flex', - flex: 1, - justifyContent: 'center', - alignItems: 'center', -})); - -const FeeContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const TickerContainer = styled.div({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-end', - flex: 1, - minHeight: 34, -}); - -const ApplyButtonContainer = styled.div` - display: flex; - flex-direction: column; - margin: 20px 16px 40px; -`; - -const StyledInputFeedback = styled(InputFeedback)` - margin-bottom: ${(props) => props.theme.spacing(2)}px; -`; - -const StyledFiatAmountText = styled(FiatAmountText)((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.white_400, -})); - -const buttons = [ - { - value: 'standard', - label: 'TRANSACTION_SETTING.STANDARD', - }, - { - value: 'high', - label: 'TRANSACTION_SETTING.HIGH', - }, - { - value: 'custom', - label: 'TRANSACTION_SETTING.CUSTOM', - }, -]; - -export type OnChangeFeeRate = (feeRate: string) => void; - -export function EditFees({ - visible, - onClose, - onClickApply, - onChangeFeeRate, - fee, - initialFeeRate, - isFeeLoading, - error, -}: { - visible: boolean; - onClose: () => void; - onClickApply: OnChangeFeeRate; - onChangeFeeRate: OnChangeFeeRate; - fee: string; - initialFeeRate: string; - isFeeLoading: boolean; - error: string; -}) { - const { t } = useTranslation('translation'); - - const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { data: feeRates } = useBtcFeeRate(); - - // save the previous state in case user clicks X without applying - const [previousFeeRate, setPreviousFeeRate] = useState(initialFeeRate); - const [previousSelectedOption, setPreviousSelectedOption] = useState('standard'); - - const [feeRateInput, setFeeRateInput] = useState(previousFeeRate); - const [selectedOption, setSelectedOption] = useState(previousSelectedOption); - - const inputRef = useRef(null); - - useEffect(() => { - onChangeFeeRate(feeRateInput); - }, [feeRateInput, onChangeFeeRate]); - - /* callbacks */ - const handleChangeFeeRateInput = (e: React.ChangeEvent) => { - setFeeRateInput(e.target.value); - if (selectedOption !== 'custom') { - setSelectedOption('custom'); - } - }; - - const handleClickFeeButton = (e: React.MouseEvent) => { - setSelectedOption(e.currentTarget.value); - if (feeRates) { - switch (e.currentTarget.value) { - case 'high': - setFeeRateInput(feeRates.priority.toString()); - break; - case 'standard': - setFeeRateInput(feeRates.regular.toString()); - break; - case 'custom': - inputRef.current?.focus(); - break; - default: - break; - } - } - }; - - const handleClickClose = () => { - // reset state - setFeeRateInput(previousFeeRate); - setSelectedOption(previousSelectedOption); - onClose(); - }; - - const handleClickApply = () => { - // save state - setPreviousFeeRate(feeRateInput); - setPreviousSelectedOption(selectedOption); - // apply state to parent - onClickApply(feeRateInput); - onClose(); - }; - - const fiatFee = getBtcFiatEquivalent(new BigNumber(fee), BigNumber(btcFiatRate)); - - return ( - - - {t('TRANSACTION_SETTING.FEE_RATE')} - - - - {t('UNITS.SATS_PER_VB')} - - {isFeeLoading ? ( - <> - - - - ) : ( - <> - {value}} - /> - - - )} - - - - - - {buttons.map(({ value, label }) => ( - - {t(label)} - - ))} - - {t('TRANSACTION_SETTING.FEE_INFO')} - - - - - - ); -} diff --git a/src/app/screens/confirmBrc20Transaction/index.styled.ts b/src/app/screens/confirmBrc20Transaction/index.styled.ts new file mode 100644 index 000000000..59fdae2ef --- /dev/null +++ b/src/app/screens/confirmBrc20Transaction/index.styled.ts @@ -0,0 +1,80 @@ +import styled from 'styled-components'; + +export const ScrollContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow-y: auto; +`; + +export const OuterContainer = styled.div` + display: flex; + flex-direction: column; + overflow-y: auto; + &::-webkit-scrollbar { + display: none; + } +`; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + marginTop: props.theme.space.l, + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, + marginBottom: props.theme.space.m, +})); + +export const Subtitle = styled.p` + ${(props) => props.theme.typography.body_medium_m}; + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.xs}; +`; + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + gap: props.theme.space.s, + marginLeft: props.theme.space.m, + marginRight: props.theme.space.m, + marginTop: props.theme.space.l, + marginBottom: props.theme.space.l, +})); + +export const ErrorContainer = styled.div((props) => ({ + marginTop: props.theme.space.m, +})); + +export const ErrorText = styled.p((props) => ({ + ...props.theme.body_xs, + color: props.theme.colors.feedback.error, +})); + +export const ReviewTransactionText = styled.h1((props) => ({ + ...props.theme.headline_s, + color: props.theme.colors.white_0, + marginBottom: props.theme.spacing(10), + textAlign: 'left', +})); + +export const StyledCallouts = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.s}; + margin-bottom: ${(props) => props.theme.space.l}; +`; + +export const RecipientCardContainer = styled.div` + margin-bottom: ${(props) => props.theme.space.s}; +`; + +export const FeeContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${(props) => props.theme.space.m}; + padding: ${(props) => props.theme.space.m}; + background-color: ${(props) => props.theme.colors.elevation1}; + border-radius: ${(props) => props.theme.radius(2)}px; +`; diff --git a/src/app/screens/confirmBrc20Transaction/index.tsx b/src/app/screens/confirmBrc20Transaction/index.tsx index 256ab37e0..c54a28cb2 100644 --- a/src/app/screens/confirmBrc20Transaction/index.tsx +++ b/src/app/screens/confirmBrc20Transaction/index.tsx @@ -1,26 +1,27 @@ -import AccountHeaderComponent from '@components/accountHeader'; -import ActionButton from '@components/button'; import InfoContainer from '@components/infoContainer'; import BottomBar from '@components/tabBar'; +import TopRow from '@components/topRow'; import TransactionDetailComponent from '@components/transactionDetailComponent'; import useCoinRates from '@hooks/queries/useCoinRates'; +import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useDebounce from '@hooks/useDebounce'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; import useWalletSelector from '@hooks/useWalletSelector'; -import { FadersHorizontal } from '@phosphor-icons/react'; -import type { BRC20ErrorCode, SettingsNetwork } from '@secretkeylabs/xverse-core'; +import type { BRC20ErrorCode } from '@secretkeylabs/xverse-core'; import { AnalyticsEvents, + brc20TransferEstimateFees, getBtcFiatEquivalent, useBrc20TransferFees, validateBtcAddressIsTaproot, } from '@secretkeylabs/xverse-core'; +import SelectFeeRate from '@ui-components/selectFeeRate'; +import Button from '@ui-library/button'; import Callout, { type CalloutProps } from '@ui-library/callout'; import { getFeeValuesForBrc20OneStepTransfer, - type Brc20TransferEstimateFeesParams, type ConfirmBrc20TransferState, type ExecuteBrc20TransferState, } from '@utils/brc20'; @@ -30,112 +31,23 @@ import BigNumber from 'bignumber.js'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import Brc20FeesComponent from './brc20FeesComponent'; -import { EditFees, type OnChangeFeeRate } from './editFees'; +import { + ButtonContainer, + Container, + ErrorContainer, + ErrorText, + FeeContainer, + OuterContainer, + RecipientCardContainer, + ReviewTransactionText, + ScrollContainer, + StyledCallouts, + Subtitle, +} from './index.styled'; import RecipientCard, { type RecipientCardProps } from './recipientCard'; -const ScrollContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - overflow-y: auto; -`; - -const OuterContainer = styled.div` - display: flex; - flex-direction: column; - overflow-y: auto; - &::-webkit-scrollbar { - display: none; - } -`; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - marginTop: props.theme.spacing(16), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - gap: props.theme.spacing(8), - marginLeft: props.theme.spacing(8), - marginRight: props.theme.spacing(8), - marginTop: props.theme.spacing(12), - marginBottom: props.theme.spacing(12), -})); - -const EditFeesButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: props.theme.spacing(3), - borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - marginTop: props.theme.spacing(12), -})); - -const ButtonText = styled.div((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const FadersHorizontalIcon = styled(FadersHorizontal)((props) => ({ - color: props.theme.colors.white_0, -})); - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.spacing(8), -})); - -const ErrorText = styled.p((props) => ({ - ...props.theme.body_xs, - color: props.theme.colors.feedback.error, -})); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.headline_s, - color: props.theme.colors.white[0], - marginBottom: props.theme.spacing(10), - textAlign: 'left', -})); - -const StyledCallouts = styled.div` - display: flex; - flex-direction: column; - gap: ${(props) => props.theme.spacing(6)}px; - margin-bottom: ${(props) => props.theme.spacing(12)}px; -`; - -const RecipientCardContainer = styled.div` - margin-bottom: ${(props) => props.theme.spacing(6)}px; -`; - -const useConfirmBrc20Transfer = (): { - callouts: CalloutProps[]; - errorMessage: string; - estimateFeesParams: Brc20TransferEstimateFeesParams; - fees: any[]; - handleClickAdvancedSetting: () => void; - handleClickApplyFee: OnChangeFeeRate; - handleClickCancel: () => void; - handleClickCloseFees: () => void; - handleClickConfirm: () => void; - isConfirmLoading: boolean; - isFeeLoading: boolean; - network: SettingsNetwork; - recipient: RecipientCardProps; - showFeeSettings: boolean; - txFee: BigNumber; - showFeeWarning: boolean; -} => { +function ConfirmBrc20Transaction() { /* hooks */ const { t } = useTranslation('translation'); const { network, fiatCurrency, feeMultipliers } = useWalletSelector(); @@ -151,15 +63,15 @@ const useConfirmBrc20Transfer = (): { }: ConfirmBrc20TransferState = useLocation().state; const [showFeeWarning, setShowFeeWarning] = useState(false); const transactionContext = useTransactionContext(); + const { data: recommendedFees } = useBtcFeeRate(); useResetUserFlow('/confirm-brc20-tx'); /* state */ const [isConfirmLoading, setIsConfirmLoading] = useState(false); - const [showFeeSettings, setShowFeeSettings] = useState(false); - const [userInputFeeRate, setUserInputFeeRate] = useState(''); + const [userInputFeeRate, setUserInputFeeRate] = useState(estimateFeesParams.feeRate.toString()); const [error, setError] = useState(''); - const debouncedUserInputFeeRate = useDebounce(userInputFeeRate, 100); + const debouncedUserInputFeeRate = useDebounce(userInputFeeRate, 500); const { commitValueBreakdown, @@ -168,7 +80,7 @@ const useConfirmBrc20Transfer = (): { } = useBrc20TransferFees({ ...estimateFeesParams, feeRate: Number(debouncedUserInputFeeRate), - skipInitialFetch: true, + skipInitialFetch: false, context: transactionContext, }); @@ -208,45 +120,7 @@ const useConfirmBrc20Transfer = (): { setIsConfirmLoading(false); }; - const handleClickCancel = () => { - navigate(-1); - }; - - const handleClickApplyFee: OnChangeFeeRate = (feeRate: string) => { - if (feeRate) { - setUserInputFeeRate(feeRate); - } - }; - - const handleClickAdvancedSetting = () => { - setShowFeeSettings(true); - }; - - const handleClickCloseFees = () => { - setShowFeeSettings(false); - }; - /* other */ - const fees = [ - { - label: 'Transaction Fee', - value: txFee, - suffix: 'sats', - }, - { - label: 'Inscription Fee', - value: inscriptionFee, - suffix: 'sats', - }, - { - label: 'Total Fee', - value: totalFee, - suffix: 'sats', - fiatValue: getBtcFiatEquivalent(totalFee, BigNumber(btcFiatRate)), - fiatCurrency, - }, - ]; - const errorMessage = errorCode ? t(`CONFIRM_BRC20.ERROR_CODES.${errorCode}`) : error; const recipient: RecipientCardProps = { @@ -270,50 +144,31 @@ const useConfirmBrc20Transfer = (): { }); } - return { - callouts, - errorMessage, - estimateFeesParams, - fees, - handleClickAdvancedSetting, - handleClickApplyFee, - handleClickCancel, - handleClickCloseFees, - handleClickConfirm, - isConfirmLoading, - isFeeLoading, - network, - recipient, - showFeeSettings, - txFee, - showFeeWarning, + const handleGoBack = () => { + navigate(`/send-brc20-one-step?principal=${token.principal}`, { + state: { + amount: estimateFeesParams.amount.toString(), + recipientAddress, + }, + }); }; -}; -function ConfirmBrc20Transaction() { - const { t } = useTranslation('translation'); - const { - callouts, - errorMessage, - estimateFeesParams, - fees, - handleClickAdvancedSetting, - handleClickApplyFee, - handleClickCancel, - handleClickCloseFees, - handleClickConfirm, - isConfirmLoading, - isFeeLoading, - network, - recipient, - showFeeSettings, - txFee, - showFeeWarning, - } = useConfirmBrc20Transfer(); + const handleClickCancel = () => { + navigate(`/coinDashboard/FT?ftKey=${token.principal}&protocol=brc-20`); + }; + + const getFeeForFeeRate = async (feeRate) => { + const estimatedFees = await brc20TransferEstimateFees( + { ...estimateFeesParams, feeRate }, + transactionContext, + ); + const { txFee: newTxFee } = getFeeValuesForBrc20OneStepTransfer(estimatedFees.valueBreakdown); + return newTxFee.toNumber(); + }; return ( <> - + @@ -334,6 +189,9 @@ function ConfirmBrc20Transaction() { ))} )} + + {t('CONFIRM_TRANSACTION.YOU_WILL_SEND')} + + + {t('CONFIRM_TRANSACTION.TRANSACTION_DETAILS')} + - -
- - - {t('CONFIRM_TRANSACTION.EDIT_FEES')} - -
+ + {t('CONFIRM_TRANSACTION.FEES')} + + + + {recommendedFees && ( + setUserInputFeeRate(newFeeRate)} + baseToFiat={(amount) => + getBtcFiatEquivalent(new BigNumber(amount), BigNumber(btcFiatRate)).toString() + } + fiatUnit={fiatCurrency} + getFeeForFeeRate={getFeeForFeeRate} + feeRates={{ + medium: recommendedFees.regular, + high: recommendedFees.priority, + }} + feeRateLimits={recommendedFees.limits} + isLoading={isFeeLoading} + amount={estimateFeesParams.amount} + /> + )} + + + {errorMessage && ( {errorMessage} @@ -361,29 +256,20 @@ function ConfirmBrc20Transaction() {
- - - + {!isInOptions() && }
diff --git a/src/app/screens/confirmBrc20Transaction/recipientCard.tsx b/src/app/screens/confirmBrc20Transaction/recipientCard.tsx index c22af4b22..9ab9a938a 100644 --- a/src/app/screens/confirmBrc20Transaction/recipientCard.tsx +++ b/src/app/screens/confirmBrc20Transaction/recipientCard.tsx @@ -1,11 +1,9 @@ -import btcIcon from '@assets/img/ledger/btc_icon.svg'; -import OutputIcon from '@assets/img/transactions/output.svg'; import FiatAmountText from '@components/fiatAmountText'; import TokenImage from '@components/tokenImage'; -import TransferDetailView from '@components/transferDetailView'; -import useCoinRates from '@hooks/queries/useCoinRates'; import useWalletSelector from '@hooks/useWalletSelector'; -import { getBtcFiatEquivalent, type FungibleToken } from '@secretkeylabs/xverse-core'; +import { type FungibleToken } from '@secretkeylabs/xverse-core'; +import { StyledP } from '@ui-library/common.styled'; +import { getTruncatedAddress } from '@utils/helper'; import { getFtTicker } from '@utils/tokens'; import BigNumber from 'bignumber.js'; import { useTranslation } from 'react-i18next'; @@ -16,10 +14,20 @@ import AmountRow from './amountRow'; const Container = styled.div` display: flex; flex-direction: column; - padding: ${(props) => props.theme.spacing(8)}px; + padding: ${(props) => props.theme.space.m}; background: ${(props) => props.theme.colors.elevation1}; border-radius: ${(props) => props.theme.radius(2)}px; - gap: ${(props) => props.theme.spacing(8)}px; + gap: ${(props) => props.theme.space.m}; +`; + +const RowBlock = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const Title = styled(StyledP)` + color: ${(props) => props.theme.colors.white_400}; `; export type RecipientCardProps = { @@ -32,10 +40,28 @@ export type RecipientCardProps = { function RecipientCard({ address, amountBrc20, amountSats, fungibleToken }: RecipientCardProps) { const { t } = useTranslation('translation'); const { fiatCurrency } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); return ( + + {t('CONFIRM_TRANSACTION.TO')} + {getTruncatedAddress(address, 6)} + + + {t('COMMON.BUNDLE')} + ( + + {value} + + )} + /> + } amountLabel={t('CONFIRM_TRANSACTION.AMOUNT')} @@ -55,29 +81,7 @@ function RecipientCard({ address, amountBrc20, amountSats, fungibleToken }: Reci /> ) } - /> - } - amountLabel={t('CONFIRM_TRANSACTION.AMOUNT')} - amount={ - - } - amountSubText={ - - } - /> - ); diff --git a/src/app/screens/confirmBtcTransaction/index.tsx b/src/app/screens/confirmBtcTransaction/index.tsx deleted file mode 100644 index c592d0fd3..000000000 --- a/src/app/screens/confirmBtcTransaction/index.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import type { ConfirmBtcTransactionState, LedgerTransactionType } from '@common/types/ledger'; -import { MESSAGE_SOURCE, SatsConnectMethods } from '@common/types/message-types'; -import { makeRPCError, makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; -import AlertMessage from '@components/alertMessage'; -import ConfirmBtcTransactionComponent from '@components/confirmBtcTransactionComponent'; -import InfoContainer from '@components/infoContainer'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import { RpcErrorCode, type Return } from '@sats-connect/core'; -import { - AnalyticsEvents, - type BtcTransactionBroadcastResponse, - type Recipient, -} from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import { isLedgerAccount } from '@utils/helper'; -import { saveTimeForNonOrdinalTransferTransaction } from '@utils/localStorage'; -import { trackMixPanel } from '@utils/mixpanel'; -import BigNumber from 'bignumber.js'; -import { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; -import SendLayout from '../../layouts/sendLayout'; - -function ConfirmBtcTransaction() { - const navigate = useNavigate(); - const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' }); - const selectedAccount = useSelectedAccount(); - const { ordinalsAddress, btcAddress } = selectedAccount; - const btcClient = useBtcClient(); - const [signedTx, setSignedTx] = useState(''); - const [showOrdinalsDetectedAlert, setShowOrdinalsDetectedAlert] = useState(false); - const location = useLocation(); - const { refetch } = useBtcWalletData(); - const { ordinals: ordinalsInBtc } = useOrdinalsByAddress(btcAddress); - const { - fee, - amount, - signedTxHex, - recipient, - recipientAddress, - isRestoreFundFlow, - unspentUtxos, - btcSendBrowserTx, - requestToken, - tabId, - isBrc20TokenFlow, - feePerVByte, - requestId, - } = location.state; - if (typeof fee !== 'string' && !BigNumber.isBigNumber(fee)) { - Object.setPrototypeOf(fee, BigNumber.prototype); - } - - const [currentFee, setCurrentFee] = useState(fee); - const [currentFeeRate, setCurrentFeeRate] = useState(feePerVByte); - - const { - isLoading, - error: txError, - data: btcTxBroadcastData, - mutate, - } = useMutation({ - mutationFn: async ({ txToBeBroadcasted }) => btcClient.sendRawTransaction(txToBeBroadcasted), - }); - - const { - error: errorBtcOrdinalTransaction, - data: btcOrdinalTxBroadcastData, - mutate: broadcastOrdinalTransaction, - } = useMutation({ - mutationFn: async ({ ordinalTxToBeBroadcasted }) => - btcClient.sendRawTransaction(ordinalTxToBeBroadcasted), - }); - - const onClick = () => { - navigate('/restore-ordinals', { - state: { isRestoreFundFlow: true }, - }); - }; - - useResetUserFlow('/confirm-btc-tx'); - - const onContinueButtonClick = () => { - mutate({ txToBeBroadcasted: signedTx }); - }; - - const handleBrowserTx = useCallback( - (broadCastResult: BtcTransactionBroadcastResponse) => { - if (requestToken) { - const btcSendMessage = { - source: MESSAGE_SOURCE, - method: SatsConnectMethods.sendBtcResponse, - payload: { - sendBtcRequest: requestToken, - sendBtcResponse: broadCastResult.tx.hash, - }, - }; - chrome.tabs.sendMessage(+tabId, btcSendMessage); - } else { - const result: Return<'sendTransfer'> = { - txid: broadCastResult.tx.hash, - }; - const response = makeRpcSuccessResponse(requestId, result); - sendRpcResponse(+tabId, response); - } - window.close(); - }, - [btcTxBroadcastData, requestToken, tabId], - ); - - useEffect(() => { - if (!btcTxBroadcastData) return; - if (btcSendBrowserTx) { - handleBrowserTx(btcTxBroadcastData); - } else { - navigate('/tx-status', { - state: { - txid: btcTxBroadcastData.tx.hash, - currency: 'BTC', - error: '', - isBrc20TokenFlow, - }, - }); - setTimeout(() => { - refetch(); - }, 1000); - } - }, [btcTxBroadcastData]); - - useEffect(() => { - if (btcOrdinalTxBroadcastData) { - saveTimeForNonOrdinalTransferTransaction(ordinalsAddress).then(() => { - navigate('/tx-status', { - state: { - txid: btcOrdinalTxBroadcastData.tx.hash, - currency: 'BTC', - isOrdinal: true, - error: '', - }, - }); - setTimeout(() => { - refetch(); - }, 1000); - }); - } - }, [btcOrdinalTxBroadcastData]); - - useEffect(() => { - if (txError) { - if (btcSendBrowserTx) { - const errorResponse = makeRPCError(requestId, { - code: RpcErrorCode.INTERNAL_ERROR, - message: txError.toString(), - }); - sendRpcResponse(+tabId, errorResponse); - } - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: txError.toString(), - browserTx: btcSendBrowserTx, - }, - }); - } - }, [txError]); - - const handleOnConfirmClick = (txHex: string) => { - if (isRestoreFundFlow) { - broadcastOrdinalTransaction({ ordinalTxToBeBroadcasted: txHex }); - return; - } - - if (ordinalsInBtc && ordinalsInBtc.length > 0) { - setSignedTx(txHex); - setShowOrdinalsDetectedAlert(true); - return; - } - - if (isLedgerAccount(selectedAccount)) { - const txType: LedgerTransactionType = 'BTC'; - const state: ConfirmBtcTransactionState = { - recipients: recipient, - type: txType, - feeRateInput: currentFeeRate, - fee: currentFee, - }; - - navigate('/confirm-ledger-tx', { state }); - return; - } - - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: 'bitcoin', - action: 'transfer', - wallet_type: selectedAccount?.accountType || 'software', - }); - - mutate({ txToBeBroadcasted: txHex }); - }; - - const goBackToScreen = () => { - if (isRestoreFundFlow || isBrc20TokenFlow) { - navigate(-1); - } else if (btcSendBrowserTx) { - if (requestToken) { - const addressMessage = { - source: MESSAGE_SOURCE, - method: SatsConnectMethods.sendBtcResponse, - payload: { - sendBtcRequest: requestToken, - sendBtcResponse: 'cancel', - }, - }; - chrome.tabs.sendMessage(+tabId, addressMessage); - } else { - const cancelError = makeRPCError(requestId as string, { - code: RpcErrorCode.USER_REJECTION, - message: `User rejected request to send transfer`, - }); - sendRpcResponse(+tabId, cancelError); - } - window.close(); - } else { - navigate('/send-btc', { - state: { - amount, - recipientAddress, - }, - }); - } - }; - const hideBackButton = location.key === 'default'; - - const onClosePress = () => { - setShowOrdinalsDetectedAlert(false); - }; - - return ( - - {showOrdinalsDetectedAlert && ( - - )} - - {ordinalsInBtc && ordinalsInBtc.length > 0 && ( - - )} - - - ); -} - -export default ConfirmBtcTransaction; diff --git a/src/app/screens/confirmInscriptionRequest/index.tsx b/src/app/screens/confirmInscriptionRequest/index.tsx deleted file mode 100644 index b2f87f6d0..000000000 --- a/src/app/screens/confirmInscriptionRequest/index.tsx +++ /dev/null @@ -1,467 +0,0 @@ -import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; -import OrdinalsIcon from '@assets/img/nftDashboard/white_ordinals_icon.svg'; -import type { ConfirmBrc20TransactionState, LedgerTransactionType } from '@common/types/ledger'; -import AlertMessage from '@components/alertMessage'; -import ActionButton from '@components/button'; -import InfoContainer from '@components/infoContainer'; -import BottomBar from '@components/tabBar'; -import TopRow from '@components/topRow'; -import TransactionDetailComponent from '@components/transactionDetailComponent'; -import TransactionSettingAlert from '@components/transactionSetting'; -import TransferFeeView from '@components/transferFeeView'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useCoinRates from '@hooks/queries/useCoinRates'; -import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useSeedVault from '@hooks/useSeedVault'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import Brc20Tile from '@screens/ordinals/brc20Tile'; -import CollapsableContainer from '@screens/signatureRequest/collapsableContainer'; -import { - AnalyticsEvents, - ResponseError, - getBtcFiatEquivalent, - parseOrdinalTextContentData, - satsToBtc, - signBtcTransaction, - type BtcTransactionBroadcastResponse, - type Recipient, - type SignedBtcTx, -} from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import { isLedgerAccount } from '@utils/helper'; -import { trackMixPanel } from '@utils/mixpanel'; -import axios from 'axios'; -import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NumericFormat } from 'react-number-format'; -import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; - -const OuterContainer = styled.div` - display: flex; - flex-direction: column; - overflow-y: auto; - height: 100%; - padding-left: 5%; - padding-right: 5%; - &::-webkit-scrollbar { - display: none; - } -`; - -const Brc20TileContainer = styled.div({ - width: 112, - height: 112, - alignSelf: 'center', - display: 'flex', - alignItems: 'center', -}); - -const TopContainer = styled.div({ - marginBottom: 32, -}); - -const TransferOrdinalContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const ReviewTransactionText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(16), - textAlign: 'center', -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - marginBottom: props.theme.spacing(5), -})); - -const TransparentButtonContainer = styled.div((props) => ({ - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), - width: '100%', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - borderRadius: props.theme.radius(1), - backgroundColor: 'transparent', - width: '100%', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(8), -})); - -const ButtonText = styled.div((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const DetailRow = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginTop: props.theme.spacing(4), - marginBottom: props.theme.spacing(8), -})); - -const TitleText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_200, -})); - -const ValueText = styled.h1((props) => ({ - ...props.theme.body_medium_m, - color: props.theme.colors.white_0, -})); - -const Icon = styled.img((props) => ({ - marginRight: props.theme.spacing(4), - width: 32, - height: 32, - borderRadius: 30, -})); - -const BottomBarContainer = styled.h1((props) => ({ - marginTop: props.theme.spacing(5), -})); - -function ConfirmInscriptionRequest() { - const navigate = useNavigate(); - const location = useLocation(); - const { t } = useTranslation('translation'); - const { - fee, - amount, - signedTxHex, - recipient, - isRestoreFundFlow, - unspentUtxos, - brcContent, - feePerVByte, - } = location.state; - const selectedAccount = useSelectedAccount(); - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { getSeed } = useSeedVault(); - const btcClient = useBtcClient(); - const [signedTx, setSignedTx] = useState(''); - const [textContent, setTextContent] = useState(''); - const [showOrdinalsDetectedAlert, setShowOrdinalsDetectedAlert] = useState(false); - const [currentFee, setCurrentFee] = useState(fee); - const [total, setTotal] = useState(new BigNumber(0)); - const [currentFeeRate, setCurrentFeeRate] = useState(feePerVByte); - const [showFeeSettings, setShowFeeSettings] = useState(false); - const { refetch } = useBtcWalletData(); - const { ordinals: ordinalsInBtc } = useOrdinalsByAddress(selectedAccount.btcAddress); - - const content = useMemo(() => textContent && JSON.parse(textContent), [textContent]); - - useResetUserFlow('/confirm-inscription-request'); - - useEffect(() => { - axios - .get(brcContent, { - timeout: 30000, - transformResponse: [(data) => parseOrdinalTextContentData(data)], - }) - .then((response) => setTextContent(response!.data)) - .catch((error) => ''); - return () => { - setTextContent(''); - }; - }, [brcContent]); - - const { - isLoading, - error: txError, - data: btcTxBroadcastData, - mutate, - } = useMutation({ - mutationFn: async ({ txToBeBroadcasted }) => btcClient.sendRawTransaction(txToBeBroadcasted), - }); - - const { - isLoading: loadingFee, - data, - error: txFeeError, - mutate: mutateTxFee, - } = useMutation< - SignedBtcTx, - ResponseError, - { - recipients: Recipient[]; - txFee: string; - seedPhrase: string; - } - >({ - mutationFn: async ({ recipients, txFee, seedPhrase }) => - signBtcTransaction( - recipients, - selectedAccount.btcAddress, - selectedAccount?.id ?? 0, - seedPhrase, - btcClient, - network.type, - new BigNumber(txFee), - ), - }); - - const onContinueButtonClick = () => { - mutate({ txToBeBroadcasted: signedTx }); - }; - - const onClick = () => { - navigate('/restore-ordinals'); - }; - - useEffect(() => { - if (data) { - setCurrentFee(data.fee); - setSignedTx(data.signedTx); - setShowFeeSettings(false); - } - }, [data]); - - useEffect(() => { - const totalAmount: BigNumber = new BigNumber(0); - let sum: BigNumber = new BigNumber(0); - if (recipient) { - recipient.map((r) => { - sum = totalAmount.plus(r.amountSats); - return sum; - }); - sum = sum?.plus(currentFee); - } - setTotal(sum); - }, [recipient, currentFee]); - - useEffect(() => { - if (btcTxBroadcastData) { - navigate('/tx-status', { - state: { - txid: btcTxBroadcastData.tx.hash, - currency: 'BTC', - error: '', - isBrc20TokenFlow: true, - }, - }); - setTimeout(() => { - refetch(); - }, 1000); - } - }, [btcTxBroadcastData]); - - useEffect(() => { - if (txError) { - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: txError.toString(), - }, - }); - } - }, [txError]); - - const handleOnConfirmClick = () => { - if (ordinalsInBtc && ordinalsInBtc.length > 0) { - setSignedTx(signedTxHex); - setShowOrdinalsDetectedAlert(true); - return; - } - - if (isLedgerAccount(selectedAccount)) { - const txType: LedgerTransactionType = 'BRC-20'; - const state: ConfirmBrc20TransactionState = { - amount: new BigNumber(amount), - recipients: recipient, - type: txType, - fee, - }; - - navigate('/confirm-ledger-tx', { state }); - return; - } - - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: 'brc20', - action: 'transfer', - wallet_type: selectedAccount?.accountType || 'software', - }); - - mutate({ txToBeBroadcasted: signedTxHex }); - }; - - const goBackToScreen = () => { - navigate(-1); - }; - - const onClosePress = () => { - setShowOrdinalsDetectedAlert(false); - }; - - const onAdvancedSettingClick = () => { - setShowFeeSettings(true); - }; - - const onApplyClick = async ({ - fee: modifiedFee, - feeRate, - }: { - fee: string; - feeRate?: string; - nonce?: string; - }) => { - const seedPhrase = await getSeed(); - setCurrentFeeRate(new BigNumber(feeRate!)); - mutateTxFee({ recipients: recipient, txFee: modifiedFee, seedPhrase }); - }; - - const closeTransactionSettingAlert = () => { - setShowFeeSettings(false); - }; - - const getAmountString = (amountTotal: BigNumber, currency: string) => ( - - ); - return ( - <> - {showOrdinalsDetectedAlert && ( - - )} - - - - - {ordinalsInBtc && ordinalsInBtc.length > 0 && ( - - )} - - {textContent && ( - - {/* TODO fix type error */} - - - )} - Inscribe Transfer Ordinal - - - - - Ordinal - - BRC-20 Transfer - - - Transfer - {getAmountString(new BigNumber(content.amt), content.tick)} - - - - - - - - - - - - - - - - - - - - ); -} - -export default ConfirmInscriptionRequest; diff --git a/src/app/screens/confirmOrdinalTransaction/index.tsx b/src/app/screens/confirmOrdinalTransaction/index.tsx deleted file mode 100644 index b49ed4af4..000000000 --- a/src/app/screens/confirmOrdinalTransaction/index.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import type { ConfirmOrdinalsTransactionState, LedgerTransactionType } from '@common/types/ledger'; -import ConfirmBtcTransactionComponent from '@components/confirmBtcTransactionComponent'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useAddressInscription from '@hooks/queries/ordinals/useAddressInscription'; -import { useGetUtxoOrdinalBundle } from '@hooks/queries/ordinals/useAddressRareSats'; -import useBtcWalletData from '@hooks/queries/useBtcWalletData'; -import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; -import { useResetUserFlow } from '@hooks/useResetUserFlow'; -import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import OrdinalImage from '@screens/ordinals/ordinalImage'; -import { AnalyticsEvents, type BtcTransactionBroadcastResponse } from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; -import { isLedgerAccount } from '@utils/helper'; -import { trackMixPanel } from '@utils/mixpanel'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import SendLayout from '../../layouts/sendLayout'; - -const Container = styled.div({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', -}); - -const NftContainer = styled.div((props) => ({ - maxWidth: 150, - maxHeight: 150, - width: '60%', - display: 'flex', - aspectRatio: 1, - justifyContent: 'center', - alignItems: 'center', - borderRadius: 8, - padding: props.theme.spacing(5), - marginBottom: props.theme.spacing(6), -})); - -function ConfirmOrdinalTransaction() { - const selectedAccount = useSelectedAccount(); - const { hasActivatedRareSatsKey } = useWalletSelector(); - const navigate = useNavigate(); - const btcClient = useBtcClient(); - const [recipientAddress, setRecipientAddress] = useState(''); - const location = useLocation(); - - // TODO tim: refactor to not use location.state. - const { feePerVByte, signedTxHex, ordinalUtxo, isRareSat } = location.state; - // this hack is necessary because the browser back/forward buttons - // serialize BigNumber objects into plain objects - let { fee } = location.state; - if (!BigNumber.isBigNumber(fee)) { - fee = BigNumber(fee); - } - const { id } = useParams(); - const { data: selectedOrdinal } = useAddressInscription(id!); - const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); - const { refetch } = useBtcWalletData(); - const [currentFee, setCurrentFee] = useState(fee); - const [currentFeeRate, setCurrentFeeRate] = useState(feePerVByte); - - const { - isLoading, - error: txError, - data: btcTxBroadcastData, - mutate, - } = useMutation({ - mutationFn: async ({ signedTx }) => btcClient.sendRawTransaction(signedTx), - }); - - useEffect(() => { - setRecipientAddress(location.state.recipientAddress); - }, [location]); - - useEffect(() => { - if (btcTxBroadcastData) { - setSelectedSatBundleDetails(null); - navigate('/tx-status', { - state: { - txid: btcTxBroadcastData.tx.hash, - currency: 'BTC', - error: '', - isRareSat, - isOrdinal: !isRareSat, - }, - }); - setTimeout(() => { - refetch(); - }, 1000); - } - }, [btcTxBroadcastData]); - - useEffect(() => { - if (txError) { - setSelectedSatBundleDetails(null); - navigate('/tx-status', { - state: { - txid: '', - currency: 'BTC', - error: txError.toString(), - isOrdinal: true, - }, - }); - } - }, [txError]); - - const { - bundle: ordinalBundle, - isPartOfABundle, - ordinalSatributes, - } = useGetUtxoOrdinalBundle( - selectedOrdinal?.output, - hasActivatedRareSatsKey, - selectedOrdinal?.number, - ); - - const holdsRareSats = ordinalSatributes?.length > 0; - - const handleConfirmClick = (txHex: string) => { - if (isLedgerAccount(selectedAccount)) { - const txType: LedgerTransactionType = 'ORDINALS'; - const state: ConfirmOrdinalsTransactionState = { - recipients: [{ address: recipientAddress, amountSats: new BigNumber(ordinalUtxo.value) }], - type: txType, - ordinalUtxo, - feeRateInput: currentFeeRate, - fee: currentFee, - }; - - navigate('/confirm-ledger-tx', { state }); - return; - } - - trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: isRareSat ? 'rare-sats' : 'ordinals', - action: 'transfer', - wallet_type: selectedAccount?.accountType || 'software', - }); - - mutate({ signedTx: txHex }); - }; - - useResetUserFlow('/confirm-ordinal-tx'); - const handleBackButtonClick = () => { - navigate(-1); - }; - const hideBackButton = location.key === 'default'; - - return ( - - - {!isRareSat && selectedOrdinal && ( - - - - - - )} - - - ); -} -export default ConfirmOrdinalTransaction; diff --git a/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts b/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts index 3ab294410..e83224b77 100644 --- a/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts +++ b/src/app/screens/connect/btcSelectAddressScreen/useBtcAddressRequest.ts @@ -143,6 +143,7 @@ export default function useRequestHelper(): UseRequestHelperReturn { { const params = new URLSearchParams(search); const token = params.get('addressRequest') ?? ''; + const origin = params.get('origin') ?? ''; let legacyRequestNetworkType; let request: GetAddressOptions | undefined; if (token) { diff --git a/src/app/screens/connectionRequest/index.tsx b/src/app/screens/connectionRequest/index.tsx index e35f09e35..06646c88a 100644 --- a/src/app/screens/connectionRequest/index.tsx +++ b/src/app/screens/connectionRequest/index.tsx @@ -93,6 +93,8 @@ function ConnectionRequestInner({ data, context }: ConnectionRequestInnerProps) + + diff --git a/src/app/screens/home/index.tsx b/src/app/screens/home/index.tsx index fd3f82e83..4e4372b71 100644 --- a/src/app/screens/home/index.tsx +++ b/src/app/screens/home/index.tsx @@ -147,26 +147,29 @@ function Home() { { - toast.remove(toastId); - - // set the visibility back to true - const payload = { - principal: spamToken.principal, - isEnabled: true, - }; - - if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setRunesManageTokensAction(payload)); - } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setSip10ManageTokensAction(payload)); - } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) { - dispatch(setBrc20ManageTokensAction(payload)); - } - - removeFromSpamTokens(spamToken.principal); - dispatch(setSpamTokenAction(null)); + dismissToast={() => toast.remove(toastId)} + action={{ + text: t('UNDO'), + onClick: () => { + toast.remove(toastId); + + // set the visibility back to true + const payload = { + principal: spamToken.principal, + isEnabled: true, + }; + + if (fullRunesCoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setRunesManageTokensAction(payload)); + } else if (fullSip10CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setSip10ManageTokensAction(payload)); + } else if (fullBrc20CoinsList?.find((ft) => ft.principal === spamToken.principal)) { + dispatch(setBrc20ManageTokensAction(payload)); + } + + removeFromSpamTokens(spamToken.principal); + dispatch(setSpamTokenAction(null)); + }, }} />, ); diff --git a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx index d741f904b..5b608790c 100644 --- a/src/app/screens/ledger/confirmLedgerTransaction/index.tsx +++ b/src/app/screens/ledger/confirmLedgerTransaction/index.tsx @@ -38,6 +38,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; +import { makeRpcSuccessResponse, sendRpcResponse } from '@common/utils/rpc/helpers'; import useSelectedAccount from '@hooks/useSelectedAccount'; import { ConfirmTxIconBig, @@ -93,6 +94,8 @@ function ConfirmLedgerTransaction(): JSX.Element { ordinalUtxo, feeRateInput, fee, + tabId, + tabMessageId: messageId, }: { amount: BigNumber; recipients: Recipient[] | StacksRecipient[]; @@ -102,6 +105,8 @@ function ConfirmLedgerTransaction(): JSX.Element { feeRateInput?: string; fee?: BigNumber; messageId?: string; + tabId?: number; + tabMessageId?: string; } = location.state; const transition = useTransition(currentStep, DEFAULT_TRANSITION_OPTIONS); @@ -129,6 +134,10 @@ function ConfirmLedgerTransaction(): JSX.Element { const transactionId = await btcClient.sendRawTransaction(txHex || taprootSignedValue); setTxId(transactionId.tx.hash); setCurrentStep(Steps.TransactionConfirmed); + if (tabId) { + const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash }); + sendRpcResponse(tabId, response); + } } catch (err) { console.error(err); setIsTxRejected(true); @@ -153,6 +162,10 @@ function ConfirmLedgerTransaction(): JSX.Element { const transactionId = await btcClient.sendRawTransaction(result); setTxId(transactionId.tx.hash); setCurrentStep(Steps.TransactionConfirmed); + if (tabId) { + const response = makeRpcSuccessResponse(messageId, { txid: transactionId.tx.hash }); + sendRpcResponse(tabId, response); + } } catch (err) { console.error(err); setIsTxRejected(true); diff --git a/src/app/screens/listRune/listRuneItem.tsx b/src/app/screens/listRune/listRuneItem.tsx index 5a9bf4c15..c846128f1 100644 --- a/src/app/screens/listRune/listRuneItem.tsx +++ b/src/app/screens/listRune/listRuneItem.tsx @@ -109,7 +109,7 @@ function ListRuneItem({ ( diff --git a/src/app/screens/listRune/setRunePriceItem.tsx b/src/app/screens/listRune/setRunePriceItem.tsx index cfb2c5b10..34e94c65f 100644 --- a/src/app/screens/listRune/setRunePriceItem.tsx +++ b/src/app/screens/listRune/setRunePriceItem.tsx @@ -78,7 +78,7 @@ function SetRunePriceItem({ floorPriceSats, handleShowCustomPriceModal, }: Props) { - const { t } = useTranslation('translation', { keyPrefix: 'LIST_RUNE_SCREEN' }); + const { t } = useTranslation('translation'); const { btcFiatRate } = useCoinRates(); const { fiatCurrency } = useWalletSelector(); @@ -100,8 +100,8 @@ function SetRunePriceItem({ ( @@ -145,7 +145,7 @@ function SetRunePriceItem({ )} /> diff --git a/src/app/screens/ordinalDetail/index.styled.ts b/src/app/screens/ordinalDetail/index.styled.ts new file mode 100644 index 000000000..b9cb58569 --- /dev/null +++ b/src/app/screens/ordinalDetail/index.styled.ts @@ -0,0 +1,337 @@ +import { BetterBarLoader } from '@components/barLoader'; +import Separator from '@components/separator'; +import WebGalleryButton from '@components/webGalleryButton'; +import Callout from '@ui-library/callout'; +import { StyledP } from '@ui-library/common.styled'; +import { Tooltip } from 'react-tooltip'; +import styled from 'styled-components'; + +interface DetailSectionProps { + isGallery: boolean; +} + +export const GalleryScrollContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + flex: 1, + alignItems: 'center', +})); + +export const GalleryContainer = styled.div({ + marginLeft: 'auto', + marginRight: 'auto', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + maxWidth: 1224, +}); + +export const BackButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + width: 820, + marginTop: props.theme.spacing(40), +})); + +export const ButtonContainer = styled.div((props) => ({ + display: 'flex', + position: 'relative', + flexDirection: 'row', + maxWidth: 400, + columnGap: props.theme.spacing(8), + marginBottom: props.theme.spacing(10.5), +})); + +export const ExtensionContainer = styled.div((props) => ({ + ...props.theme.scrollbar, + display: 'flex', + flexDirection: 'column', + marginTop: props.theme.spacing(4), + alignItems: 'center', + flex: 1, + paddingLeft: props.theme.spacing(4), + paddingRight: props.theme.spacing(4), +})); + +export const OrdinalsContainer = styled.div((props) => ({ + width: 376.5, + height: 376.5, + display: 'flex', + aspectRatio: '1', + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + borderRadius: 8, + marginBottom: props.theme.spacing(12), +})); + +export const ExtensionOrdinalsContainer = styled.div((props) => ({ + maxHeight: 136, + width: 136, + display: 'flex', + aspectRatio: '1', + justifyContent: 'center', + alignItems: 'center', + borderRadius: props.theme.radius(1), + marginTop: props.theme.spacing(12), + marginBottom: props.theme.space.m, +})); + +export const OrdinalTitleText = styled.h1((props) => ({ + ...props.theme.typography.headline_m, + color: props.theme.colors.white_0, + marginTop: props.theme.spacing(1), + textAlign: 'center', +})); + +export const OrdinalGalleryTitleText = styled.h1((props) => ({ + ...props.theme.typography.headline_l, + color: props.theme.colors.white_0, +})); + +export const DescriptionText = styled.h1((props) => ({ + ...props.theme.typography.headline_l, + color: props.theme.colors.white_0, + fontSize: 24, +})); + +export const BottomBarContainer = styled.div({ + marginTop: 'auto', +}); + +export const RowContainer = styled.div<{ + withGap?: boolean; +}>((props) => ({ + display: 'flex', + alignItems: 'flex-start', + marginTop: props.theme.spacing(8), + marginBottom: props.theme.spacing(12), + flexDirection: 'row', + columnGap: props.withGap ? props.theme.spacing(20) : 0, +})); + +export const ColumnContainer = styled.div({ + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + width: '100%', +}); + +export const OrdinalDetailsContainer = styled.div((props) => ({ + display: 'flex', + alignItems: 'flex-start', + flexDirection: 'column', + wordBreak: 'break-all', + whiteSpace: 'pre-wrap', + width: props.isGallery ? 400 : '100%', + marginTop: props.theme.spacing(8), +})); + +export const Row = styled.div({ + display: 'flex', + justifyContent: 'space-between', + flexDirection: 'row', + width: '100%', +}); + +export const DescriptionContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + marginBottom: props.theme.spacing(30), +})); + +export const StyledWebGalleryButton = styled(WebGalleryButton)` + margintop: ${(props) => props.theme.space.s}; +`; + +export const ViewInExplorerButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'transparent', + width: props.isGallery ? 392 : 328, + height: 44, + padding: 12, + borderRadius: 12, + marginTop: props.theme.spacing(16), + marginBottom: props.theme.spacing(18), + border: `1px solid ${props.theme.colors.white_800}`, +})); + +export const ButtonText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_400, +})); + +export const ButtonHiglightedText = styled.h1((props) => ({ + ...props.theme.typography.body_medium_m, + color: props.theme.colors.white_0, + marginLeft: props.theme.spacing(2), + marginRight: props.theme.spacing(2), +})); + +export const StyledTooltip = styled(Tooltip)` + &&& { + font-size: 12px; + background-color: #ffffff; + color: #12151e; + border-radius: 8px; + padding: 7px; + } +`; + +export const ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +export const Button = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.spacing(12), +})); + +export const AssetDeatilButtonText = styled.div((props) => ({ + ...props.theme.typography.body_s, + fontWeight: 400, + fontSize: 14, + color: props.theme.colors.white_0, + textAlign: 'center', +})); + +export const CollectibleText = styled.h1((props) => ({ + ...props.theme.typography.body_bold_m, + color: props.theme.colors.white_400, + textAlign: 'center', +})); + +export const GalleryCollectibleText = styled.h1((props) => ({ + ...props.theme.typography.body_bold_l, + color: props.theme.colors.white_400, +})); + +export const GalleryButtonContainer = styled.div` + width: 190px; + border-radius: 12px; +`; + +export const RowButtonContainer = styled.div((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + columnGap: props.theme.spacing(11), + marginBottom: props.theme.space.l, + marginTop: props.theme.space.m, + width: '100%', +})); + +export const Divider = styled.div((props) => ({ + width: '100%', + borderBottom: `1px solid ${props.theme.colors.elevation3}`, +})); + +export const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: !props.isGallery ? 'row' : 'column', + justifyContent: 'space-between', + columnGap: props.theme.space.m, + width: '100%', +})); + +export const ExtensionLoaderContainer = styled.div({ + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + alignItems: 'center', +}); + +export const GalleryLoaderContainer = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +export const StyledBarLoader = styled(BetterBarLoader)<{ + withMarginBottom?: boolean; +}>((props) => ({ + padding: 0, + borderRadius: props.theme.radius(1), + marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, +})); + +export const StyledSeparator = styled(Separator)` + width: 100%; +`; + +export const TitleLoader = styled.div` + display: flex; + flex-direction: column; +`; + +export const ActionButtonsLoader = styled.div((props) => ({ + display: 'flex', + justifyContent: 'center', + columnGap: props.theme.spacing(11), +})); + +export const ActionButtonLoader = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + rowGap: props.theme.spacing(4), +})); + +export const InfoContainer = styled.div((props) => ({ + width: '100%', + display: 'flex', + justifyContent: 'space-between', + padding: `0 ${props.theme.spacing(8)}px`, +})); + +export const RareSatsBundleCallout = styled(Callout)((props) => ({ + width: props.isGallery ? 400 : '100%', + marginBottom: props.isGallery ? 0 : props.theme.space.l, + marginTop: props.isGallery ? props.theme.space.xs : 0, +})); + +export const SatributesIconsContainer = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + marginTop: props.isGallery ? props.theme.space.m : 0, +})); + +export const SatributesBadgeContainer = styled.div((props) => ({ + marginTop: props.isGallery ? 0 : props.theme.space.m, +})); +export const SatributesBadges = styled.div((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + flexWrap: 'wrap', + maxWidth: props.isGallery ? 400 : '100%', + marginTop: props.theme.space.s, +})); +export const Badge = styled.div<{ backgroundColor?: string; isLastItem: boolean }>((props) => ({ + display: 'inline-flex', + flexDirection: 'row', + alignItems: 'center', + backgroundColor: props.backgroundColor, + padding: `${props.theme.space.s} ${props.theme.space.s}`, + borderRadius: props.theme.radius(2), + border: `1px solid ${props.theme.colors.elevation3}`, + marginRight: props.isLastItem ? 0 : props.theme.space.xs, + marginBottom: props.theme.space.xs, +})); +export const SatributeBadgeLabel = styled(StyledP)` + margin-left: ${(props) => props.theme.space.xs}; +`; +export const DataItemsContainer = styled.div` + margin-top: ${(props) => props.theme.space.l}; +`; diff --git a/src/app/screens/ordinalDetail/index.tsx b/src/app/screens/ordinalDetail/index.tsx index 926640b9e..ea8bd71a7 100644 --- a/src/app/screens/ordinalDetail/index.tsx +++ b/src/app/screens/ordinalDetail/index.tsx @@ -1,393 +1,69 @@ import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; import AlertMessage from '@components/alertMessage'; -import { BetterBarLoader } from '@components/barLoader'; import ActionButton from '@components/button'; import CollectibleDetailTile from '@components/collectibleDetailTile'; import RareSatIcon from '@components/rareSatIcon/rareSatIcon'; -import Separator from '@components/separator'; import SquareButton from '@components/squareButton'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import WebGalleryButton from '@components/webGalleryButton'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import { ArrowUp, Share } from '@phosphor-icons/react'; import OrdinalImage from '@screens/ordinals/ordinalImage'; -import Callout from '@ui-library/callout'; import { StyledP } from '@ui-library/common.styled'; import { EMPTY_LABEL } from '@utils/constants'; import { getRareSatsColorsByRareSatsType, getRareSatsLabelByType } from '@utils/rareSats'; import { useTranslation } from 'react-i18next'; -import { Tooltip } from 'react-tooltip'; -import styled from 'styled-components'; +import { + ActionButtonLoader, + ActionButtonsLoader, + AssetDeatilButtonText, + BackButtonContainer, + Badge, + BottomBarContainer, + Button, + ButtonContainer, + ButtonHiglightedText, + ButtonImage, + ButtonText, + CollectibleText, + ColumnContainer, + DataItemsContainer, + DescriptionContainer, + DescriptionText, + DetailSection, + Divider, + ExtensionContainer, + ExtensionLoaderContainer, + ExtensionOrdinalsContainer, + GalleryButtonContainer, + GalleryCollectibleText, + GalleryContainer, + GalleryLoaderContainer, + GalleryScrollContainer, + InfoContainer, + OrdinalDetailsContainer, + OrdinalGalleryTitleText, + OrdinalsContainer, + OrdinalTitleText, + RareSatsBundleCallout, + Row, + RowButtonContainer, + RowContainer, + SatributeBadgeLabel, + SatributesBadgeContainer, + SatributesBadges, + SatributesIconsContainer, + StyledBarLoader, + StyledSeparator, + StyledTooltip, + StyledWebGalleryButton, + TitleLoader, + ViewInExplorerButton, +} from './index.styled'; import OrdinalAttributeComponent from './ordinalAttributeComponent'; import useOrdinalDetail from './useOrdinalDetail'; -interface DetailSectionProps { - isGallery: boolean; -} - -const GalleryScrollContainer = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - flex: 1, - alignItems: 'center', -})); - -const GalleryContainer = styled.div({ - marginLeft: 'auto', - marginRight: 'auto', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', - width: '100%', - maxWidth: 1224, -}); - -const BackButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - width: 820, - marginTop: props.theme.spacing(40), -})); - -const ButtonContainer = styled.div((props) => ({ - display: 'flex', - position: 'relative', - flexDirection: 'row', - maxWidth: 400, - columnGap: props.theme.spacing(8), - marginBottom: props.theme.spacing(10.5), -})); - -const ExtensionContainer = styled.div((props) => ({ - ...props.theme.scrollbar, - display: 'flex', - flexDirection: 'column', - marginTop: props.theme.spacing(4), - alignItems: 'center', - flex: 1, - paddingLeft: props.theme.spacing(4), - paddingRight: props.theme.spacing(4), -})); - -const OrdinalsContainer = styled.div((props) => ({ - width: 376.5, - height: 376.5, - display: 'flex', - aspectRatio: '1', - flexDirection: 'column', - justifyContent: 'flex-start', - alignItems: 'flex-start', - borderRadius: 8, - marginBottom: props.theme.spacing(12), -})); - -const ExtensionOrdinalsContainer = styled.div((props) => ({ - maxHeight: 136, - width: 136, - display: 'flex', - aspectRatio: '1', - justifyContent: 'center', - alignItems: 'center', - borderRadius: props.theme.radius(1), - marginTop: props.theme.spacing(12), - marginBottom: props.theme.space.m, -})); - -const OrdinalTitleText = styled.h1((props) => ({ - ...props.theme.typography.headline_m, - color: props.theme.colors.white_0, - marginTop: props.theme.spacing(1), - textAlign: 'center', -})); - -const OrdinalGalleryTitleText = styled.h1((props) => ({ - ...props.theme.typography.headline_l, - color: props.theme.colors.white_0, -})); - -const DescriptionText = styled.h1((props) => ({ - ...props.theme.typography.headline_l, - color: props.theme.colors.white_0, - fontSize: 24, -})); - -const NftOwnedByText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, -})); - -const OwnerAddressText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - marginLeft: props.theme.spacing(3), -})); - -const BottomBarContainer = styled.div({ - marginTop: 'auto', -}); - -const RowContainer = styled.div<{ - withGap?: boolean; -}>((props) => ({ - display: 'flex', - alignItems: 'flex-start', - marginTop: props.theme.spacing(8), - marginBottom: props.theme.spacing(12), - flexDirection: 'row', - columnGap: props.withGap ? props.theme.spacing(20) : 0, -})); - -const ColumnContainer = styled.div({ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - width: '100%', -}); - -const OrdinalDetailsContainer = styled.div((props) => ({ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - wordBreak: 'break-all', - whiteSpace: 'pre-wrap', - width: props.isGallery ? 400 : '100%', - marginTop: props.theme.spacing(8), -})); - -const Row = styled.div({ - display: 'flex', - justifyContent: 'space-between', - flexDirection: 'row', - width: '100%', -}); - -const DescriptionContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - marginBottom: props.theme.spacing(30), -})); - -const StyledWebGalleryButton = styled(WebGalleryButton)` - margintop: ${(props) => props.theme.space.s}; -`; - -const ViewInExplorerButton = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'transparent', - width: props.isGallery ? 392 : 328, - height: 44, - padding: 12, - borderRadius: 12, - marginTop: props.theme.spacing(16), - marginBottom: props.theme.spacing(18), - border: `1px solid ${props.theme.colors.white_800}`, -})); - -const ButtonText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_400, -})); - -const ButtonHiglightedText = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_0, - marginLeft: props.theme.spacing(2), - marginRight: props.theme.spacing(2), -})); - -const StyledTooltip = styled(Tooltip)` - &&& { - font-size: 12px; - background-color: #ffffff; - color: #12151e; - border-radius: 8px; - padding: 7px; - } -`; - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDeatilButtonText = styled.div((props) => ({ - ...props.theme.typography.body_s, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white_0, - textAlign: 'center', -})); - -const ButtonIcon = styled.img({ - width: 12, - height: 12, -}); - -const OrdinalsTag = styled.div({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - marginLeft: 2, - background: 'rgba(39, 42, 68, 0.6)', - borderRadius: 40, - height: 22, - padding: '3px 6px', -}); - -const CollectibleText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_400, - textAlign: 'center', -})); - -const GalleryCollectibleText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_l, - color: props.theme.colors.white_400, -})); - -const Text = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - textTransform: 'uppercase', - color: props.theme.colors.white_0, - fontSize: 10, - marginLeft: props.theme.spacing(2), -})); - -const GalleryButtonContainer = styled.div` - width: 190px; - border-radius: 12px; -`; - -const RowButtonContainer = styled.div((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - columnGap: props.theme.spacing(11), - marginBottom: props.theme.space.l, - marginTop: props.theme.space.m, - width: '100%', -})); - -const Divider = styled.div((props) => ({ - width: '100%', - borderBottom: `1px solid ${props.theme.colors.elevation3}`, -})); - -const DetailSection = styled.div((props) => ({ - display: 'flex', - flexDirection: !props.isGallery ? 'row' : 'column', - justifyContent: 'space-between', - columnGap: props.theme.space.m, - width: '100%', -})); - -const ExtensionLoaderContainer = styled.div({ - height: '100%', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-around', - alignItems: 'center', -}); - -const GalleryLoaderContainer = styled.div({ - display: 'flex', - flexDirection: 'column', -}); - -const StyledBarLoader = styled(BetterBarLoader)<{ - withMarginBottom?: boolean; -}>((props) => ({ - padding: 0, - borderRadius: props.theme.radius(1), - marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0, -})); - -const StyledSeparator = styled(Separator)` - width: 100%; -`; - -const TitleLoader = styled.div` - display: flex; - flex-direction: column; -`; - -const ActionButtonsLoader = styled.div((props) => ({ - display: 'flex', - justifyContent: 'center', - columnGap: props.theme.spacing(11), -})); - -const ActionButtonLoader = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - rowGap: props.theme.spacing(4), -})); - -const InfoContainer = styled.div((props) => ({ - width: '100%', - display: 'flex', - justifyContent: 'space-between', - padding: `0 ${props.theme.spacing(8)}px`, -})); - -const RareSatsBundleCallout = styled(Callout)((props) => ({ - width: props.isGallery ? 400 : '100%', - marginBottom: props.isGallery ? 0 : props.theme.space.l, - marginTop: props.isGallery ? props.theme.space.xs : 0, -})); - -const SatributesIconsContainer = styled.div((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - marginTop: props.isGallery ? props.theme.space.m : 0, -})); - -const SatributesBadgeContainer = styled.div((props) => ({ - marginTop: props.isGallery ? 0 : props.theme.space.m, -})); -const SatributesBadges = styled.div((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - flexWrap: 'wrap', - maxWidth: props.isGallery ? 400 : '100%', - marginTop: props.theme.space.s, -})); -const Badge = styled.div<{ backgroundColor?: string; isLastItem: boolean }>((props) => ({ - display: 'inline-flex', - flexDirection: 'row', - alignItems: 'center', - backgroundColor: props.backgroundColor, - padding: `${props.theme.space.s} ${props.theme.space.s}`, - borderRadius: props.theme.radius(2), - border: `1px solid ${props.theme.colors.elevation3}`, - marginRight: props.isLastItem ? 0 : props.theme.space.xs, - marginBottom: props.theme.space.xs, -})); -const SatributeBadgeLabel = styled(StyledP)` - margin-left: ${(props) => props.theme.space.xs}; -`; -const DataItemsContainer = styled.div` - margin-top: ${(props) => props.theme.space.l}; -`; - function OrdinalDetailScreen() { const { t } = useTranslation('translation', { keyPrefix: 'NFT_DETAIL_SCREEN' }); const { t: commonT } = useTranslation('translation', { keyPrefix: 'COMMON' }); @@ -500,7 +176,15 @@ function OrdinalDetailScreen() { case 'mint': return ( - + + + + {!isGallery && ( props.theme.scrollbar}; + overflow-y: auto; + padding-bottom: ${(props) => props.theme.space.l}; +`; + +export const PageHeader = styled.div` + padding: ${(props) => (props.isGalleryOpen ? props.theme.space.m : 0)}; + padding-top: 0; + max-width: 1224px; + margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; + margin-left: auto; + margin-right: auto; + width: 100%; +`; + +// TODO tim: use media queries for this once we get responsive layouts with breakpoints +export const PageHeaderContent = styled.div` + display: flex; + flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; + justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; +`; + +export const AttributesContainer = styled.div((props) => ({ + maxWidth: props.isGalleryOpen ? '285px' : '100%', + padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, +})); + +export const StyledSeparator = styled(Separator)` + margin-bottom: ${(props) => props.theme.space.xxl}; +`; + +/* components */ + +export const StyledWebGalleryButton = styled(WebGalleryButton)` + color: ${(props) => props.theme.colors.white_200}; + margin-top: ${(props) => props.theme.space.xs}; +`; + +export const SendButtonContainer = styled.div` + margin-top: ${(props) => props.theme.space.l}; + width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; +`; + +export const BundleRarityLinkContainer = styled.button` + margin-top: ${(props) => props.theme.space.l}; + display: flex; + justify-content: flex-start; + align-items: center; + gap: ${(props) => props.theme.space.xxs}; + background-color: transparent; + color: ${(props) => props.theme.colors.white_200}; + :hover:enabled { + color: ${(props) => props.theme.colors.white_200}; + } + :active ; + :disabled { + color: ${(props) => props.theme.colors.white_400}; + } + svg { + flex-grow: 0; + flex-shrink: 0; + } +`; + +export const BackButtonContainer = styled.div` + display: flex; + flex-direction: row; + margin-bottom: ${(props) => props.theme.space.xxl}; +`; + +export const ButtonImage = styled.img((props) => ({ + marginRight: props.theme.spacing(3), + alignSelf: 'center', + transform: 'all', +})); + +export const BackButton = styled.button((props) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + background: 'transparent', + marginBottom: props.theme.spacing(12), +})); + +export const AssetDetailButtonText = styled.div((props) => ({ + ...props.theme.body_xs, + fontWeight: 400, + fontSize: 14, + color: props.theme.colors.white['0'], + textAlign: 'center', +})); + +export const NoCollectiblesText = styled.p((props) => ({ + ...props.theme.body_bold_m, + color: props.theme.colors.white_200, + marginTop: props.theme.spacing(16), + marginBottom: 'auto', + textAlign: 'center', +})); + +export const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ + display: props.isGalleryOpen ? 'block' : 'flex', + flexDirection: props.isGalleryOpen ? 'row' : 'column', + alignItems: props.isGalleryOpen ? 'flex-start' : 'center', +})); + +export const SatRangeContainer = styled.div((props) => ({ + marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, + maxWidth: '1224px', + marginLeft: 'auto', + marginRight: 'auto', + width: '100%', +})); + +export const DetailSection = styled.div((props) => ({ + display: 'flex', + flexDirection: props.isGalleryOpen ? 'column' : 'row', + justifyContent: 'space-between', + columnGap: props.theme.space.m, + width: '100%', +})); + +export const SeeRarityContainer = styled.div` + padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; +`; diff --git a/src/app/screens/rareSatsBundle/index.tsx b/src/app/screens/rareSatsBundle/index.tsx index b62f73251..a5eb7bd36 100644 --- a/src/app/screens/rareSatsBundle/index.tsx +++ b/src/app/screens/rareSatsBundle/index.tsx @@ -1,11 +1,8 @@ import ArrowLeft from '@assets/img/dashboard/arrow_left.svg'; import AccountHeaderComponent from '@components/accountHeader'; import AlertMessage from '@components/alertMessage'; -import ActionButton from '@components/button'; -import Separator from '@components/separator'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import WebGalleryButton from '@components/webGalleryButton'; import usePendingOrdinalTxs from '@hooks/queries/usePendingOrdinalTx'; import useNftDataSelector from '@hooks/stores/useNftDataSelector'; import useSatBundleDataReducer from '@hooks/stores/useSatBundleReducer'; @@ -14,6 +11,7 @@ import useSelectedAccount from '@hooks/useSelectedAccount'; import useWalletSelector from '@hooks/useWalletSelector'; import { ArrowRight, ArrowUp } from '@phosphor-icons/react'; import type { BundleSatRange } from '@secretkeylabs/xverse-core'; +import Button from '@ui-library/button'; import { StyledHeading, StyledP } from '@ui-library/common.styled'; import { getBtcTxStatusUrl, @@ -24,143 +22,28 @@ import { import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import OrdinalAttributeComponent from '../ordinalDetail/ordinalAttributeComponent'; +import { + AssetDetailButtonText, + AttributesContainer, + BackButton, + BackButtonContainer, + BundleRarityLinkContainer, + ButtonImage, + Container, + DetailSection, + Header, + NoCollectiblesText, + PageHeader, + PageHeaderContent, + SatRangeContainer, + SeeRarityContainer, + SendButtonContainer, + StyledSeparator, + StyledWebGalleryButton, +} from './index.styled'; import { RareSatsBundleGridItem } from './rareSatsBundleGridItem'; -interface DetailSectionProps { - isGalleryOpen?: boolean; -} - -/* layout */ -const Container = styled.div` - ...${(props) => props.theme.scrollbar}; - overflow-y: auto; - padding-bottom: ${(props) => props.theme.space.l}; -`; - -const PageHeader = styled.div` - padding: ${(props) => (props.isGalleryOpen ? props.theme.space.m : 0)}; - padding-top: 0; - max-width: 1224px; - margin-top: ${(props) => (props.isGalleryOpen ? props.theme.space.xxl : props.theme.space.l)}; - margin-left: auto; - margin-right: auto; - width: 100%; -`; - -// TODO tim: use media queries for this once we get responsive layouts with breakpoints -const PageHeaderContent = styled.div` - display: flex; - flex-direction: ${(props) => (props.isGalleryOpen ? 'row' : 'column')}; - justify-content: ${(props) => (props.isGalleryOpen ? 'space-between' : 'initial')}; -`; - -const AttributesContainer = styled.div((props) => ({ - maxWidth: props.isGalleryOpen ? '285px' : '100%', - padding: props.isGalleryOpen ? 0 : `0 ${props.theme.space.m}`, -})); - -const StyledSeparator = styled(Separator)` - margin-bottom: ${(props) => props.theme.space.xxl}; -`; - -/* components */ - -const StyledWebGalleryButton = styled(WebGalleryButton)` - color: ${(props) => props.theme.colors.white_200}; - margin-top: ${(props) => props.theme.space.xs}; -`; - -const SendButtonContainer = styled.div` - margin-top: ${(props) => props.theme.space.l}; - width: ${(props) => (props.isGalleryOpen ? '222px' : '155px')}; -`; - -const BundleRarityLinkContainer = styled.button` - margin-top: ${(props) => props.theme.space.l}; - display: flex; - justify-content: flex-start; - align-items: center; - gap: ${(props) => props.theme.space.xxs}; - background-color: transparent; - color: ${(props) => props.theme.colors.white_200}; - :hover:enabled { - color: ${(props) => props.theme.colors.white_200}; - } - :active ; - :disabled { - color: ${(props) => props.theme.colors.white_400}; - } - svg { - flex-grow: 0; - flex-shrink: 0; - } -`; - -const BackButtonContainer = styled.div` - display: flex; - flex-direction: row; - margin-bottom: ${(props) => props.theme.space.xxl}; -`; - -const ButtonImage = styled.img((props) => ({ - marginRight: props.theme.spacing(3), - alignSelf: 'center', - transform: 'all', -})); - -const Button = styled.button((props) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - background: 'transparent', - marginBottom: props.theme.spacing(12), -})); - -const AssetDetailButtonText = styled.div((props) => ({ - ...props.theme.body_xs, - fontWeight: 400, - fontSize: 14, - color: props.theme.colors.white['0'], - textAlign: 'center', -})); - -const NoCollectiblesText = styled.p((props) => ({ - ...props.theme.body_bold_m, - color: props.theme.colors.white_200, - marginTop: props.theme.spacing(16), - marginBottom: 'auto', - textAlign: 'center', -})); - -const Header = styled.div<{ isGalleryOpen: boolean }>((props) => ({ - display: props.isGalleryOpen ? 'block' : 'flex', - flexDirection: props.isGalleryOpen ? 'row' : 'column', - alignItems: props.isGalleryOpen ? 'flex-start' : 'center', -})); - -const SatRangeContainer = styled.div((props) => ({ - marginTop: props.isGalleryOpen ? 0 : props.theme.space.xl, - maxWidth: '1224px', - marginLeft: 'auto', - marginRight: 'auto', - width: '100%', -})); - -const DetailSection = styled.div((props) => ({ - display: 'flex', - flexDirection: props.isGalleryOpen ? 'column' : 'row', - justifyContent: 'space-between', - columnGap: props.theme.space.m, - width: '100%', -})); - -const SeeRarityContainer = styled.div` - padding: ${(props) => props.theme.space.l} ${(props) => props.theme.space.m}; -`; - function RareSatsBundle() { const { t } = useTranslation('translation'); const navigate = useNavigate(); @@ -170,7 +53,7 @@ function RareSatsBundle() { const { network } = useWalletSelector(); const { selectedSatBundle: bundle } = useNftDataSelector(); const { isPending, pendingTxHash } = usePendingOrdinalTxs(bundle?.txid); - const [showSendOrdinalsAlert, setShowSendOrdinalsAlert] = useState(false); + const [showSendOrdinalsAlert, setShowSendOrdinalsAlert] = useState(false); const { setSelectedSatBundleDetails } = useSatBundleDataReducer(); const isGalleryOpen: boolean = useMemo(() => document.documentElement.clientWidth > 360, []); @@ -201,14 +84,16 @@ function RareSatsBundle() { return setShowSendOrdinalsAlert(true); } + const link = `/nft-dashboard/ordinal-detail/${bundle?.txid}/send-ordinal?isRareSat=true&vout=${bundle?.vout}`; + if (isLedgerAccount(selectedAccount) && !isInOptions()) { await chrome.tabs.create({ - url: chrome.runtime.getURL('options.html#/nft-dashboard/send-rare-sat'), + url: chrome.runtime.getURL(`options.html#${link}`), }); return; } - navigate('/nft-dashboard/send-rare-sat'); + navigate(link); }; const handleRedirectToTx = () => { @@ -239,12 +124,12 @@ function RareSatsBundle() { {isGalleryOpen && ( - + )} @@ -257,10 +142,10 @@ function RareSatsBundle() { {!isGalleryOpen && } - } - text={t('COMMON.SEND')} - onPress={handleSendOrdinal} + title={t('COMMON.SEND')} + onClick={handleSendOrdinal} /> {isGalleryOpen && ( @@ -284,10 +169,10 @@ function RareSatsBundle() { )} {!isGalleryOpen && ( - )} diff --git a/src/app/screens/receive/index.tsx b/src/app/screens/receive/index.tsx index 7b7c09f88..71429e5bd 100644 --- a/src/app/screens/receive/index.tsx +++ b/src/app/screens/receive/index.tsx @@ -162,7 +162,7 @@ function Receive() { {renderData[currency].title} {renderData[currency].desc} - + {showBnsName && {selectedAccount?.bnsName}} - {renderData[currency].address} + {renderData[currency].address} diff --git a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx index 21afd5566..aa20e9d27 100644 --- a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx +++ b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx @@ -1,23 +1,21 @@ import ActionButton from '@components/button'; +import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import BottomTabBar from '@components/tabBar'; import TopRow from '@components/topRow'; -import useBtcClient from '@hooks/apiClients/useBtcClient'; -import useCoinRates from '@hooks/queries/useCoinRates'; +import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useOrdinalsByAddress from '@hooks/useOrdinalsByAddress'; -import useSeedVault from '@hooks/useSeedVault'; import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; +import useTransactionContext from '@hooks/useTransactionContext'; +import type { TransactionSummary } from '@screens/sendBtc/helpers'; import { - ErrorCodes, - getBtcFiatEquivalent, - signOrdinalSendTransaction, + AnalyticsEvents, type BtcOrdinal, - type SignedBtcTx, + btcTransaction, + type Transport, } from '@secretkeylabs/xverse-core'; -import { useMutation } from '@tanstack/react-query'; import Spinner from '@ui-library/spinner'; -import BigNumber from 'bignumber.js'; -import { useEffect, useMemo, useState } from 'react'; +import { trackMixPanel } from '@utils/mixpanel'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; @@ -52,7 +50,7 @@ const LoaderContainer = styled.div({ }); const ErrorText = styled.h1((props) => ({ - ...props.theme.body_xs, + ...props.theme.typography.body_s, marginBottom: 20, color: props.theme.colors.feedback.error, })); @@ -68,52 +66,27 @@ function RestoreOrdinals() { const { t } = useTranslation('translation'); const selectedAccount = useSelectedAccount(); const { ordinalsAddress, btcAddress } = selectedAccount; - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - const { getSeed } = useSeedVault(); const navigate = useNavigate(); const ordinalsQuery = useOrdinalsByAddress(btcAddress); - const [error, setError] = useState(''); - const [transferringOrdinalId, setTransferringOrdinalId] = useState(null); const location = useLocation(); - const btcClient = useBtcClient(); - - const isRestoreFundFlow = location.state?.isRestoreFundFlow; + const context = useTransactionContext(); + const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); - const ordinalsUtxos = useMemo( - () => ordinalsQuery.ordinals?.map((ord) => ord.utxo), - [ordinalsQuery.ordinals], - ); + const [transaction, setTransaction] = useState(); + const [summary, setSummary] = useState(); + const [error, setError] = useState(''); + const [feeRate, setFeeRate] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [selectedOrdinal, setSelectedOrdinal] = useState(null); - const { - isLoading, - error: transactionError, - mutateAsync, - } = useMutation({ - mutationFn: async ({ ordinal, seedPhrase }) => { - const tx = await signOrdinalSendTransaction( - ordinalsAddress, - ordinal.utxo, - btcAddress, - Number(selectedAccount?.id), - seedPhrase, - btcClient, - network.type, - ordinalsUtxos!, - ); - return tx; - }, - }); + const isRestoreFundFlow = location.state?.isRestoreFundFlow; useEffect(() => { - if (transactionError) { - if (Number(transactionError) === ErrorCodes.InSufficientBalance) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE')); - } else if (Number(transactionError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); - } else setError(transactionError.toString()); + if (!feeRate && btcFeeRate && !feeRatesLoading) { + setFeeRate(btcFeeRate.regular.toString()); } - }, [transactionError]); + }, [feeRate, btcFeeRate, feeRatesLoading]); const handleOnCancelClick = () => { if (isRestoreFundFlow) { @@ -123,33 +96,119 @@ function RestoreOrdinals() { } }; - const onClickTransfer = async (selectedOrdinal: BtcOrdinal) => { - setTransferringOrdinalId(selectedOrdinal.id); - const seedPhrase = await getSeed(); - const signedTx = await mutateAsync({ ordinal: selectedOrdinal, seedPhrase }); - navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedOrdinal.id}`, { - state: { - signedTxHex: signedTx.signedTx, - recipientAddress: ordinalsAddress, - fee: signedTx.fee, - feePerVByte: signedTx.feePerVByte, - fiatFee: getBtcFiatEquivalent(signedTx.fee, BigNumber(btcFiatRate)), - total: signedTx.total, - fiatTotal: getBtcFiatEquivalent(signedTx.total, BigNumber(btcFiatRate)), - ordinalUtxo: selectedOrdinal.utxo, - }, - }); + const onClickTransfer = async (ordinal: BtcOrdinal, desiredFeeRate: string) => { + setSelectedOrdinal(ordinal); + setFeeRate(desiredFeeRate); + try { + setIsLoading(true); + setError(''); + const txSummary = await btcTransaction.sendOrdinals( + context, + [{ toAddress: ordinalsAddress, inscriptionId: ordinal.id }], + Number(desiredFeeRate), + ); + setTransaction(txSummary); + setSummary(await txSummary.getSummary()); + } catch (err) { + setError(t('TX_ERRORS.INSUFFICIENT_BALANCE_FEES')); + setSelectedOrdinal(null); + setTransaction(undefined); + setSummary(undefined); + } finally { + setIsLoading(false); + } + }; + + const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { + const transactionDetail = await btcTransaction.sendOrdinals( + context, + [{ toAddress: ordinalsAddress, inscriptionId: selectedOrdinal?.id! }], + desiredFeeRate, + ); + if (!transactionDetail) return; + const txSummary = await transactionDetail.getSummary(); + if (txSummary) return Number(txSummary.fee); + return undefined; }; + const handleBack = () => { + setSelectedOrdinal(null); + setFeeRate(''); + setTransaction(undefined); + setSummary(undefined); + }; + + const handleSubmit = async (ledgerTransport?: Transport) => { + try { + setIsSubmitting(true); + const txnId = await transaction?.broadcast({ ledgerTransport, rbfEnabled: true }); + trackMixPanel(AnalyticsEvents.TransactionConfirmed, { + protocol: 'ordinals', + action: 'transfer', + wallet_type: selectedAccount?.accountType || 'software', + }); + navigate('/tx-status', { + state: { + txid: txnId, + currency: 'BTC', + error: '', + browserTx: false, + }, + }); + } catch (e) { + console.error(e); + navigate('/tx-status', { + state: { + txid: '', + currency: 'BTC', + error: `${e}`, + browserTx: false, + }, + }); + } finally { + setIsSubmitting(false); + } + }; + + if (ordinalsQuery.isLoading || feeRatesLoading) { + return ( + <> + + + + + + + + ); + } + + if (summary) { + return ( + onClickTransfer(selectedOrdinal!, newFeeRate.toString())} + feeRate={+feeRate} + isSubmitting={isSubmitting} + hideBottomBar + isBroadcast + /> + ); + } + return ( <> - {ordinalsQuery.isLoading ? ( - - - - ) : ordinalsQuery.ordinals?.length === 0 ? ( + {t('RESTORE_ORDINAL_SCREEN.DESCRIPTION')} + {ordinalsQuery.ordinals?.length === 0 ? ( <> {t('RESTORE_ORDINAL_SCREEN.NO_FUNDS')} @@ -157,21 +216,21 @@ function RestoreOrdinals() { ) : ( - <> - {t('RESTORE_ORDINAL_SCREEN.DESCRIPTION')} - {ordinalsQuery.ordinals?.map((ordinal) => ( - - ))} - - {error} - - + ordinalsQuery.ordinals?.map((ordinal) => ( + + )) + )} + {error && ( + + {error} + )} diff --git a/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx b/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx index 30a0a76f7..90faec8dc 100644 --- a/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx +++ b/src/app/screens/restoreFunds/restoreOrdinals/ordinalRow.tsx @@ -60,12 +60,19 @@ const LoaderContainer = styled.div({ interface Props { ordinal: BtcOrdinal; + feeRate: string; isLoading: boolean; disableTransfer: boolean; - handleOrdinalTransfer: (ordinal: BtcOrdinal) => Promise; + handleOrdinalTransfer: (ordinal: BtcOrdinal, feeRate: string) => Promise; } -function OrdinalRow({ ordinal, isLoading, disableTransfer, handleOrdinalTransfer }: Props) { +function OrdinalRow({ + ordinal, + feeRate, + isLoading, + disableTransfer, + handleOrdinalTransfer, +}: Props) { const { t } = useTranslation('translation'); const { data: ordinalData, isLoading: isQuerying } = useInscriptionDetails(ordinal.id); @@ -80,7 +87,10 @@ function OrdinalRow({ ordinal, isLoading, disableTransfer, handleOrdinalTransfer Ordinal - handleOrdinalTransfer(ordinal)} disabled={disableTransfer}> + handleOrdinalTransfer(ordinal, feeRate)} + disabled={disableTransfer} + > {isLoading ? ( diff --git a/src/app/screens/sendBrc20OneStep/index.tsx b/src/app/screens/sendBrc20OneStep/index.tsx index 5d42c4403..327df29ef 100644 --- a/src/app/screens/sendBrc20OneStep/index.tsx +++ b/src/app/screens/sendBrc20OneStep/index.tsx @@ -16,23 +16,24 @@ import { isDangerFeedback, type InputFeedbackProps } from '@ui-library/inputFeed import type { Brc20TransferEstimateFeesParams, ConfirmBrc20TransferState } from '@utils/brc20'; import { isInOptions, replaceCommaByDot } from '@utils/helper'; import { getFtTicker } from '@utils/tokens'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import Brc20TransferForm from './brc20TransferForm'; function SendBrc20Screen() { const { t } = useTranslation('translation', { keyPrefix: 'SEND_BRC20' }); const navigate = useNavigate(); const [searchParams] = useSearchParams(); + const location = useLocation(); const { btcAddress, ordinalsAddress } = useSelectedAccount(); const { network } = useWalletSelector(); const { data: brc20CoinsList } = useGetBrc20FungibleTokens(); const { data: feeRate } = useBtcFeeRate(); + const [amountToSend, setAmountToSend] = useState(location.state?.amount || ''); const [amountError, setAmountError] = useState(null); - const [amountToSend, setAmountToSend] = useState(''); + const [recipientAddress, setRecipientAddress] = useState(location.state?.recipientAddress || ''); const [recipientError, setRecipientError] = useState(null); - const [recipientAddress, setRecipientAddress] = useState(''); const [processing, setProcessing] = useState(false); const transactionContext = useTransactionContext(); @@ -42,10 +43,6 @@ function SendBrc20Screen() { const principal = searchParams.get('principal'); const fungibleToken = brc20CoinsList?.find((coin) => coin.principal === principal); - if (!fungibleToken) { - navigate('/'); - return null; - } const isNextEnabled = !isDangerFeedback(amountError) && @@ -54,7 +51,7 @@ function SendBrc20Screen() { amountToSend !== ''; const handleBackButtonClick = () => { - navigate(-1); + navigate(`/coinDashboard/FT?ftKey=${fungibleToken?.principal}&protocol=brc-20`); }; const validateAmount = (amountInput: string): boolean => { @@ -72,15 +69,6 @@ function SendBrc20Screen() { return true; }; - const onInputChange = (e: React.FormEvent) => { - const newValue = e.currentTarget.value; - const resultRegex = /^\d*\.?\d*$/; - if (resultRegex.test(newValue)) { - validateAmount(newValue); - setAmountToSend(newValue); - } - }; - const validateRecipientAddress = (address: string): boolean => { if (!address) { setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); @@ -103,6 +91,24 @@ function SendBrc20Screen() { return true; }; + useEffect(() => { + if (location.state?.amount) { + validateAmount(location.state.amount); + } + if (location.state?.recipientAddress) { + validateRecipientAddress(location.state.recipientAddress); + } + }, [location.state]); + + const onInputChange = (e: React.FormEvent) => { + const newValue = e.currentTarget.value; + const resultRegex = /^\d*\.?\d*$/; + if (resultRegex.test(newValue)) { + validateAmount(newValue); + setAmountToSend(newValue); + } + }; + const onAddressInputChange = (e: React.ChangeEvent) => { validateRecipientAddress(e.target.value); setRecipientAddress(e.target.value); @@ -111,6 +117,7 @@ function SendBrc20Screen() { const handleOnPressNext = async () => { try { if ( + !fungibleToken || !validateAmount(amountToSend) || !validateRecipientAddress(recipientAddress) || !feeRate @@ -151,6 +158,11 @@ function SendBrc20Screen() { } }; + if (!fungibleToken) { + navigate('/'); + return null; + } + return ( <> diff --git a/src/app/screens/sendBtc/index.tsx b/src/app/screens/sendBtc/index.tsx index 109171f3d..958e6127d 100644 --- a/src/app/screens/sendBtc/index.tsx +++ b/src/app/screens/sendBtc/index.tsx @@ -1,7 +1,6 @@ import useBtcFeeRate from '@hooks/useBtcFeeRate'; import useDebounce from '@hooks/useDebounce'; import useHasFeature from '@hooks/useHasFeature'; -import useNetworkSelector from '@hooks/useNetwork'; import { useResetUserFlow } from '@hooks/useResetUserFlow'; import useSelectedAccount from '@hooks/useSelectedAccount'; import useTransactionContext from '@hooks/useTransactionContext'; @@ -16,7 +15,7 @@ import { import { isInOptions, isLedgerAccount } from '@utils/helper'; import { trackMixPanel } from '@utils/mixpanel'; import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { generateSendMaxTransaction, generateTransaction, @@ -32,31 +31,19 @@ function SendBtcScreen() { useResetUserFlow('/send-btc'); - const location = useLocation(); - const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); const selectedAccount = useSelectedAccount(); const transactionContext = useTransactionContext(); - const [recipientAddress, setRecipientAddress] = useState( - location.state?.recipientAddress || '', - ); + const [recipientAddress, setRecipientAddress] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const [amountSats, setAmountSats] = useState(location.state?.amount || ''); + const [amountSats, setAmountSats] = useState(''); const [feeRate, setFeeRate] = useState(''); const [sendMax, setSendMax] = useState(false); - const amountEditable = location.state?.disableAmountEdit ?? true; - const addressEditable = location.state?.disableAddressEdit ?? true; const debouncedRecipient = useDebounce(recipientAddress, 500); - const initialStep = addressEditable - ? Step.SelectRecipient - : amountEditable - ? Step.SelectRecipient - : Step.Confirm; - - const [currentStep, setCurrentStep] = useState(initialStep); + const [currentStep, setCurrentStep] = useState(Step.SelectRecipient); const [transaction, setTransaction] = useState(); const [summary, setSummary] = useState(); @@ -150,7 +137,7 @@ function SendBtcScreen() { const handleBackButtonClick = () => { if (currentStep > 0) { - setCurrentStep(getPreviousStep(currentStep, addressEditable, amountEditable)); + setCurrentStep(getPreviousStep(currentStep)); } else { handleCancel(); } @@ -159,7 +146,6 @@ function SendBtcScreen() { const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { const { summary: tempSummary } = await generateTransactionAndSummary(desiredFeeRate); if (tempSummary) return Number(tempSummary.fee); - return undefined; }; @@ -222,11 +208,9 @@ function SendBtcScreen() { setAmountSats={setAmountSatsSafe} feeRate={feeRate} setFeeRate={handleFeeRateChange} + getFeeForFeeRate={calculateFeeForFeeRate} sendMax={sendMax} setSendMax={setSendMax} - getFeeForFeeRate={calculateFeeForFeeRate} - addressEditable={addressEditable} - amountEditable={amountEditable} onBack={handleBackButtonClick} onCancel={handleCancel} onConfirm={handleSubmit} diff --git a/src/app/screens/sendBtc/stepDisplay.tsx b/src/app/screens/sendBtc/stepDisplay.tsx index 00d7be006..a43a6c5b6 100644 --- a/src/app/screens/sendBtc/stepDisplay.tsx +++ b/src/app/screens/sendBtc/stepDisplay.tsx @@ -42,8 +42,6 @@ type Props = { sendMax: boolean; setSendMax: (sendMax: boolean) => void; getFeeForFeeRate: (feeRate: number, useEffectiveFeeRate?: boolean) => Promise; - addressEditable: boolean; - amountEditable: boolean; onConfirm: () => void; onBack: () => void; onCancel: () => void; @@ -65,8 +63,6 @@ function StepDisplay({ sendMax, setSendMax, getFeeForFeeRate, - addressEditable, - amountEditable, onConfirm, onBack, onCancel, @@ -89,7 +85,7 @@ function StepDisplay({ header={header} recipientAddress={recipientAddress} setRecipientAddress={setRecipientAddress} - onNext={() => setCurrentStep(getNextStep(Step.SelectRecipient, amountEditable))} + onNext={() => setCurrentStep(getNextStep(Step.SelectRecipient))} isLoading={isLoading} />
@@ -110,7 +106,7 @@ function StepDisplay({ fee={(summary as TransactionSummary)?.fee.toString()} getFeeForFeeRate={getFeeForFeeRate} dustFiltered={(summary as TransactionSummary)?.dustFiltered ?? false} - onNext={() => setCurrentStep(getNextStep(Step.SelectAmount, amountEditable))} + onNext={() => setCurrentStep(getNextStep(Step.SelectAmount))} hasSufficientFunds={!!summary || isLoading} isLoading={isLoading} /> diff --git a/src/app/screens/sendBtc/steps.tsx b/src/app/screens/sendBtc/steps.tsx index 87c859581..2d464b7b1 100644 --- a/src/app/screens/sendBtc/steps.tsx +++ b/src/app/screens/sendBtc/steps.tsx @@ -4,10 +4,10 @@ export enum Step { Confirm = 2, } -export const getNextStep = (currentStep: Step, amountEditable: boolean) => { +export const getNextStep = (currentStep: Step) => { switch (currentStep) { case Step.SelectRecipient: - return amountEditable ? Step.SelectAmount : Step.Confirm; + return Step.SelectAmount; case Step.SelectAmount: return Step.Confirm; case Step.Confirm: @@ -17,19 +17,14 @@ export const getNextStep = (currentStep: Step, amountEditable: boolean) => { } }; -export const getPreviousStep = ( - currentStep: Step, - addressEditable: boolean, - amountEditable: boolean, -) => { +export const getPreviousStep = (currentStep: Step) => { switch (currentStep) { case Step.SelectRecipient: return Step.SelectRecipient; case Step.SelectAmount: - return addressEditable ? Step.SelectRecipient : Step.SelectAmount; return Step.SelectRecipient; case Step.Confirm: - return amountEditable ? Step.SelectAmount : Step.SelectRecipient; + return Step.SelectAmount; default: throw new Error(`Unknown step: ${currentStep}`); } diff --git a/src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx b/src/app/screens/sendInscriptionsRequest/index.tsx similarity index 100% rename from src/app/screens/confirmOrdinalTransaction/SendInscriptionsRequest.tsx rename to src/app/screens/sendInscriptionsRequest/index.tsx diff --git a/src/app/screens/confirmOrdinalTransaction/useSendInscriptions.ts b/src/app/screens/sendInscriptionsRequest/useSendInscriptions.ts similarity index 100% rename from src/app/screens/confirmOrdinalTransaction/useSendInscriptions.ts rename to src/app/screens/sendInscriptionsRequest/useSendInscriptions.ts diff --git a/src/app/screens/sendOrdinal/index.tsx b/src/app/screens/sendOrdinal/index.tsx index 0ad9ebdf5..0869e5b22 100644 --- a/src/app/screens/sendOrdinal/index.tsx +++ b/src/app/screens/sendOrdinal/index.tsx @@ -21,14 +21,19 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import StepDisplay from './stepDisplay'; import { getPreviousStep, Step } from './steps'; -function SendRuneScreen() { +function SendOrdinalScreen() { const navigate = useNavigate(); const isInOption = isInOptions(); - const context = useTransactionContext(); const location = useLocation(); const { id } = useParams(); - const { data: selectedOrdinal } = useAddressInscription(id!); + const params = new URLSearchParams(location.search); + const isRareSatParam = params.get('isRareSat'); + const vout = params.get('vout'); + const isRareSat = isRareSatParam === 'true'; + + const context = useTransactionContext(); + const { data: selectedOrdinal } = useAddressInscription(isRareSat ? undefined : id); const selectedAccount = useSelectedAccount(); const { data: btcFeeRate, isLoading: feeRatesLoading } = useBtcFeeRate(); const [currentStep, setCurrentStep] = useState(Step.SelectRecipient); @@ -49,7 +54,7 @@ function SendRuneScreen() { useResetUserFlow(RoutePaths.SendOrdinal); useEffect(() => { - if (!selectedOrdinal) { + if (!selectedOrdinal && !isRareSat) { return; } if (!feeRate && btcFeeRate && !feeRatesLoading) { @@ -70,11 +75,18 @@ function SendRuneScreen() { setIsLoading(true); setInsufficientFundsError(false); try { - const transactionDetails = await btcTransaction.sendOrdinalsWithSplit( - context, - [{ toAddress: recipientAddress, inscriptionId: id! }], - Number(feeRate), - ); + const transactionDetails = isRareSat + ? await btcTransaction.sendOrdinals( + context, + [{ toAddress: recipientAddress, outpoint: `${id}:${vout}` }], + Number(feeRate), + ) + : await btcTransaction.sendOrdinalsWithSplit( + context, + [{ toAddress: recipientAddress, inscriptionId: id! }], + Number(feeRate), + ); + if (!isActiveEffect) return; if (!transactionDetails) return; setTransaction(transactionDetails); @@ -111,7 +123,7 @@ function SendRuneScreen() { }; }, [context, recipientAddress, feeRate, id]); - if (!selectedOrdinal) { + if (!selectedOrdinal && !isRareSat) { navigate('/'); return null; } @@ -121,7 +133,11 @@ function SendRuneScreen() { window.close(); return; } - navigate(`/nft-dashboard/ordinal-detail/${selectedOrdinal?.id}`); + navigate( + isRareSat + ? `/nft-dashboard/rare-sats-bundle` + : `/nft-dashboard/ordinal-detail/${selectedOrdinal?.id}`, + ); }; const handleBackButtonClick = () => { @@ -133,11 +149,18 @@ function SendRuneScreen() { }; const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => { - const transactionDetails = await btcTransaction.sendOrdinalsWithSplit( - context, - [{ toAddress: recipientAddress, inscriptionId: id! }], - desiredFeeRate, - ); + const transactionDetails = isRareSat + ? await btcTransaction.sendOrdinals( + context, + [{ toAddress: recipientAddress, outpoint: `${id}:${vout}` }], + desiredFeeRate, + ) + : await btcTransaction.sendOrdinalsWithSplit( + context, + [{ toAddress: recipientAddress, inscriptionId: id! }], + desiredFeeRate, + ); + if (!transactionDetails) return; const txSummary = await transactionDetails.getSummary(); if (txSummary) return Number(txSummary.fee); @@ -150,7 +173,7 @@ function SendRuneScreen() { const txnId = await transaction?.broadcast({ ledgerTransport, rbfEnabled: true }); trackMixPanel(AnalyticsEvents.TransactionConfirmed, { - protocol: 'runes', + protocol: 'ordinals', action: 'transfer', wallet_type: selectedAccount?.accountType || 'software', }); @@ -202,4 +225,4 @@ function SendRuneScreen() { ); } -export default SendRuneScreen; +export default SendOrdinalScreen; diff --git a/src/app/screens/sendOrdinal/stepDisplay.tsx b/src/app/screens/sendOrdinal/stepDisplay.tsx index 231d5ec97..b7be5811e 100644 --- a/src/app/screens/sendOrdinal/stepDisplay.tsx +++ b/src/app/screens/sendOrdinal/stepDisplay.tsx @@ -1,10 +1,16 @@ import OrdinalIcon from '@assets/img/rareSats/ic_ordinal_small_over_card.svg'; import ConfirmBtcTransaction from '@components/confirmBtcTransaction'; import RecipientSelector from '@components/recipientSelector'; +import useTextOrdinalContent from '@hooks/useTextOrdinalContent'; import OrdinalImage from '@screens/ordinals/ordinalImage'; import type { TransactionSummary } from '@screens/sendBtc/helpers'; -import type { Inscription, RuneSummary } from '@secretkeylabs/xverse-core'; +import { getBrc20Details, type Inscription, type RuneSummary } from '@secretkeylabs/xverse-core'; import Avatar from '@ui-library/avatar'; +import { + getInscriptionsCollectionGridItemSubText, + getInscriptionsCollectionGridItemSubTextColor, +} from '@utils/inscriptions'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import SendLayout from '../../layouts/sendLayout'; @@ -32,7 +38,7 @@ const Container = styled.div` type Props = { summary: TransactionSummary | undefined; runeSummary: RuneSummary | undefined; - ordinal: Inscription; + ordinal?: Inscription; currentStep: Step; setCurrentStep: (step: Step) => void; recipientAddress: string; @@ -68,12 +74,37 @@ function StepDisplay({ }: Props) { const { t } = useTranslation('translation'); - const header = ( + const textContent = useTextOrdinalContent(ordinal); + const contentType = ordinal?.content_type ?? ''; + const brc20Details = useMemo( + () => getBrc20Details(textContent!, contentType), + [textContent, contentType], + ); + const brc20Status = getInscriptionsCollectionGridItemSubText(ordinal); + const brc20StatusColor = getInscriptionsCollectionGridItemSubTextColor(ordinal); + const brc20Summary = brc20Details + ? { + ...brc20Details, + status: brc20Status, + statusColor: brc20StatusColor, + } + : undefined; + + let header: React.ReactNode = ( - } /> - {t('SEND.SEND')} Ordinal + {t('SEND.SEND_TO')} ); + + if (ordinal) { + header = ( + + } /> + {t('SEND.SEND')} Ordinal + + ); + } + switch (currentStep) { case Step.SelectRecipient: return ( @@ -100,6 +131,7 @@ function StepDisplay({ props.theme.space.l}; -`; - -const InputGroup = styled.div` - margin-top: ${(props) => props.theme.spacing(8)}px; -`; - -const Label = styled.label((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.theme.colors.white_200, - display: 'flex', - flex: 1, -})); - -const AmountInputContainer = styled.div<{ error: boolean }>((props) => ({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - marginTop: props.theme.space.xs, - marginBottom: props.theme.space.xs, - border: props.error - ? `1px solid ${props.theme.colors.danger_dark_200}` - : `1px solid ${props.theme.colors.white_800}`, - backgroundColor: props.theme.colors.elevation_n1, - borderRadius: props.theme.radius(1), - paddingLeft: props.theme.space.s, - paddingRight: props.theme.space.s, - height: 44, -})); - -const InputFieldContainer = styled.div(() => ({ - flex: 1, -})); - -const InputField = styled.input((props) => ({ - ...props.theme.typography.body_m, - backgroundColor: 'transparent', - color: props.theme.colors.white_0, - width: '100%', - border: 'transparent', -})); - -const ErrorContainer = styled.div((props) => ({ - marginTop: props.theme.space.xs, - marginBottom: props.theme.space.l, -})); - -const RowContainer = styled.div({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', -}); - -const StyledCallout = styled(Callout)` - margin-bottom: ${(props) => props.theme.spacing(14)}px; -`; - -function SendOrdinal() { - const { t } = useTranslation('translation', { keyPrefix: 'SEND' }); - const navigate = useNavigate(); - const { selectedSatBundle } = useNftDataSelector(); - const btcClient = useBtcClient(); - const location = useLocation(); - const selectedAccount = useSelectedAccount(); - const { network } = useWalletSelector(); - const { btcFiatRate } = useCoinRates(); - - const { getSeed } = useSeedVault(); - const [ordinalUtxo, setOrdinalUtxo] = useState(undefined); - const [recipientAddress, setRecipientAddress] = useState(''); - const [recipientError, setRecipientError] = useState(null); - useResetUserFlow('/send-rare-sat'); - - const { - isLoading, - data, - error: txError, - mutate, - } = useMutation({ - mutationFn: async (recipient) => { - const addressUtxos = await btcClient.getUnspentUtxos(selectedAccount.ordinalsAddress); - const ordUtxo = addressUtxos.find( - (utxo) => - `${utxo.txid}:${utxo.vout}` === `${selectedSatBundle?.txid}:${selectedSatBundle?.vout}`, - ); - setOrdinalUtxo(ordUtxo); - if (ordUtxo) { - const seedPhrase = await getSeed(); - const signedTx = await signOrdinalSendTransaction( - recipient, - ordUtxo, - selectedAccount.btcAddress, - Number(selectedAccount.id), - seedPhrase, - btcClient, - network.type, - [ordUtxo], - ); - return signedTx; - } - }, - }); - - useEffect(() => { - if (data) { - navigate(`/nft-dashboard/confirm-ordinal-tx/${selectedSatBundle?.txid}`, { - state: { - signedTxHex: data.signedTx, - recipientAddress, - fee: data.fee, - feePerVByte: data.feePerVByte, - fiatFee: getBtcFiatEquivalent(data.fee, new BigNumber(btcFiatRate)), - total: data.total, - fiatTotal: getBtcFiatEquivalent(data.total, new BigNumber(btcFiatRate)), - ordinalUtxo, - isRareSat: true, - }, - }); - } - }, [data]); // eslint-disable-line react-hooks/exhaustive-deps - - useEffect(() => { - if (txError) { - if (Number(txError) === ErrorCodes.InSufficientBalance) { - setRecipientError({ variant: 'danger', message: t('ERRORS.INSUFFICIENT_BALANCE') }); - } else if (Number(txError) === ErrorCodes.InSufficientBalanceWithTxFee) { - setRecipientError({ - variant: 'danger', - message: t('ERRORS.INSUFFICIENT_BALANCE_FEES'), - }); - } else setRecipientError({ variant: 'danger', message: txError.toString() }); - } - }, [txError]); // eslint-disable-line react-hooks/exhaustive-deps - - const handleBackButtonClick = () => { - navigate(-1); - }; - - const validateRecipientAddress = (address: string): boolean => { - if (!address) { - setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_REQUIRED') }); - return false; - } - if ( - !validateBtcAddress({ - btcAddress: address, - network: network.type, - }) - ) { - setRecipientError({ variant: 'danger', message: t('ERRORS.ADDRESS_INVALID') }); - return false; - } - if (address === selectedAccount.ordinalsAddress || address === selectedAccount.btcAddress) { - setRecipientError({ variant: 'info', message: t('YOU_ARE_TRANSFERRING_TO_YOURSELF') }); - return true; - } - setRecipientError(null); - return true; - }; - - const onPressNext = async () => { - if (validateRecipientAddress(recipientAddress)) { - mutate(recipientAddress); - } - }; - - const handleAddressChange = (e: React.ChangeEvent) => { - validateRecipientAddress(e.target.value); - setRecipientAddress(e.target.value); - }; - - const isNextEnabled = !isDangerFeedback(recipientError) && !!recipientAddress; - - // hide back button if there is no history - const hideBackButton = location.key === 'default'; - - return ( - - -
- - {t('SEND_TO')} - - - - - - - - - - - - {recipientError && } - - - -
- - - -
-
- ); -} - -export default SendOrdinal; diff --git a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx index 6a406e8a8..aaa680125 100644 --- a/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx +++ b/src/app/screens/swap/components/psbtConfirmation/psbtConfirmation.tsx @@ -65,7 +65,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop currency: 'BTC', errorTitle: '', error, - browserTx: true, + browserTx: false, }, }); } @@ -143,7 +143,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop txid: orderResponse.txid, currency: 'BTC', error: '', - browserTx: true, + browserTx: false, }, }); } catch (err) { @@ -155,7 +155,7 @@ export default function PsbtConfirmation({ orderInfo, onClose, onConfirm }: Prop currency: 'BTC', errorTitle: '', error: err.message, - browserTx: true, + browserTx: false, }, }); } diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx index 1a989bb10..7bd4adacc 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenFromBottomSheet/index.tsx @@ -1,8 +1,8 @@ import TokenTile from '@components/tokenTile'; -import type { FungibleToken, TokenBasic } from '@secretkeylabs/xverse-core'; +import { AnalyticsEvents, type FungibleToken, type Token } from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Sheet from '@ui-library/sheet'; -import Spinner from '@ui-library/spinner'; +import { trackMixPanel } from '@utils/mixpanel'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import useFromTokens from './useFromTokens'; @@ -15,12 +15,6 @@ const Container = styled.div((props) => ({ gap: props.theme.space.xl, })); -const SpinnerContainer = styled.div` - display: flex; - justify-content: center; - align-items: center; -`; - const StyledTokenTile = styled(TokenTile)` padding: 0; background-color: transparent; @@ -29,55 +23,55 @@ const StyledTokenTile = styled(TokenTile)` interface Props { visible: boolean; title: string; - to?: TokenBasic; onSelectCoin: (token: FungibleToken | 'BTC') => void; onClose: () => void; + to?: Token; } -export default function TokenFromBottomSheet({ visible, title, to, onSelectCoin, onClose }: Props) { +export default function TokenFromBottomSheet({ visible, title, onSelectCoin, onClose, to }: Props) { const { t } = useTranslation('translation', { keyPrefix: 'SWAP_SCREEN' }); - const { data, error, isLoading } = useFromTokens(to); + const fromTokens = useFromTokens(to); return ( - {isLoading && ( - - - - )} - {!!(data && !isLoading) && - data.map((token) => { - if (token === 'BTC') { - return ( - { - onSelectCoin(token); - onClose(); - }} - /> - ); - } - if (token.protocol === 'runes' && 'principal' in token) { - return ( - { - onSelectCoin(token); - onClose(); - }} - fungibleToken={token} - /> - ); - } - return null; - })} - {!!(data?.length === 0 || error) && !isLoading && ( + {fromTokens.map((token) => { + if (token === 'BTC') { + return ( + { + onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { + token: 'Bitcoin', + }); + onClose(); + }} + /> + ); + } + if (token.protocol === 'runes' && 'principal' in token) { + return ( + { + onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapFrom, { + token: token.name, + }); + onClose(); + }} + fungibleToken={token} + /> + ); + } + return null; + })} + {!!(fromTokens.length === 0) && !fromTokens && ( {t('ERRORS.NO_TOKENS_FOUND')} diff --git a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts index 06d585620..7ce7dd4bc 100644 --- a/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts +++ b/src/app/screens/swap/components/tokenFromBottomSheet/useFromTokens.ts @@ -1,69 +1,26 @@ import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import useCoinRates from '@hooks/queries/useCoinRates'; import useSelectedAccount from '@hooks/useSelectedAccount'; -import useWalletSelector from '@hooks/useWalletSelector'; -import { mapFTProtocolToSwapProtocol } from '@screens/swap/utils'; -import { - getXverseApiClient, - type FungibleToken, - type TokenBasic, -} from '@secretkeylabs/xverse-core'; -import { useQuery } from '@tanstack/react-query'; -import { handleRetries } from '@utils/query'; +import { type FungibleToken, type Token } from '@secretkeylabs/xverse-core'; import { sortFtByFiatBalance } from '@utils/tokens'; -const useFromTokens = (to?: TokenBasic) => { - const { network } = useWalletSelector(); +const useFromTokens = (toToken?: Token) => { const { unfilteredData: runesCoinsList } = useRuneFungibleTokensQuery(); const { stxBtcRate, btcFiatRate } = useCoinRates(); const { btcAddress } = useSelectedAccount(); - const filteredRunesTokensObject = (runesCoinsList ?? []).reduce((acc, ft) => { - acc[ft.principal] = ft; - return acc; - }, {} as Record); + // Create a copy of the tokens array to avoid global changes + const tokens: (FungibleToken | 'BTC')[] = [...(runesCoinsList ?? [])].sort((a, b) => + sortFtByFiatBalance(a, b, stxBtcRate, btcFiatRate), + ); - const runesBasicTokens = - Object.values(filteredRunesTokensObject).map((ft) => ({ - ticker: ft.principal, - protocol: mapFTProtocolToSwapProtocol(ft.protocol ?? 'runes'), - })) ?? []; + if (btcAddress && toToken?.protocol !== 'btc') tokens.unshift('BTC'); - const btcBasicToken: TokenBasic = { protocol: 'btc', ticker: 'BTC' }; - const userTokens = [...(btcAddress ? [btcBasicToken] : [])].concat(runesBasicTokens); + const filteredTokens = toToken + ? tokens.filter((token) => token === 'BTC' || token.principal !== toToken.ticker) + : tokens; - const queryFn = async () => { - const response = await getXverseApiClient(network.type).swaps.getSourceTokens({ - to, - userTokens, - }); - - return response - .filter((token) => token.protocol === 'btc' || !!filteredRunesTokensObject[token.ticker]) - .map((token) => { - if (token.protocol === 'btc') { - return 'BTC'; - } - if (token.protocol === 'runes') { - return filteredRunesTokensObject[token.ticker]; - } - - return token; - }) - .sort((a, b) => { - if (b === 'BTC') return 1; - const aFT = a as FungibleToken; - const bFT = b as FungibleToken; - return sortFtByFiatBalance(aFT, bFT, stxBtcRate, btcFiatRate); - }); - }; - - return useQuery({ - enabled: userTokens.length > 0, - retry: handleRetries, - queryKey: ['swap-from-tokens', network.type, to, userTokens], - queryFn, - }); + return filteredTokens; }; export default useFromTokens; diff --git a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx index 6ba0e75c9..51dfa62e2 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/index.tsx +++ b/src/app/screens/swap/components/tokenToBottomSheet/index.tsx @@ -2,11 +2,18 @@ import TokenTile from '@components/tokenTile'; import useDebounce from '@hooks/useDebounce'; import { MagnifyingGlass } from '@phosphor-icons/react'; import { mapFTProtocolToSwapProtocol, mapSwapTokenToFT } from '@screens/swap/utils'; -import type { FungibleToken, Protocol, Token, TokenBasic } from '@secretkeylabs/xverse-core'; +import { + AnalyticsEvents, + type FungibleToken, + type Protocol, + type Token, + type TokenBasic, +} from '@secretkeylabs/xverse-core'; import { StyledP } from '@ui-library/common.styled'; import Input from '@ui-library/input'; import Sheet from '@ui-library/sheet'; import Spinner from '@ui-library/spinner'; +import { trackMixPanel } from '@utils/mixpanel'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -152,6 +159,9 @@ export default function TokenToBottomSheet({ currency="BTC" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { + token: 'Bitcoin', + }); handleClose(); }} hideBalance @@ -166,6 +176,9 @@ export default function TokenToBottomSheet({ currency="FT" onPress={() => { onSelectCoin(token); + trackMixPanel(AnalyticsEvents.SelectTokenToSwapTo, { + token: token.name ?? token.ticker, + }); handleClose(); }} fungibleToken={mapSwapTokenToFT(token)} diff --git a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts index d1e46ec39..71fc4c184 100644 --- a/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts +++ b/src/app/screens/swap/components/tokenToBottomSheet/useToTokens.ts @@ -29,7 +29,13 @@ const useToTokens = (protocol: Protocol, from?: TokenBasic, query?: string) => { userTokens, }); - return response.items.sort((a) => (a.protocol === 'btc' ? -1 : 1)); + const sortedResponse = response.items.sort((a) => (a.protocol === 'btc' ? -1 : 1)); + + const filteredResponse = from + ? sortedResponse.filter((s) => s.ticker !== from.ticker) + : sortedResponse; + + return filteredResponse; }; return useQuery({ diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index b12ecdf54..6f3d0f26a 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -1,6 +1,7 @@ import ArrowSwap from '@assets/img/icons/ArrowSwap.svg'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; +import useRuneFloorPriceQuery from '@hooks/queries/runes/useRuneFloorPriceQuery'; import { useRuneFungibleTokensQuery } from '@hooks/queries/runes/useRuneFungibleTokensQuery'; import useGetQuotes from '@hooks/queries/swaps/useGetQuotes'; import useBtcWalletData from '@hooks/queries/useBtcWalletData'; @@ -36,6 +37,7 @@ import PsbtConfimation from './components/psbtConfirmation/psbtConfirmation'; import RouteItem from './components/routeItem'; import TokenFromBottomSheet from './components/tokenFromBottomSheet'; import TokenToBottomSheet from './components/tokenToBottomSheet'; +import trackSwapMixPanel from './mixpanel'; import QuoteSummary from './quoteSummary'; import QuotesModal from './quotesModal'; import { @@ -134,6 +136,7 @@ export default function SwapScreen() { const params = new URLSearchParams(location.search); const defaultFrom = params.get('from'); const { quotes, loading: quotesLoading, error: quotesError, fetchQuotes } = useGetQuotes(); + const { data: runeFloorPrice } = useRuneFloorPriceQuery(toToken?.name ?? ''); const runesCoinsList = unfilteredData ?? []; @@ -170,9 +173,13 @@ export default function SwapScreen() { return; } - trackMixPanel(AnalyticsEvents.FetchSwapQuote, { - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, + trackSwapMixPanel(AnalyticsEvents.FetchSwapQuote, { + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, }); fetchQuotes({ @@ -317,13 +324,7 @@ export default function SwapScreen() { useEffect(() => { if (errorMessage) { const toastId = toast.custom( - { - toast.remove(toastId); - }} - />, + toast.remove(toastId)} />, { duration: 3000 }, ); // Reset @@ -367,10 +368,14 @@ export default function SwapScreen() { if (!fromToken || !toToken || !provider) { return; } - trackMixPanel(AnalyticsEvents.SignSwap, { - provider: provider.name, - from: fromToken === 'BTC' ? 'BTC' : fromToken.name, - to: toToken.protocol === 'btc' ? 'BTC' : toToken.name ?? toToken.ticker, + trackSwapMixPanel(AnalyticsEvents.SignSwap, { + provider, + fromToken, + toToken, + amount, + quote, + btcFiatRate, + runeFloorPrice, }); }; @@ -515,7 +520,6 @@ export default function SwapScreen() { )} - {QuoteModal} {!hasQuoteError && ( ); diff --git a/src/app/screens/settings/changeNetwork/nodeInput.tsx b/src/app/screens/settings/changeNetwork/nodeInput.tsx index bfab5cb48..2a34d42b3 100644 --- a/src/app/screens/settings/changeNetwork/nodeInput.tsx +++ b/src/app/screens/settings/changeNetwork/nodeInput.tsx @@ -20,7 +20,7 @@ const NodeText = styled.label` const NodeResetButton = styled.button` ${(props) => props.theme.typography.body_medium_m} background: none; - color: ${(props) => props.theme.colors.white_200}; + color: ${(props) => props.theme.colors.tangerine}; `; // TODO create and use a ui-library input with proper input box styling diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts index 0cd594b60..29edd0662 100644 --- a/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.styles.ts @@ -6,9 +6,8 @@ export const Container = styled.div((props) => ({ display: 'flex', flexDirection: 'column', flex: 1, - marginTop: props.theme.spacing(20), - paddingLeft: props.theme.spacing(8), - paddingRight: props.theme.spacing(8), + paddingLeft: props.theme.space.m, + paddingRight: props.theme.space.m, })); export const ClientHeader = styled('div')({ diff --git a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx index 60bef473d..716ad6ceb 100644 --- a/src/app/screens/settings/connectedAppsAndPermissions/index.tsx +++ b/src/app/screens/settings/connectedAppsAndPermissions/index.tsx @@ -3,7 +3,9 @@ import * as utils from '@components/permissionsManager/utils'; import BottomBar from '@components/tabBar'; import TopRow from '@components/topRow'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { SubTitle, Title } from '../index.styles'; import { Button, ClientHeader, @@ -16,6 +18,7 @@ import { } from './index.styles'; function ConnectedAppsAndPermissionsScreen() { + const { t } = useTranslation('translation', { keyPrefix: 'CONNECTED_APPS' }); const navigate = useNavigate(); const { removeClient } = usePermissionsUtils(); const { store } = usePermissionsStore(); @@ -30,8 +33,10 @@ function ConnectedAppsAndPermissionsScreen() { return ( <> - + + {t('TITLE')} + {store.clients.size === 0 ? t('EMPTY_MESSAGE') : t('SUBTITLE')} {[...store.clients].map((client) => (
diff --git a/src/app/screens/settings/index.styles.ts b/src/app/screens/settings/index.styles.ts new file mode 100644 index 000000000..718399e32 --- /dev/null +++ b/src/app/screens/settings/index.styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const Container = styled.div((props) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + padding: `0 ${props.theme.space.s}`, + ...props.theme.scrollbar, +})); + +export const Title = styled.h1((props) => ({ + ...props.theme.typography.headline_xs, + paddingTop: props.theme.space.xs, + paddingBottom: props.theme.space.m, +})); + +export const SubTitle = styled.p((props) => ({ + ...props.theme.typography.body_m, + color: props.theme.colors.white_200, + marginBottom: props.theme.space.l, +})); diff --git a/src/app/screens/settings/index.tsx b/src/app/screens/settings/index.tsx index f7781a074..39b470845 100644 --- a/src/app/screens/settings/index.tsx +++ b/src/app/screens/settings/index.tsx @@ -1,7 +1,6 @@ import BottomBar from '@components/tabBar'; import useWalletSelector from '@hooks/useWalletSelector'; import { SUPPORT_LINK } from '@utils/constants'; -import { getLockCountdownLabel, isInOptions, isLedgerAccount } from '@utils/helper'; import RoutePaths from 'app/routes/paths'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -12,91 +11,22 @@ const Container = styled.div` display: flex; flex-direction: column; flex: 1; - padding: 0 ${(props) => props.theme.space.xs}; + padding: 0 ${(props) => props.theme.space.s}; ${(props) => props.theme.scrollbar} `; const Title = styled.h1((props) => ({ ...props.theme.typography.headline_l, paddingTop: props.theme.space.xxl, - paddingBottom: props.theme.space.xl, + paddingBottom: props.theme.space.m, })); -type SettingOptions = { - title: string; - text?: string; - textDetail?: string; - onClick?: () => void; - showDivider?: boolean; - showWarningTitle?: boolean; - icon?: string; -}; - function Setting() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { network } = useWalletSelector(); const navigate = useNavigate(); - const SettingsOptions: SettingOptions[] = [ - { - title: 'Preferences', - text: 'Option 1 Text', - onClick: () => { - // Handle Option 1 click - }, - showDivider: true, - }, - { - title: 'Security', - text: 'Option 2 Text', - onClick: () => { - // Handle Option 2 click - }, - showDivider: true, - }, - { - title: 'Connected apps', - text: 'Option 3 Text', - onClick: () => { - // Handle Option 3 click - }, - showDivider: true, - }, - { - title: 'Advanced', - text: 'Option 4 Text', - onClick: () => { - // Handle Option 4 click - }, - showDivider: true, - }, - { - title: 'Network', - text: 'Mainnet', - onClick: () => { - // Handle Option 5 click - }, - showDivider: true, - }, - { - title: 'About', - text: 'Option 6 Text', - onClick: () => { - // Handle Option 6 click - }, - showDivider: true, - }, - { - title: 'Support Center', - text: 'Option 7 Text', - onClick: () => { - // Handle Option 6 click - }, - showDivider: true, - }, - ]; - const openSupport = () => { window.open(SUPPORT_LINK); }; @@ -105,35 +35,38 @@ function Setting() { navigate('/change-network'); }; - const openConnectedAppsAndPermissionsScreen = () => { - navigate(RoutePaths.ConnectedAppsAndPermissions); + const createNavigationHandler = (path: RoutePaths) => () => { + navigate(path); }; + const openPreferences = createNavigationHandler(RoutePaths.Preferences); + const openSecurity = createNavigationHandler(RoutePaths.Security); + const openAdvancedSettings = createNavigationHandler(RoutePaths.AdvancedSettings); + const openAbout = createNavigationHandler(RoutePaths.About); + const openConnectedAppsAndPermissionsScreen = createNavigationHandler( + RoutePaths.ConnectedAppsAndPermissions, + ); + return ( <> {t('TITLE')} navigate(RoutePaths.Preferences)} - showDivider - /> - navigate(RoutePaths.Security)} + onClick={openPreferences} showDivider /> + {process.env.NODE_ENV !== 'production' && ( )} navigate(RoutePaths.AdvancedSettings)} + onClick={openAdvancedSettings} showDivider /> - navigate(RoutePaths.About)} - showDivider - /> - + + diff --git a/src/app/screens/settings/preferences/fiatCurrency/index.tsx b/src/app/screens/settings/preferences/fiatCurrency/index.tsx index 743a84b38..49c4a5b5c 100644 --- a/src/app/screens/settings/preferences/fiatCurrency/index.tsx +++ b/src/app/screens/settings/preferences/fiatCurrency/index.tsx @@ -3,27 +3,15 @@ import TopRow from '@components/topRow'; import { useGetBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens'; import { useGetSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens'; import useWalletSelector from '@hooks/useWalletSelector'; +import { Container, Title } from '@screens/settings/index.styles'; import type { SupportedCurrency } from '@secretkeylabs/xverse-core'; import { ChangeFiatCurrencyAction } from '@stores/wallet/actions/actionCreators'; import { currencyList } from '@utils/currency'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import styled from 'styled-components'; import CurrencyRow from './currencyRow'; -const Container = styled.div` - display: flex; - flex-direction: column; - flex: 1; - overflow-y: auto; - padding: ${(props) => props.theme.space.m}; - padding-top: 0; - &::-webkit-scrollbar { - display: none; - } -`; - function FiatCurrencyScreen() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { fiatCurrency } = useWalletSelector(); @@ -34,7 +22,7 @@ function FiatCurrencyScreen() { useGetBrc20FungibleTokens(); const handleBackButtonClick = () => { - navigate('/settings'); + navigate(-1); }; const handleCurrencyClick = (currency: SupportedCurrency) => { @@ -48,8 +36,9 @@ function FiatCurrencyScreen() { return ( <> - + + {t('CURRENCY')} {currencyList.map((coin, index) => ( ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + padding: `0 ${props.theme.space.xs}`, + ...props.theme.scrollbar, +})); + +const Title = styled.h1((props) => ({ + ...props.theme.typography.headline_xs, + paddingTop: props.theme.space.xs, + paddingBottom: props.theme.space.m, +})); + function Preferences() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { fiatCurrency, walletLockPeriod } = useWalletSelector(); @@ -21,29 +36,33 @@ function Preferences() { navigate('/privacy-preferences'); }; + const handleBackButtonClick = () => { + navigate(-1); + }; + return ( -
-

Preferences

- - - -
+ <> + + + {t('CATEGORIES.PREFERENCES')} + + + + + ); } diff --git a/src/app/screens/settings/preferences/lockCountdown/index.tsx b/src/app/screens/settings/preferences/lockCountdown/index.tsx index 7b27dee89..37a19c741 100644 --- a/src/app/screens/settings/preferences/lockCountdown/index.tsx +++ b/src/app/screens/settings/preferences/lockCountdown/index.tsx @@ -11,25 +11,7 @@ import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; - -const Container = styled.div((props) => ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - overflowY: 'auto', - padding: props.theme.space.m, - paddingTop: 0, - '&::-webkit-scrollbar': { - display: 'none', - }, -})); - -const Title = styled.p((props) => ({ - ...props.theme.typography.body_m, - color: props.theme.colors.white_200, - marginTop: props.theme.space.l, - marginBottom: props.theme.space.l, -})); +import { Container, SubTitle, Title } from '../../index.styles'; interface TimeSelectionBoxProps { selected: boolean; @@ -85,9 +67,10 @@ function LockCountdown() { return ( <> - + - {t('LOCK_COUNTDOWN_TITLE')} + {t('LOCK_COUNTDOWN')} + {t('LOCK_COUNTDOWN_TITLE')} {periodOptions.map((period) => ( ({ - display: 'flex', - flexDirection: 'column', - flex: 1, - overflowY: 'auto', - padding: props.theme.spacing(8), - fontSize: '0.875rem', - color: props.theme.colors.white['200'], - '&::-webkit-scrollbar': { - display: 'none', - }, -})); - -const TextContainer = styled.div({ - lineHeight: '140%', -}); - const SwitchContainer = styled.div((props) => ({ + ...props.theme.typography.body_medium_m, display: 'flex', justifyContent: 'space-between', alignItems: 'center', - marginTop: props.theme.spacing(16), fontWeight: 500, })); @@ -49,7 +33,7 @@ function PrivacyPreferencesScreen() { const [isEnabled, setIsEnabled] = useState(false); const handleBackButtonClick = () => { - navigate('/settings'); + navigate(-1); }; const handleSwitchChange = (checked: boolean) => { @@ -76,9 +60,10 @@ function PrivacyPreferencesScreen() { return ( <> - + - {t('PRIVACY_PREFERENCES.DESCRIPTION')} + {t('PRIVACY_PREFERENCES.TITLE')} + {t('PRIVACY_PREFERENCES.DESCRIPTION')}
{t('PRIVACY_PREFERENCES.AUTHORIZE_DATA_COLLECTION')}
({ width: '100%', height: '100%', @@ -29,7 +18,7 @@ const EnterPasswordContainer = styled.div((props) => ({ right: 0, position: 'fixed', zIndex: 10, - background: 'rgba(25, 25, 48, 0.5)', + background: props.theme.colors.elevation0, backdropFilter: 'blur(16px)', padding: 16, paddingTop: props.theme.spacing(40), @@ -60,8 +49,8 @@ function BackupWalletScreen() { }; }, []); - const goToSettingScreen = () => { - navigate('/settings'); + const goBack = () => { + navigate(-1); }; const handlePasswordNextClick = async () => { @@ -80,8 +69,9 @@ function BackupWalletScreen() { return ( <> - + + {t('SETTING_SCREEN.BACKUP_WALLET')} {!showSeed && ( )} - {showSeed && } + {showSeed && } diff --git a/src/app/screens/settings/security/changePassword/index.tsx b/src/app/screens/settings/security/changePassword/index.tsx index 436f71962..b21e0d743 100644 --- a/src/app/screens/settings/security/changePassword/index.tsx +++ b/src/app/screens/settings/security/changePassword/index.tsx @@ -53,7 +53,7 @@ function ChangePasswordScreen() { const navigate = useNavigate(); const handleBackButtonClick = () => { - navigate('/settings'); + navigate(-1); }; const dismissToast = () => { @@ -103,7 +103,7 @@ function ChangePasswordScreen() { return ( <> - + {currentStepIndex === 0 && ( ({ @@ -17,8 +19,7 @@ const ResetWalletContainer = styled.div((props) => ({ right: 0, position: 'fixed', zIndex: 10, - background: 'rgba(25, 25, 48, 0.5)', - backdropFilter: 'blur(10px)', + background: props.theme.colors.elevation0, paddingLeft: props.theme.space.m, paddingRight: props.theme.space.m, paddingTop: props.theme.spacing(50), @@ -74,44 +75,48 @@ function Security() { navigate('/backup-wallet'); }; + const handleBackButtonClick = () => { + navigate('/settings'); + }; + return ( -
- {showResetWalletDisplay && ( - - - - )} -

Security

- - - - -
+ <> + + + Security + {showResetWalletDisplay && ( + + + + )} + + + + + + ); } diff --git a/src/app/screens/settings/settingComponent/index.tsx b/src/app/screens/settings/settingComponent/index.tsx index 3bb5138e8..63ede3300 100644 --- a/src/app/screens/settings/settingComponent/index.tsx +++ b/src/app/screens/settings/settingComponent/index.tsx @@ -1,4 +1,4 @@ -import { ArrowSquareOut, CaretRight } from '@phosphor-icons/react'; +import { ArrowUpRight, CaretRight } from '@phosphor-icons/react'; import Switch from 'react-switch'; import styled, { useTheme } from 'styled-components'; @@ -18,7 +18,7 @@ const Button = styled.button<{ background: 'transparent', justifyContent: 'flex-start', paddingTop: props.theme.space.m, - paddingBottom: props.theme.space.m, + paddingBottom: props.theme.space.l, borderBottom: props.border, })); @@ -37,15 +37,15 @@ const TitleText = styled.h1((props) => ({ const ComponentText = styled.h1<{ textColor: string; }>((props) => ({ - ...props.theme.typography.body_m, + ...props.theme.typography.body_medium_l, color: props.textColor, flex: 1, textAlign: 'left', })); const ComponentDescriptionText = styled.h1((props) => ({ - ...props.theme.typography.body_bold_m, - color: props.theme.colors.white_0, + ...props.theme.typography.body_medium_l, + color: props.theme.colors.white_200, })); const DescriptionText = styled.p((props) => ({ @@ -53,7 +53,7 @@ const DescriptionText = styled.p((props) => ({ marginTop: props.theme.space.xxs, color: props.theme.colors.white_400, textAlign: 'left', - paddingRight: props.theme.space.m, + paddingRight: props.theme.space.s, })); const Column = styled.div({ @@ -81,7 +81,7 @@ interface SettingComponentProps { title?: string; text: string; textDetail?: string; - link?: string; + link?: boolean; onClick?: () => void; showDivider?: boolean; showWarningTitle?: boolean; @@ -115,7 +115,7 @@ function SettingComponent({
diff --git a/src/app/screens/confirmBrc20Transaction/index.tsx b/src/app/screens/confirmBrc20Transaction/index.tsx index c54a28cb2..1439d3ba5 100644 --- a/src/app/screens/confirmBrc20Transaction/index.tsx +++ b/src/app/screens/confirmBrc20Transaction/index.tsx @@ -212,7 +212,7 @@ function ConfirmBrc20Transaction() { )} Date: Tue, 20 Aug 2024 16:48:55 +0200 Subject: [PATCH 215/219] [ENG-4951] fix: Going back to homepage in the swap flow needs additional clicking (#513) --- src/app/screens/swap/index.tsx | 12 ++---------- src/app/screens/swap/utils.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/screens/swap/index.tsx b/src/app/screens/swap/index.tsx index cc66ef399..3b611aa46 100644 --- a/src/app/screens/swap/index.tsx +++ b/src/app/screens/swap/index.tsx @@ -42,6 +42,7 @@ import type { OrderInfo } from './types'; import { mapFTNativeSwapTokenToTokenBasic, mapFTProtocolToSwapProtocol, + mapFtToSwapToken, mapSwapProtocolToFTProtocol, mapSwapTokenToFT, } from './utils'; @@ -94,15 +95,6 @@ const Icon = styled.img` rotate: 90deg; `; -const mapFtToSwapToken = (ft: FungibleToken | 'BTC'): Token => ({ - ticker: ft === 'BTC' ? 'BTC' : ft.principal ?? '', - name: ft === 'BTC' ? 'Bitcoin' : ft.name ?? ft.assetName ?? '', - protocol: ft === 'BTC' ? 'btc' : mapFTProtocolToSwapProtocol(ft.protocol ?? 'runes'), - divisibility: ft === 'BTC' ? 8 : ft?.decimals ?? 0, - logo: ft === 'BTC' ? undefined : ft.image ?? ft.runeInscriptionId ?? '', - symbol: ft === 'BTC' ? undefined : ft.runeSymbol ?? '', -}); - export default function SwapScreen() { const [amount, setAmount] = useState(''); const [quote, setQuote] = useState(); @@ -148,7 +140,7 @@ export default function SwapScreen() { }, [defaultFrom, runesCoinsList.length]); const handleGoBack = () => { - navigate(-1); + navigate('/'); }; useEffect(() => { diff --git a/src/app/screens/swap/utils.ts b/src/app/screens/swap/utils.ts index 98996e8d9..091f4f0a8 100644 --- a/src/app/screens/swap/utils.ts +++ b/src/app/screens/swap/utils.ts @@ -57,3 +57,12 @@ export const mapFTNativeSwapTokenToTokenBasic = ( const safeTypeToken = token as Token; return { ticker: safeTypeToken.ticker, protocol: safeTypeToken.protocol }; }; + +export const mapFtToSwapToken = (ft: FungibleToken | 'BTC'): Token => ({ + ticker: ft === 'BTC' ? 'BTC' : ft.principal ?? '', + name: ft === 'BTC' ? 'Bitcoin' : ft.name ?? ft.assetName ?? '', + protocol: ft === 'BTC' ? 'btc' : mapFTProtocolToSwapProtocol(ft.protocol ?? 'runes'), + divisibility: ft === 'BTC' ? 8 : ft?.decimals ?? 0, + logo: ft === 'BTC' ? undefined : ft.image ?? ft.runeInscriptionId ?? '', + symbol: ft === 'BTC' ? undefined : ft.runeSymbol ?? '', +}); From 3d9db045f2965b14aaa39fdd6727443e9f741456 Mon Sep 17 00:00:00 2001 From: Terence Ng Date: Wed, 21 Aug 2024 17:55:58 +0800 Subject: [PATCH 216/219] Terence/40 fixes (#515) --- .../components/confirmBtcTransaction/delegateSection.tsx | 2 +- .../components/confirmBtcTransaction/transactionSummary.tsx | 2 +- .../settings/advanced/restoreFunds/recoverRunes/index.tsx | 6 +++--- src/app/screens/signBatchPsbtRequest/index.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/components/confirmBtcTransaction/delegateSection.tsx b/src/app/components/confirmBtcTransaction/delegateSection.tsx index cfe541f51..099f95306 100644 --- a/src/app/components/confirmBtcTransaction/delegateSection.tsx +++ b/src/app/components/confirmBtcTransaction/delegateSection.tsx @@ -55,7 +55,7 @@ const Title = styled(StyledP)` `; type Props = { - delegations?: RuneSummary['transfers']; + delegations?: RuneSummary['receipts']; }; function DelegateSection({ delegations }: Props) { diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx index 80fbdf390..e54e7c2fb 100644 --- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx +++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx @@ -106,7 +106,7 @@ function TransactionSummary({ isSubmitting, getFeeForFeeRate, onFeeRateSet, feeR {runeSummary?.mint && !runeSummary?.mint?.runeIsMintable && ( )} - {hasRuneDelegation && } + {hasRuneDelegation && } diff --git a/src/app/screens/settings/advanced/restoreFunds/recoverRunes/index.tsx b/src/app/screens/settings/advanced/restoreFunds/recoverRunes/index.tsx index 3f45720be..42e2ee89c 100644 --- a/src/app/screens/settings/advanced/restoreFunds/recoverRunes/index.tsx +++ b/src/app/screens/settings/advanced/restoreFunds/recoverRunes/index.tsx @@ -160,9 +160,9 @@ function RecoverRunes() { {t('DESCRIPTION')} - {(runeSummary?.transfers ?? []).map((transfer) => ( - - + {(runeSummary?.receipts ?? []).map((receipt) => ( + + ))} diff --git a/src/app/screens/signBatchPsbtRequest/index.tsx b/src/app/screens/signBatchPsbtRequest/index.tsx index 0f291daaa..60777d69e 100644 --- a/src/app/screens/signBatchPsbtRequest/index.tsx +++ b/src/app/screens/signBatchPsbtRequest/index.tsx @@ -275,7 +275,7 @@ function SignBatchPsbtRequest() { const runeBurns = parsedPsbts.map((psbt) => psbt.runeSummary?.burns ?? []).flat(); const runeDelegations = parsedPsbts .filter((psbt) => !psbt.summary.isFinal) - .map((psbt) => psbt.runeSummary?.transfers ?? []) + .map((psbt) => psbt.runeSummary?.receipts ?? []) .flat(); const hasSomeRuneDelegation = runeDelegations.length > 0; From 724ce0fedd9b1381b3fc8dd5bb8bd4d8d2178667 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Wed, 21 Aug 2024 15:07:44 +0300 Subject: [PATCH 217/219] Mahmoud/eng 4948 UI fixes for settings page (#509) * style fixes * fix default extension toggle flicker * Update src/app/screens/settings/settingComponent/index.tsx * touchup * fix type * fix divider in preference settings --------- Co-authored-by: Terence Ng --- .../settings/changeNetwork/networkRow.tsx | 14 +++++++++---- .../preferences/fiatCurrency/currencyRow.tsx | 4 +++- .../screens/settings/preferences/index.tsx | 4 ++-- .../settings/settingComponent/index.tsx | 20 +++++++------------ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/app/screens/settings/changeNetwork/networkRow.tsx b/src/app/screens/settings/changeNetwork/networkRow.tsx index fc15ea49c..03356d38f 100644 --- a/src/app/screens/settings/changeNetwork/networkRow.tsx +++ b/src/app/screens/settings/changeNetwork/networkRow.tsx @@ -3,7 +3,8 @@ import type { SettingsNetwork } from '@secretkeylabs/xverse-core'; import styled, { useTheme } from 'styled-components'; interface TitleProps { - color: string; + isActive: boolean; + variant: React.CSSProperties; } interface ButtonProps { @@ -20,8 +21,8 @@ const Button = styled.button((props) => ({ })); const Text = styled.h1((props) => ({ - ...props.theme.typography.body_medium_m, - color: props.color, + ...props.variant, + color: props.isActive ? props.theme.colors.white_0 : props.theme.colors.white_0, flex: 1, textAlign: 'left', })); @@ -44,7 +45,12 @@ function NetworkRow({ network, isSelected, onNetworkSelected, showDivider }: Pro onClick={onClick} border={showDivider ? `1px solid ${theme.colors.white_900}` : 'transparent'} > - {network.type} + + {network.type} + {isSelected && tick} ); diff --git a/src/app/screens/settings/preferences/fiatCurrency/currencyRow.tsx b/src/app/screens/settings/preferences/fiatCurrency/currencyRow.tsx index 7310508b9..41b3cc968 100644 --- a/src/app/screens/settings/preferences/fiatCurrency/currencyRow.tsx +++ b/src/app/screens/settings/preferences/fiatCurrency/currencyRow.tsx @@ -6,8 +6,9 @@ import styled, { useTheme } from 'styled-components'; const Button = styled.button<{ $border: string; $color: string; + $variant: React.CSSProperties; }>((props) => ({ - ...props.theme.typography.body_medium_m, + ...props.$variant, color: props.$color, display: 'flex', alignItems: 'center', @@ -50,6 +51,7 @@ function CurrencyRow({ currency, isSelected, onCurrencySelected, showDivider }: onClick={onClick} $border={showDivider ? `1px solid ${theme.colors.white_900}` : 'transparent'} $color={isSelected ? theme.colors.white_0 : theme.colors.white_400} + $variant={isSelected ? theme.typography.body_bold_l : theme.typography.body_medium_l} > diff --git a/src/app/screens/settings/preferences/index.tsx b/src/app/screens/settings/preferences/index.tsx index 88ff57fca..d3963f5be 100644 --- a/src/app/screens/settings/preferences/index.tsx +++ b/src/app/screens/settings/preferences/index.tsx @@ -26,7 +26,7 @@ function Preferences() { const { t } = useTranslation('translation', { keyPrefix: 'SETTING_SCREEN' }); const { fiatCurrency, walletLockPeriod } = useWalletSelector(); const navigate = useNavigate(); - const [isPriorityWallet, setIsPriorityWallet] = useChromeLocalStorage( + const [isPriorityWallet, setIsPriorityWallet] = useChromeLocalStorage( chromeLocalStorageKeys.isPriorityWallet, true, ); @@ -71,6 +71,7 @@ function Preferences() {
diff --git a/src/app/screens/settings/settingComponent/index.tsx b/src/app/screens/settings/settingComponent/index.tsx index 63ede3300..f2e237c08 100644 --- a/src/app/screens/settings/settingComponent/index.tsx +++ b/src/app/screens/settings/settingComponent/index.tsx @@ -45,7 +45,8 @@ const ComponentText = styled.h1<{ const ComponentDescriptionText = styled.h1((props) => ({ ...props.theme.typography.body_medium_l, - color: props.theme.colors.white_200, + marginRight: props.theme.space.xxs, + color: props.theme.colors.white_0, })); const DescriptionText = styled.p((props) => ({ @@ -107,12 +108,10 @@ function SettingComponent({ toggleFunction, }: SettingComponentProps) { const theme = useTheme(); - return ( {title && {title}} -