Skip to content

Commit

Permalink
refactor: move btc tx summary and runes summary parse logic into cont…
Browse files Browse the repository at this point in the history
…ext (#417)

* refactor: move btc tx summary and runes summary parse logic into context

refactor: move btc tx summary and runes summary parse logic into context

refactor: move btc tx summary and runes summary parse logic into context

and split the transferSection with youWillSendSection

refactor: move btc tx summary and runes summary parse logic into context

refactor: move btc tx summary and runes summary parse logic into context

and split the transferSection with youWillSendSection

* chore: do TODO for a core type

* apply suggestions

* use newest core

* unused import

* refactor naming

* prepare todos

* commit work

* revert

* rebase new work

* fix some bugs

* fix linting

* Update src/app/screens/sendRune/index.tsx

Co-authored-by: Tim Man <[email protected]>

* review

* rename

* touchup + add next todo

* update todo

* remove unnecessary data imo

* Fix playwright-report upload for PR workflow

* touchup

---------

Co-authored-by: Terence Ng <[email protected]>
Co-authored-by: Christine Pinto <[email protected]>
  • Loading branch information
3 people authored Jul 26, 2024
1 parent dd0592a commit 0ecb8ad
Show file tree
Hide file tree
Showing 26 changed files with 819 additions and 562 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: xvfb-run --auto-servernum --server-args="-screen 0 360x360x24" npm run e2etest:smoketest --reporter=html
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
getInputsWithAssetsFromUserAddress,
getNetAmount,
getOutputsWithAssetsFromUserAddress,
getOutputsWithAssetsToUserAddress,
isScriptOutput,
} from '@components/confirmBtcTransaction/utils';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
import type { TransactionSummary } from '@screens/sendBtc/helpers';
import {
type btcTransaction,
type RuneSummary,
type RuneSummaryActions,
} from '@secretkeylabs/xverse-core';
import { createContext, useContext } from 'react';

export type ParsedTxSummaryContextProps = {
summary?: TransactionSummary | btcTransaction.PsbtSummary;
runeSummary?: RuneSummary | RuneSummaryActions;
};

export const ParsedTxSummaryContext = createContext<ParsedTxSummaryContextProps>({
summary: undefined,
runeSummary: undefined,
});

