diff --git a/package-lock.json b/package-lock.json
index c2af757ff..9098aaf24 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "xverse-web-extension",
- "version": "0.34.0",
+ "version": "0.34.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "xverse-web-extension",
- "version": "0.34.0",
+ "version": "0.34.1",
"dependencies": {
"@ledgerhq/hw-transport-webusb": "^6.27.13",
"@phosphor-icons/react": "^2.0.10",
diff --git a/package.json b/package.json
index 4eeb83f78..b9f930fce 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "xverse-web-extension",
"description": "A Bitcoin wallet for Web3",
- "version": "0.34.0",
+ "version": "0.34.1",
"private": true,
"engines": {
"node": "^18.18.2"
diff --git a/src/app/components/confirmBtcTransaction/delegateSection.tsx b/src/app/components/confirmBtcTransaction/delegateSection.tsx
new file mode 100644
index 000000000..2415e0da2
--- /dev/null
+++ b/src/app/components/confirmBtcTransaction/delegateSection.tsx
@@ -0,0 +1,114 @@
+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['burns'];
+};
+
+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/confirmBtcTransaction/index.tsx b/src/app/components/confirmBtcTransaction/index.tsx
index 7e3f03ab6..3a316abcd 100644
--- a/src/app/components/confirmBtcTransaction/index.tsx
+++ b/src/app/components/confirmBtcTransaction/index.tsx
@@ -1,6 +1,7 @@
import { delay } from '@common/utils/ledger';
import BottomModal from '@components/bottomModal';
import ActionButton from '@components/button';
+import { Tab } from '@components/tabBar';
import useWalletSelector from '@hooks/useWalletSelector';
import TransportFactory from '@ledgerhq/hw-transport-webusb';
import { RuneSummary, Transport, btcTransaction } from '@secretkeylabs/xverse-core';
@@ -47,6 +48,7 @@ type Props = {
outputs: btcTransaction.EnhancedOutput[];
feeOutput?: btcTransaction.TransactionFeeOutput;
runeSummary?: RuneSummary;
+ showCenotaphCallout: boolean;
isLoading: boolean;
isSubmitting: boolean;
isBroadcast?: boolean;
@@ -66,6 +68,8 @@ type Props = {
onFeeRateSet?: (feeRate: number) => void;
feeRate?: number;
hasSigHashNone?: boolean;
+ title?: string;
+ selectedBottomTab?: Tab;
};
function ConfirmBtcTransaction({
@@ -73,6 +77,7 @@ function ConfirmBtcTransaction({
outputs,
feeOutput,
runeSummary,
+ showCenotaphCallout,
isLoading,
isSubmitting,
isBroadcast,
@@ -89,6 +94,8 @@ function ConfirmBtcTransaction({
onFeeRateSet,
feeRate,
hasSigHashNone = false,
+ title,
+ selectedBottomTab,
}: Props) {
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentStep, setCurrentStep] = useState(Steps.ConnectLedger);
@@ -177,14 +184,14 @@ function ConfirmBtcTransaction({
) : (
<>
- {t('REVIEW_TRANSACTION')}
+ {title || t('REVIEW_TRANSACTION')}
{hasSigHashNone && (
!receipt.sourceAddresses.some(
- (address) => address === ordinalsAddress || address === btcAddress,
+ (address) =>
+ (address === ordinalsAddress && receipt.destinationAddress === ordinalsAddress) ||
+ (address === btcAddress && receipt.destinationAddress === btcAddress),
),
) ?? [];
const ordinalRuneReceipts = filteredRuneReceipts.filter(
diff --git a/src/app/components/confirmBtcTransaction/transactionSummary.tsx b/src/app/components/confirmBtcTransaction/transactionSummary.tsx
index 146f7956e..b54f3c913 100644
--- a/src/app/components/confirmBtcTransaction/transactionSummary.tsx
+++ b/src/app/components/confirmBtcTransaction/transactionSummary.tsx
@@ -14,6 +14,7 @@ import BigNumber from 'bignumber.js';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
+import DelegateSection from './delegateSection';
import AmountWithInscriptionSatribute from './itemRow/amountWithInscriptionSatribute';
import ReceiveSection from './receiveSection';
import TransferSection from './transferSection';
@@ -33,6 +34,7 @@ const WarningCallout = styled(Callout)`
type Props = {
isPartialTransaction: boolean;
+ showCenotaphCallout: boolean;
inputs: btcTransaction.EnhancedInput[];
outputs: btcTransaction.EnhancedOutput[];
feeOutput?: btcTransaction.TransactionFeeOutput;
@@ -48,6 +50,7 @@ type Props = {
function TransactionSummary({
isPartialTransaction,
+ showCenotaphCallout,
inputs,
outputs,
feeOutput,
@@ -100,6 +103,8 @@ function TransactionSummary({
const showFeeSelector = !!(feeRate && getFeeForFeeRate && onFeeRateSet);
+ const hasRuneDelegation = (runeSummary?.burns.length ?? 0) > 0 && isPartialTransaction;
+
return (
<>
{inscriptionToShow && (
@@ -124,12 +129,16 @@ function TransactionSummary({
{isUnConfirmedInput && (
)}
+ {showCenotaphCallout && (
+
+ )}
{runeSummary?.mint && !runeSummary?.mint?.runeIsOpen && (
)}
{runeSummary?.mint && !runeSummary?.mint?.runeIsMintable && (
)}
+ {hasRuneDelegation && }
-
+ {!hasRuneDelegation && }
{hasOutputScript && !runeSummary && }
diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx
index d60163308..a871566a8 100644
--- a/src/app/routes/index.tsx
+++ b/src/app/routes/index.tsx
@@ -47,6 +47,7 @@ import RareSatsBundle from '@screens/rareSatsBundle';
import RareSatsDetailScreen from '@screens/rareSatsDetail/rareSatsDetail';
import Receive from '@screens/receive';
import RestoreFunds from '@screens/restoreFunds';
+import RecoverRunes from '@screens/restoreFunds/recoverRunes';
import RestoreOrdinals from '@screens/restoreFunds/restoreOrdinals';
import RestoreWallet from '@screens/restoreWallet';
// import SendBrc20Screen from '@screens/sendBrc20';
@@ -340,6 +341,10 @@ const router = createHashRouter([
path: 'restore-ordinals',
element: ,
},
+ {
+ path: 'recover-runes',
+ element: ,
+ },
{
path: 'fiat-currency',
element: ,
diff --git a/src/app/screens/confirmBtcTransaction/index.tsx b/src/app/screens/confirmBtcTransaction/index.tsx
index 61a9eb014..ce9985a8f 100644
--- a/src/app/screens/confirmBtcTransaction/index.tsx
+++ b/src/app/screens/confirmBtcTransaction/index.tsx
@@ -242,7 +242,7 @@ function ConfirmBtcTransaction() {
description={t('BTC_TRANSFER_DANGER_ALERT_DESC')}
buttonText={t('BACK')}
onClose={onClosePress}
- secondButtonText={t('CONITNUE')}
+ secondButtonText={t('CONTINUE')}
onButtonClick={onClosePress}
onSecondButtonClick={onContinueButtonClick}
isWarningAlert
diff --git a/src/app/screens/confirmInscriptionRequest/index.tsx b/src/app/screens/confirmInscriptionRequest/index.tsx
index 6ecc408ce..be57df492 100644
--- a/src/app/screens/confirmInscriptionRequest/index.tsx
+++ b/src/app/screens/confirmInscriptionRequest/index.tsx
@@ -344,7 +344,7 @@ function ConfirmInscriptionRequest() {
description={t('CONFIRM_TRANSACTION.BTC_TRANSFER_DANGER_ALERT_DESC')}
buttonText={t('CONFIRM_TRANSACTION.BACK')}
onClose={onClosePress}
- secondButtonText={t('CONITNUE')}
+ secondButtonText={t('CONTINUE')}
onButtonClick={onClosePress}
onSecondButtonClick={onContinueButtonClick}
isWarningAlert
diff --git a/src/app/screens/restoreFunds/fundsRow.tsx b/src/app/screens/restoreFunds/fundsRow.tsx
index 166f9ccf2..522ba104b 100644
--- a/src/app/screens/restoreFunds/fundsRow.tsx
+++ b/src/app/screens/restoreFunds/fundsRow.tsx
@@ -4,7 +4,6 @@ const Icon = styled.img((props) => ({
marginRight: props.theme.spacing(8),
width: 32,
height: 32,
- borderRadius: 30,
}));
const TitleText = styled.h1((props) => ({
@@ -34,6 +33,11 @@ const RowContainer = styled.button((props) => ({
background: 'transparent',
width: '100%',
marginBottom: 12,
+ ':disabled': {
+ backgroundColor: props.theme.colors.elevation0,
+ opacity: 0.5,
+ cursor: 'not-allowed',
+ },
}));
interface Props {
@@ -41,11 +45,12 @@ interface Props {
title: string;
description: string;
onClick: () => void;
+ disabled?: boolean;
}
-function FundsRow({ image, title, description, onClick }: Props) {
+function FundsRow({ image, title, description, onClick, disabled }: Props) {
return (
-
+
{title}
diff --git a/src/app/screens/restoreFunds/index.tsx b/src/app/screens/restoreFunds/index.tsx
index 079bbbc41..50d885a49 100644
--- a/src/app/screens/restoreFunds/index.tsx
+++ b/src/app/screens/restoreFunds/index.tsx
@@ -1,13 +1,15 @@
import OrdinalsIcon from '@assets/img/nftDashboard/ordinals_icon.svg';
+import RuneIcon from '@assets/img/nftDashboard/rune_icon.svg';
import BottomTabBar from '@components/tabBar';
import TopRow from '@components/topRow';
+import useWalletSelector from '@hooks/useWalletSelector';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import FundsRow from './fundsRow';
const RestoreFundTitle = styled.h1((props) => ({
- ...props.theme.body_l,
+ ...props.theme.typography.body_l,
marginBottom: 15,
marginTop: 24,
marginLeft: 16,
@@ -24,6 +26,7 @@ const Container = styled.div({
function RestoreFunds() {
const { t } = useTranslation('translation', { keyPrefix: 'RESTORE_FUND_SCREEN' });
+ const { hasActivatedOrdinalsKey } = useWalletSelector();
const navigate = useNavigate();
const handleOnCancelClick = () => {
@@ -34,6 +37,10 @@ function RestoreFunds() {
navigate('/restore-ordinals');
};
+ const onRecoverRunesClick = () => {
+ navigate('/recover-runes');
+ };
+
return (
<>
@@ -41,12 +48,19 @@ function RestoreFunds() {
+
-
+
>
);
}
diff --git a/src/app/screens/restoreFunds/recoverRunes/index.tsx b/src/app/screens/restoreFunds/recoverRunes/index.tsx
new file mode 100644
index 000000000..cb7997505
--- /dev/null
+++ b/src/app/screens/restoreFunds/recoverRunes/index.tsx
@@ -0,0 +1,223 @@
+import ConfirmBitcoinTransaction from '@components/confirmBtcTransaction';
+import RuneAmount from '@components/confirmBtcTransaction/itemRow/runeAmount';
+import BottomTabBar from '@components/tabBar';
+import TopRow from '@components/topRow';
+import useBtcFeeRate from '@hooks/useBtcFeeRate';
+import useTransactionContext from '@hooks/useTransactionContext';
+import { TransactionSummary } from '@screens/sendBtc/helpers';
+import { RuneSummary, parseSummaryForRunes, runesTransaction } from '@secretkeylabs/xverse-core';
+import Button from '@ui-library/button';
+import { StyledP } from '@ui-library/common.styled';
+import Spinner from '@ui-library/spinner';
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import styled from 'styled-components';
+
+const ScrollContainer = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ ${(props) => props.theme.scrollbar}
+ padding: 0 ${(props) => props.theme.space.xs};
+`;
+
+const Description = styled(StyledP)`
+ text-align: left;
+ margin: 0 ${(props) => props.theme.space.m} ${(props) => props.theme.space.l}
+ ${(props) => props.theme.space.m};
+`;
+
+const RowContainer = styled.div((props) => ({
+ marginBottom: `${props.theme.space.s}`,
+ background: props.theme.colors.elevation1,
+ borderRadius: 12,
+ padding: `${props.theme.space.m} ${props.theme.space.s}`,
+}));
+
+const Container = styled.div((props) => ({
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ marginTop: props.theme.space.xl,
+}));
+
+const LoaderContainer = styled.div({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flex: 1,
+});
+
+const ButtonContainer = styled.div((props) => ({
+ marginBottom: props.theme.space.l,
+ display: 'flex',
+ alignItems: 'flex-end',
+ padding: `0 ${props.theme.space.m}`,
+}));
+
+// TODO: export this from core
+type EnhancedTransaction = Awaited>;
+
+function RecoverRunes() {
+ const { t } = useTranslation('translation', { keyPrefix: 'RECOVER_RUNES_SCREEN' });
+ const navigate = useNavigate();
+ const [error, setError] = useState('');
+ const [feeRate, setFeeRate] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [isBroadcasting, setIsBroadcasting] = useState(false);
+ const [enhancedTxn, setEnhancedTxn] = useState();
+ const [summary, setSummary] = useState();
+ const [runeSummary, setRuneSummary] = useState();
+ const [isConfirmTx, setIsConfirmTx] = useState(false);
+
+ const { data: btcFeeRates } = useBtcFeeRate();
+ const context = useTransactionContext();
+
+ const generateTransactionAndSummary = async (desiredFeeRate: number) => {
+ const tx = await runesTransaction.recoverRunes(context, desiredFeeRate);
+ const txSummary = await tx.getSummary();
+ const txRuneSummary = await parseSummaryForRunes(context, txSummary, context.network);
+
+ return { transaction: tx, summary: txSummary, runeSummary: txRuneSummary };
+ };
+
+ useEffect(() => {
+ if (!btcFeeRates?.priority) return;
+
+ if (!feeRate) {
+ setFeeRate(btcFeeRates.priority.toString());
+ return;
+ }
+
+ const buildTx = async () => {
+ try {
+ const txDetails = await generateTransactionAndSummary(+feeRate);
+ setEnhancedTxn(txDetails.transaction);
+ setSummary(txDetails.summary);
+ setRuneSummary(txDetails.runeSummary);
+ } catch (e) {
+ setEnhancedTxn(undefined);
+ setSummary(undefined);
+ if (e instanceof Error) {
+ if (e.message === 'No runes to recover') {
+ setError(t('NO_RUNES'));
+ return;
+ }
+ if (e.message.includes('Insufficient funds')) {
+ setError(t('INSUFFICIENT_FUNDS'));
+ return;
+ }
+ }
+ setError((e as Error).message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ buildTx();
+ }, [context, btcFeeRates, feeRate, t]);
+
+ const calculateFeeForFeeRate = async (desiredFeeRate: number): Promise => {
+ const { summary: tempSummary } = await generateTransactionAndSummary(desiredFeeRate);
+ if (tempSummary) return Number(tempSummary.fee);
+
+ return undefined;
+ };
+
+ const handleToggleConfirmTx = () => setIsConfirmTx(!isConfirmTx);
+ const handleOnNavigateBack = () => navigate(-1);
+
+ const onClickTransfer = async () => {
+ setIsBroadcasting(true);
+ try {
+ const txnId = await enhancedTxn?.broadcast();
+ navigate('/tx-status', {
+ state: {
+ txid: txnId,
+ currency: 'BTC',
+ error: '',
+ },
+ });
+ } catch (e) {
+ setError((e as Error).message);
+ } finally {
+ setIsBroadcasting(false);
+ }
+ };
+
+ if (!error && !isLoading) {
+ return !isConfirmTx ? (
+ <>
+
+
+ {t('DESCRIPTION')}
+
+
+ {(runeSummary?.transfers ?? []).map((transfer) => (
+
+
+
+ ))}
+
+
+
+
+
+ >
+ ) : (
+ setFeeRate(newFeeRate.toString())}
+ feeRate={+feeRate}
+ isError={!!error}
+ hideBottomBar={false}
+ selectedBottomTab="settings"
+ showAccountHeader={false}
+ isBroadcast
+ onBackClick={handleToggleConfirmTx}
+ />
+ );
+ }
+ return (
+ <>
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {error}
+
+ )}
+
+ {!isLoading && (
+
+
+
+ )}
+
+ >
+ );
+}
+
+export default RecoverRunes;
diff --git a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx
index 3642d6e75..762488b6e 100644
--- a/src/app/screens/restoreFunds/restoreOrdinals/index.tsx
+++ b/src/app/screens/restoreFunds/restoreOrdinals/index.tsx
@@ -22,7 +22,7 @@ import styled from 'styled-components';
import OrdinalRow from './ordinalRow';
const RestoreFundTitle = styled.h1((props) => ({
- ...props.theme.body_l,
+ ...props.theme.typography.body_l,
marginBottom: 32,
color: props.theme.colors.white_200,
}));
@@ -170,7 +170,7 @@ function RestoreOrdinals() {
>
)}
-
+
>
);
}
diff --git a/src/app/screens/sendBtc/stepDisplay.tsx b/src/app/screens/sendBtc/stepDisplay.tsx
index 0f344e71d..6caae368d 100644
--- a/src/app/screens/sendBtc/stepDisplay.tsx
+++ b/src/app/screens/sendBtc/stepDisplay.tsx
@@ -125,6 +125,7 @@ function StepDisplay({
inputs={summary.inputs}
outputs={summary.outputs}
feeOutput={summary.feeOutput}
+ showCenotaphCallout={!!summary?.runeOp?.Cenotaph?.flaws}
isLoading={false}
confirmText={t('COMMON.CONFIRM')}
cancelText={t('COMMON.CANCEL')}
diff --git a/src/app/screens/sendRune/stepDisplay.tsx b/src/app/screens/sendRune/stepDisplay.tsx
index 3310e4151..c2870b1a5 100644
--- a/src/app/screens/sendRune/stepDisplay.tsx
+++ b/src/app/screens/sendRune/stepDisplay.tsx
@@ -131,6 +131,7 @@ function StepDisplay({
inputs={summary.inputs}
outputs={summary.outputs}
feeOutput={summary.feeOutput}
+ showCenotaphCallout={!!summary?.runeOp?.Cenotaph?.flaws}
runeSummary={runeSummary}
isLoading={false}
confirmText={t('COMMON.CONFIRM')}
diff --git a/src/app/screens/settings/changeNetwork/nodeInput.tsx b/src/app/screens/settings/changeNetwork/nodeInput.tsx
index f23992727..9520c2a74 100644
--- a/src/app/screens/settings/changeNetwork/nodeInput.tsx
+++ b/src/app/screens/settings/changeNetwork/nodeInput.tsx
@@ -77,7 +77,7 @@ function NodeInput({
{t('RESET_TO_DEFAULT')}
-
+
{value && (