export const useParsedTxSummaryContext = (): {
summary:
| (TransactionSummary & { dustFiltered?: boolean })
| btcTransaction.PsbtSummary
| undefined;
runeSummary: RuneSummary | RuneSummaryActions | undefined;
hasExternalInputs: boolean;
isUnconfirmedInput: boolean;
showCenotaphCallout: boolean;
netBtcAmount: number;
hasInsufficientRunes: boolean;
hasSigHashSingle: boolean;
transactionIsFinal: boolean;
hasOutputScript: boolean;
hasSigHashNone: boolean;
validMintingRune: undefined | boolean;
sendSection: {
showSendSection: boolean;
showBtcAmount: boolean;
showRuneTransfers: boolean;
hasInscriptionsRareSatsInOrdinal: boolean;
outputsFromOrdinal: (
| btcTransaction.TransactionOutput
| btcTransaction.TransactionPubKeyOutput
)[];
inputFromOrdinal: btcTransaction.EnhancedInput[];
inscriptionsFromPayment: btcTransaction.IOInscription[];
satributesFromPayment: btcTransaction.IOSatribute[];
};
receiveSection: {
showBtcAmount: boolean;
showOrdinalSection: boolean;
showPaymentSection: boolean;
outputsToOrdinal: btcTransaction.TransactionOutput[];
showPaymentRunes: boolean;
ordinalRuneReceipts: RuneSummary['receipts'];
inscriptionsRareSatsInPayment: btcTransaction.TransactionOutput[];
outputsToPayment: btcTransaction.TransactionOutput[];
showOrdinalRunes: boolean;
paymentRuneReceipts: RuneSummary['receipts'];
};
} => {
const { summary, runeSummary } = useContext(ParsedTxSummaryContext);
const { btcAddress, ordinalsAddress } = useSelectedAccount();
const { hasActivatedRareSatsKey } = useWalletSelector();

const showCenotaphCallout = !!summary?.runeOp?.Cenotaph?.flaws;
const hasInsufficientRunes =
runeSummary?.transfers?.some((transfer) => !transfer.hasSufficientBalance) ?? false;
const validMintingRune =
!runeSummary?.mint ||
(runeSummary?.mint && runeSummary.mint.runeIsOpen && runeSummary.mint.runeIsMintable);
const hasOutputScript = summary?.outputs.some((output) => isScriptOutput(output)) ?? false;
const hasExternalInputs =
summary?.inputs.some(
(input) =>
input.extendedUtxo.address !== btcAddress && input.extendedUtxo.address !== ordinalsAddress,
) ?? false;
const isUnconfirmedInput =
summary?.inputs.some(
(input) => !input.extendedUtxo.utxo.status.confirmed && input.walletWillSign,
) ?? false;

const netBtcAmount = getNetAmount({
inputs: summary?.inputs,
outputs: summary?.outputs,
btcAddress,
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 */

const { inputFromPayment, inputFromOrdinal } = getInputsWithAssetsFromUserAddress({
inputs: summary?.inputs,
btcAddress,
ordinalsAddress,
});

const { outputsFromPayment, outputsFromOrdinal } = getOutputsWithAssetsFromUserAddress({
outputs: summary?.outputs,
btcAddress,
ordinalsAddress,
});

// TODO move to utils
const inscriptionsFromPayment: btcTransaction.IOInscription[] = [];
const satributesFromPayment: btcTransaction.IOSatribute[] = [];
(transactionIsFinal ? outputsFromPayment : inputFromPayment).forEach(
(
item:
| btcTransaction.EnhancedInput
| btcTransaction.TransactionOutput
| btcTransaction.TransactionPubKeyOutput,
) => {
inscriptionsFromPayment.push(...item.inscriptions);
satributesFromPayment.push(...item.satributes);
},
);

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,
ordinalsAddress,
});

// if receiving runes from own addresses, hide it because it is change, unless it swap addresses (recover runes)
const filteredRuneReceipts: RuneSummary['receipts'] =
runeSummary?.receipts?.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),
);

// if transaction is not final, then runes will be delegated and will show up in the delegation section
const showOrdinalRunes = !!(transactionIsFinal && ordinalRuneReceipts.length);

// if transaction is not final, then runes will be delegated and will show up in the delegation section
const showPaymentRunes = !!(transactionIsFinal && paymentRuneReceipts.length);

return {
summary,
runeSummary,
hasExternalInputs,
hasInsufficientRunes,
hasOutputScript,
hasSigHashNone,
hasSigHashSingle,
isUnconfirmedInput,
netBtcAmount,
showCenotaphCallout,
transactionIsFinal,
validMintingRune,
sendSection: {
showSendSection: showSendBtcAmount || showRuneTransfers || hasInscriptionsRareSatsInOrdinal,
showBtcAmount: showSendBtcAmount,
showRuneTransfers,
hasInscriptionsRareSatsInOrdinal,
outputsFromOrdinal,
inputFromOrdinal,
inscriptionsFromPayment,
satributesFromPayment,
},
receiveSection: {
showOrdinalSection: showOrdinalRunes || outputsToOrdinal.length > 0,
showPaymentSection:
showReceiveBtcAmount || showPaymentRunes || inscriptionsRareSatsInPayment.length > 0,
showBtcAmount: showReceiveBtcAmount,
inscriptionsRareSatsInPayment,
ordinalRuneReceipts,
outputsToOrdinal,
outputsToPayment,
paymentRuneReceipts,
showOrdinalRunes,
showPaymentRunes,
},
};
};
58 changes: 20 additions & 38 deletions src/app/components/confirmBtcTransaction/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { delay } from '@common/utils/ledger';
import {
ParsedTxSummaryContext,
type ParsedTxSummaryContextProps,
useParsedTxSummaryContext,
} from '@components/confirmBtcTransaction/hooks/useParsedTxSummaryContext';
import TransactionSummary from '@components/confirmBtcTransaction/transactionSummary';
import type { Tab } from '@components/tabBar';
import useSelectedAccount from '@hooks/useSelectedAccount';
import TransportFactory from '@ledgerhq/hw-transport-webusb';
import type {
RuneSummary,
RuneSummaryActions,
Transport,
btcTransaction,
} from '@secretkeylabs/xverse-core';
import type { Transport } 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 { useMemo, 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',
Expand Down Expand Up @@ -49,11 +49,8 @@ const SuccessActionsContainer = styled.div((props) => ({
}));

type Props = {
inputs: btcTransaction.EnhancedInput[];
outputs: btcTransaction.EnhancedOutput[];
feeOutput?: btcTransaction.TransactionFeeOutput;
runeSummary?: RuneSummaryActions | RuneSummary;
showCenotaphCallout: boolean;
summary?: ParsedTxSummaryContextProps['summary'];
runeSummary?: ParsedTxSummaryContextProps['runeSummary'];
isLoading: boolean;
isSubmitting: boolean;
isBroadcast?: boolean;
Expand All @@ -72,20 +69,13 @@ type Props = {
) => Promise<number | undefined>;
onFeeRateSet?: (feeRate: number) => void;
feeRate?: number;
isFinal?: boolean;
// TODO: add sighash single warning
hasSigHashSingle?: boolean;
hasSigHashNone?: boolean;
title?: string;
selectedBottomTab?: Tab;
};

function ConfirmBtcTransaction({
inputs,
outputs,
feeOutput,
summary,
runeSummary,
showCenotaphCallout,
isLoading,
isSubmitting,
isBroadcast,
Expand All @@ -101,18 +91,20 @@ function ConfirmBtcTransaction({
getFeeForFeeRate,
onFeeRateSet,
feeRate,
hasSigHashNone = false,
isFinal = true,
hasSigHashSingle = false,
title,
selectedBottomTab,
}: Props) {
const parsedTxSummaryContextValue = useMemo(
() => ({ summary, runeSummary }),
[summary, runeSummary],
);
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 { hasInsufficientRunes, hasSigHashNone, validMintingRune } = useParsedTxSummaryContext();

const { t } = useTranslation('translation', { keyPrefix: 'CONFIRM_TRANSACTION' });
const { t: signatureRequestTranslate } = useTranslation('translation', {
Expand All @@ -121,17 +113,11 @@ function ConfirmBtcTransaction({
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);
};
Expand Down Expand Up @@ -189,7 +175,8 @@ function ConfirmBtcTransaction({
<Spinner size={50} />
</LoaderContainer>
) : (
<>
<ParsedTxSummaryContext.Provider value={parsedTxSummaryContextValue}>
{/* TODO start a new layout. SendLayout was not intended for the review screens */}
<SendLayout
selectedBottomTab={selectedBottomTab ?? 'dashboard'}
onClickBack={onBackClick}
Expand All @@ -200,6 +187,7 @@ function ConfirmBtcTransaction({
<ReviewTransactionText typography="headline_s">
{title || t('REVIEW_TRANSACTION')}
</ReviewTransactionText>
{/* TODO: add sighash single warning */}
{hasSigHashNone && (
<SpacedCallout
variant="danger"
Expand All @@ -209,12 +197,6 @@ function ConfirmBtcTransaction({
)}
{!isBroadcast && <SpacedCallout bodyText={t('PSBT_NO_BROADCAST_DISCLAIMER')} />}
<TransactionSummary
runeSummary={runeSummary}
inputs={inputs}
outputs={outputs}
feeOutput={feeOutput}
transactionIsFinal={isFinal}
showCenotaphCallout={showCenotaphCallout}
getFeeForFeeRate={getFeeForFeeRate}
onFeeRateSet={onFeeRateSet}
feeRate={feeRate}
Expand Down Expand Up @@ -264,7 +246,7 @@ function ConfirmBtcTransaction({
)}
</SuccessActionsContainer>
</Sheet>
</>
</ParsedTxSummaryContext.Provider>
);
}

Expand Down
Loading

0 comments on commit 0ecb8ad

Please sign in to comment.