diff --git a/src/app/common/hooks/use-bitcoin-contracts.ts b/src/app/common/hooks/use-bitcoin-contracts.ts index 13da93229b3..ae377b29e72 100644 --- a/src/app/common/hooks/use-bitcoin-contracts.ts +++ b/src/app/common/hooks/use-bitcoin-contracts.ts @@ -3,13 +3,17 @@ import { useNavigate } from 'react-router-dom'; import { RpcErrorCode } from '@btckit/types'; import { bytesToHex } from '@stacks/common'; +import { hexToBytes } from '@stacks/common'; import { JsDLCInterface } from 'dlc-tools'; +import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network'; import { deriveAddressIndexKeychainFromAccount, extractAddressIndexFromPath, + getAddressFromOutScript, } from '@shared/crypto/bitcoin/bitcoin.utils'; import { Money, createMoneyFromDecimal } from '@shared/models/money.model'; +import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model'; import { RouteUrls } from '@shared/route-urls'; import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract'; import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods'; @@ -37,7 +41,7 @@ import { useDefaultRequestParams } from './use-default-request-search-params'; export interface SimplifiedBitcoinContract { bitcoinContractId: string; bitcoinContractCollateralAmount: number; - bitcoinContractGasFee: number; + bitcoinTxDetails: BitcoinContractTransactionDetails; bitcoinContractExpirationDate: string; } @@ -60,7 +64,7 @@ export interface BitcoinContractOfferDetails { counterpartyWalletDetails: CounterpartyWalletDetails; } -type BitcoinTransaction = { +export interface RawBitcoinContractTransaction { input: { previous_output: string; script_sig: string; @@ -73,7 +77,25 @@ type BitcoinTransaction = { value: number; }[]; version: number; -}; +} + +export interface BitcoinContractInput { + txId: string; + address: string; + value: number; +} + +export interface BitcoinContractOutput { + address: string; + value: number; +} + +export interface BitcoinContractTransactionDetails { + inputs: BitcoinContractInput[]; + outputs: BitcoinContractOutput[]; + fee: number; + rawTx: RawBitcoinContractTransaction; +} export function useBitcoinContracts() { const navigate = useNavigate(); @@ -84,31 +106,59 @@ export function useBitcoinContracts() { const currentIndex = useCurrentAccountIndex(); const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex); const currentBitcoinNetwork = useCurrentNetwork(); + const btcSignerLibNetworkConfig = getBtcSignerLibNetworkConfigByMode( + currentBitcoinNetwork.chain.bitcoin.network + ); const bitcoinClient = useBitcoinClient(); const [bitcoinContractCollateralAmount, setBitcoinContractCollateralAmount] = useState(0); const [acceptedBitcoinContract, setAcceptedBitcoinContract] = useState(); const [counterpartyWalletDetails, setCounterpartyWalletDetails] = useState(); - async function calculateFee(fundingTX: BitcoinTransaction) { + async function getTxInputDetails(txId: string, outputIndex: number) { + const bitcoinTx: BitcoinTx = await bitcoinClient.transactionsApi.getBitcoinTransaction(txId); + const bitcoinAddress = bitcoinTx.vout[outputIndex].scriptpubkey_address; + const inputAmount = bitcoinTx.vout[outputIndex].value; + return { bitcoinAddress, inputAmount }; + } + + async function getBitcoinTxDetails(fundingTX: RawBitcoinContractTransaction) { const inputs = fundingTX.input; const outputs = fundingTX.output; - let outputAmount = 0; - let inputAmount = 0; + let inputValueSum = 0; + let outputValueSum = 0; + + const bitcoinTxDetails: BitcoinContractTransactionDetails = { + inputs: [], + outputs: [], + fee: 0, + rawTx: fundingTX, + }; for (const input of inputs) { const [txId, outputIndex] = input.previous_output.split(':'); - const txDetails = await bitcoinClient.transactionsApi.getBitcoinTransaction(txId); - inputAmount += parseInt(txDetails.vout[parseInt(outputIndex)].value); + const { bitcoinAddress, inputAmount } = await getTxInputDetails(txId, parseInt(outputIndex)); + const txInput = { txId, address: bitcoinAddress, value: inputAmount }; + bitcoinTxDetails.inputs.push(txInput); + inputValueSum += inputAmount; } for (const output of outputs) { - outputAmount += output.value; + const outputAmount = output.value; + const outputAddress = getAddressFromOutScript( + hexToBytes(output.script_pubkey), + btcSignerLibNetworkConfig + ); + const txOutput = { address: outputAddress, value: outputAmount }; + bitcoinTxDetails.outputs.push(txOutput); + outputValueSum += outputAmount; } - const fee = inputAmount - outputAmount; + const fee = inputValueSum - outputValueSum; + + bitcoinTxDetails.fee = fee; - return fee; + return bitcoinTxDetails; } async function getBitcoinContractInterface(): Promise { @@ -150,7 +200,11 @@ export function useBitcoinContracts() { const bitcoinContractCollateralAmount = bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral; - let bitcoinContractGasFee = 0; + let bitcoinTxDetails: BitcoinContractTransactionDetails = { + inputs: [], + outputs: [], + fee: 0, + }; const bitcoinContractExpirationDate = new Date( bitcoinContractOffer.cetLocktime * 1000 @@ -183,7 +237,7 @@ export function useBitcoinContracts() { const acceptedBitcoinContract = JSON.parse(acceptedBitcoinContractJSON); - bitcoinContractGasFee = await calculateFee(acceptedBitcoinContract.fundingTX); + bitcoinTxDetails = await getBitcoinTxDetails(acceptedBitcoinContract.fundingTX); setAcceptedBitcoinContract(acceptedBitcoinContract); } catch (error) { @@ -201,7 +255,7 @@ export function useBitcoinContracts() { bitcoinContractId, bitcoinContractCollateralAmount, bitcoinContractExpirationDate, - bitcoinContractGasFee, + bitcoinTxDetails, }; const bitcoinContractOfferDetails: BitcoinContractOfferDetails = { diff --git a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx index bd58ff01a49..ea424ad5111 100644 --- a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx +++ b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx @@ -1,44 +1,48 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { token } from 'leather-styles/tokens'; - +import { createMoney } from '@shared/models/money.model'; import { RouteUrls } from '@shared/route-urls'; import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract'; +import { closeWindow } from '@shared/utils'; -import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts'; -import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts'; +import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance'; +import { + BitcoinContractOfferDetails, + useBitcoinContracts, +} from '@app/common/hooks/use-bitcoin-contracts'; import { useOnMount } from '@app/common/hooks/use-on-mount'; -import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { initialSearchParams } from '@app/common/initial-search-params'; -import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer'; -import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-card/info-card'; - -import { BitcoinContractOfferInput } from './components/bitcoin-contract-offer/bitcoin-contract-offer-input'; -import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions'; -import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout'; -import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; +import { initialSearchParams } from '@app/common/initial-search-params'; +import { LoadingSpinner } from '@app/components/loading-spinner'; import { PopupHeader } from '@app/features/current-account/popup-header'; import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed'; -import { closeWindow } from '@shared/utils'; -import { LoadingSpinner } from '@app/components/loading-spinner'; +import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; -import { Stack } from 'leather-styles/jsx'; -import { LeatherButton } from '@app/components/button/button'; + +import { BitcoinContractCollateralAmount } from './components/bitcoin-contract-collateral-amount'; +import { BitcoinContractExpirationDate } from './components/bitcoin-contract-expiration-date'; +import { BitcoinContractFee } from './components/bitcoin-contract-fee'; +import { BitcoinContractInputsAndOutputs } from './components/bitcoin-contract-inputs-outputs'; +import { BitcoinContractRequestRawTransaction } from './components/bitcoin-contract-raw-transaction'; +import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions'; +import { BitcoinContractRequestDetails } from './components/bitcoin-contract-request-details'; +import { BitcoinContractRequestDetailsHeader } from './components/bitcoin-contract-request-details-header'; +import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header'; +import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout'; export function BitcoinContractRequest() { const navigate = useNavigate(); const network = useCurrentNetwork(); const { address: bitcoinAddress } = useCurrentAccountNativeSegwitIndexZeroSigner(); const { handleOffer, handleSigning, handleReject, sendRpcResponse } = useBitcoinContracts(); + const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress); - const [requiredAmount, setRequiredAmount] = useState(0); const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] = useState(); const [isProcessing, setProcessing] = useState(false); - const [showTransactionDetails, setShowTransactionDetails] = useState(false); - + const [canAccept, setCanAccept] = useState(false); + useRouteHeader(); useOnOriginTabClose(() => closeWindow()); @@ -76,91 +80,73 @@ export function BitcoinContractRequest() { counterpartyWalletDetailsJSON ); if (!currentBitcoinContractOfferDetails) return; - setRequiredAmount( - currentBitcoinContractOfferDetails?.simplifiedBitcoinContract - .bitcoinContractCollateralAmount + - currentBitcoinContractOfferDetails?.simplifiedBitcoinContract.bitcoinContractGasFee + + setCanAccept( + btcAvailableAssetBalance.balance.amount.isGreaterThan( + currentBitcoinContractOfferDetails.simplifiedBitcoinContract + .bitcoinContractCollateralAmount + + currentBitcoinContractOfferDetails?.simplifiedBitcoinContract.bitcoinTxDetails.fee + ) ); setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails); }; handleBitcoinContractOffer(); }); - if (!bitcoinContractOfferDetails || !bitcoinContractOfferDetails.counterpartyWalletDetails - .counterpartyWalletAddress) return ; + if ( + !bitcoinContractOfferDetails || + !bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletAddress + ) + return ; return ( <> - - + + + + + + - - - - setShowTransactionDetails(!showTransactionDetails)} - > - {showTransactionDetails ? 'Hide Transaction Details' : 'Show Transaction Details'} - - - {showTransactionDetails && ( - - - - } - /> - - - - - - - )} - + + + ); } diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-collateral-amount.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-collateral-amount.tsx new file mode 100644 index 00000000000..7b9f6546442 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-collateral-amount.tsx @@ -0,0 +1,39 @@ +import { Stack } from '@stacks/ui'; +import { truncateMiddle } from '@stacks/ui-utils'; +import { HStack, styled } from 'leather-styles/jsx'; + +import { Money } from '@shared/models/money.model'; + +import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money'; +import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks'; + +import { BitcoinContractRequestDetailsSectionLayout } from './bitcoin-contract-request-details-section-layout'; + +interface BitcoinContractCollateralAmountProps { + bitcoinAddress: string; + collateralAmount: Money; +} +export function BitcoinContractCollateralAmount({ + bitcoinAddress, + collateralAmount, +}: BitcoinContractCollateralAmountProps) { + const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue(); + return ( + + + + Collateral Amount + + {truncateMiddle(bitcoinAddress)} + + + + {formatMoney(collateralAmount)} + + {i18nFormatCurrency(calculateBitcoinFiatValue(collateralAmount))} + + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-expiration-date.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-expiration-date.tsx new file mode 100644 index 00000000000..1436e132bdf --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-expiration-date.tsx @@ -0,0 +1,18 @@ +import { HStack, Stack, styled } from 'leather-styles/jsx'; + +import { BitcoinContractRequestDetailsSectionLayout } from './bitcoin-contract-request-details-section-layout'; + +export function BitcoinContractExpirationDate(props: { expirationDate: string }) { + const { expirationDate } = props; + + return ( + + + Expiration Date + + {expirationDate} + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-fee.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-fee.tsx new file mode 100644 index 00000000000..e8a2f92715f --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-fee.tsx @@ -0,0 +1,27 @@ +import { HStack, Stack, styled } from 'leather-styles/jsx'; + +import { Money } from '@shared/models/money.model'; + +import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money'; +import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks'; + +import { BitcoinContractRequestDetailsSectionLayout } from './bitcoin-contract-request-details-section-layout'; + +export function BitcoinContractFee(props: { fee: Money }) { + const { fee } = props; + const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue(); + + return ( + + + Transaction fee + + {formatMoney(fee)} + + {i18nFormatCurrency(calculateBitcoinFiatValue(fee))} + + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-item.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-item.tsx new file mode 100644 index 00000000000..5f9043e2749 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-item.tsx @@ -0,0 +1,20 @@ +import { truncateMiddle } from '@stacks/ui-utils'; + +import { createMoney } from '@shared/models/money.model'; + +import { BitcoinContractInput } from '@app/common/hooks/use-bitcoin-contracts'; +import { formatMoney } from '@app/common/money/format-money'; + +import { BitcoinContractInputOutputItemLayout } from './bitcoin-contract-input-output-item.layout'; + +export function BitcoinContractInputItem({ utxo }: { utxo: BitcoinContractInput }) { + return ( + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-list.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-list.tsx new file mode 100644 index 00000000000..651d08a58e2 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-list.tsx @@ -0,0 +1,14 @@ +import { BitcoinContractInput } from '@app/common/hooks/use-bitcoin-contracts'; + +import { BitcoinContractInputItem } from './bitcoin-contract-input-item'; +import { BitcoinContractInputListLayout } from './bitcoin-contract-inputs-list-layout'; + +export function BitcoinContractInputList({ inputs }: { inputs: BitcoinContractInput[] }) { + return ( + + {inputs.map((input, i) => ( + + ))} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-output-item.layout.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-output-item.layout.tsx new file mode 100644 index 00000000000..584a8c86042 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-input-output-item.layout.tsx @@ -0,0 +1,77 @@ +import { Box, Flex, HStack, styled } from 'leather-styles/jsx'; + +import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; +import { useExplorerLink } from '@app/common/hooks/use-explorer-link'; +import { LeatherButton } from '@app/components/button/button'; +import { CopyIcon } from '@app/components/icons/copy-icon'; +import { Flag } from '@app/components/layout/flag'; +import { Tooltip } from '@app/components/tooltip'; + +interface BitcoinContractInputOutputItemLayoutProps { + address: string; + addressHoverLabel?: string; + amount: string; + txId?: string; + txIdHoverLabel?: string; +} +export function BitcoinContractInputOutputItemLayout({ + address, + addressHoverLabel, + amount, + txId, + txIdHoverLabel, +}: BitcoinContractInputOutputItemLayoutProps) { + const { onCopy, hasCopied } = useClipboard(addressHoverLabel ?? ''); + const { handleOpenTxLink } = useExplorerLink(); + + return ( + } mt="loose" spacing="base"> + + + + {address} + + + + + {addressHoverLabel ? : null} + + + + + {amount} + + + {txId && txIdHoverLabel ? ( + + handleOpenTxLink({ + blockchain: 'bitcoin', + txId: txIdHoverLabel ?? '', + }) + } + variant="text" + > + + {txId} + + + ) : null} + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-list-layout.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-list-layout.tsx new file mode 100644 index 00000000000..73eb1d82e54 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-list-layout.tsx @@ -0,0 +1,11 @@ +import { Box } from 'leather-styles/jsx'; + +import { HasChildren } from '@app/common/has-children'; + +export function BitcoinContractInputListLayout({ children }: HasChildren) { + return ( + + {children} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-outputs.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-outputs.tsx new file mode 100644 index 00000000000..29ffb2f2791 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-inputs-outputs.tsx @@ -0,0 +1,41 @@ +import { useState } from 'react'; + +import { + BitcoinContractInput, + BitcoinContractOutput, +} from '@app/common/hooks/use-bitcoin-contracts'; + +import { BitcoinContractInputList } from './bitcoin-contract-input-list'; +import { BitcoinContractOutputList } from './bitcoin-contract-output-list'; +import { BitcoinContractRequestDetailsSectionHeader } from './bitcoin-contract-request-details-section-header'; +import { BitcoinContractRequestDetailsSectionLayout } from './bitcoin-contract-request-details-section-layout'; + +interface BitcoinContractInputsOutputsProps { + inputs: BitcoinContractInput[]; + outputs: BitcoinContractOutput[]; +} + +export function BitcoinContractInputsAndOutputs(props: BitcoinContractInputsOutputsProps) { + const { inputs, outputs } = props; + const [showDetails, setShowDetails] = useState(false); + + if (!inputs.length || !outputs.length) return null; + + return ( + + setShowDetails(value)} + showDetails={showDetails} + title={showDetails ? 'Inputs' : 'Inputs and Outputs'} + /> + {showDetails ? ( + <> + + + + + ) : null} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx deleted file mode 100644 index 01d19d16a1a..00000000000 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Flex } from '@stacks/ui'; -import { BitcoinContractRequestSelectors } from '@tests/selectors/bitcoin-contract-request.selectors'; - -import { Text } from '@app/components/typography'; - -interface BitcoinContractExpirationDateProps { - expirationDate: string; -} -export function BitcoinContractExpirationDate({ - expirationDate, -}: BitcoinContractExpirationDateProps) { - return ( - - - Expiration Date - - - {expirationDate} - - - ); -} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx deleted file mode 100644 index 41e38a07ec2..00000000000 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { FiArrowUpRight, FiCopy } from 'react-icons/fi'; - -import { Box, Stack, Text, color } from '@stacks/ui'; -import { BitcoinContractRequestSelectors } from '@tests/selectors/bitcoin-contract-request.selectors'; -import { HStack } from 'leather-styles/jsx'; - -import { useClipboard } from '@app/common/hooks/use-copy-to-clipboard'; -import { BtcIcon } from '@app/components/icons/btc-icon'; -import { Flag } from '@app/components/layout/flag'; -import { Tooltip } from '@app/components/tooltip'; - -interface BitcoinContractLockAmountProps { - hoverLabel?: string; - image?: JSX.Element; - subtitle?: string; - subValue?: string; - subValueAction?(): void; - title?: string; - value: string; -} -export function BitcoinContractLockAmount({ - hoverLabel, - image, - subtitle, - subValue, - subValueAction, - title, - value, -}: BitcoinContractLockAmountProps) { - const { onCopy, hasCopied } = useClipboard(hoverLabel ?? ''); - - return ( - } align="middle" width="100%"> - - - {title ? title : 'BTC'} - - - {value} - - - - {subtitle ? ( - - - - {subtitle} - - {hoverLabel ? : null} - - - ) : null} - {subValue ? ( - - - {subValue} - - {subValueAction ? : null} - - ) : null} - - - ); -} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx deleted file mode 100644 index 2401bb70009..00000000000 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts'; - -import { BitcoinContractExpirationDate } from './bitcoin-contract-expiration-date'; -import { BitcoinContractOfferInput } from './bitcoin-contract-offer-input'; - -interface BitcoinContractOfferDetailsSimpleProps { - bitcoinAddress: string; - bitcoinContractOffer: SimplifiedBitcoinContract; -} -export function BitcoinContractOfferDetailsSimple({ - bitcoinAddress, - bitcoinContractOffer, -}: BitcoinContractOfferDetailsSimpleProps) { - return ( - - ); -} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx deleted file mode 100644 index 5e1e329c10e..00000000000 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Stack, Text } from '@stacks/ui'; -import { truncateMiddle } from '@stacks/ui-utils'; - -import { createMoneyFromDecimal } from '@shared/models/money.model'; - -import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts'; -import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money'; -import { satToBtc } from '@app/common/money/unit-conversion'; -import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks'; - -import { BitcoinContractLockAmount } from './bitcoin-contract-lock-amount'; - -interface BitcoinContractOfferInputProps { - addressNativeSegwit: string; - bitcoinContractOffer: SimplifiedBitcoinContract; -} -export function BitcoinContractOfferInput({ - addressNativeSegwit, - bitcoinContractOffer, -}: BitcoinContractOfferInputProps) { - const calculateFiatValue = useCalculateBitcoinFiatValue(); - - const bitcoinValue = satToBtc(bitcoinContractOffer.bitcoinContractCollateralAmount); - const money = createMoneyFromDecimal(bitcoinValue, 'BTC'); - const fiatValue = calculateFiatValue(money); - const formattedBitcoinValue = formatMoney(money); - const formattedFiatValue = i18nFormatCurrency(fiatValue); - - return ( - - Amount - - - ); -} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-item.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-item.tsx new file mode 100644 index 00000000000..cea99275e87 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-item.tsx @@ -0,0 +1,22 @@ +import { truncateMiddle } from '@stacks/ui-utils'; + +import { createMoney } from '@shared/models/money.model'; + +import { BitcoinContractOutput } from '@app/common/hooks/use-bitcoin-contracts'; +import { formatMoney } from '@app/common/money/format-money'; + +import { BitcoinContractInputOutputItemLayout } from './bitcoin-contract-input-output-item.layout'; + +export function BitcoinContractOutputItem({ output }: { output: BitcoinContractOutput }) { + const isUnknownAddress = output.address === ''; + + if (isUnknownAddress) return null; + + return ( + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.layout.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.layout.tsx new file mode 100644 index 00000000000..22da093b415 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.layout.tsx @@ -0,0 +1,11 @@ +import { Box } from 'leather-styles/jsx'; + +import { HasChildren } from '@app/common/has-children'; + +export function BitcoinContractOutputListLayout({ children }: HasChildren) { + return ( + + {children} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.tsx new file mode 100644 index 00000000000..89675c2d2ea --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-output-list.tsx @@ -0,0 +1,14 @@ +import { BitcoinContractOutput } from '@app/common/hooks/use-bitcoin-contracts'; + +import { BitcoinContractOutputItem } from './bitcoin-contract-output-item'; +import { BitcoinContractOutputListLayout } from './bitcoin-contract-output-list.layout'; + +export function BitcoinContractOutputList({ outputs }: { outputs: BitcoinContractOutput[] }) { + return ( + + {outputs.map((output, i) => ( + + ))} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-raw-transaction.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-raw-transaction.tsx new file mode 100644 index 00000000000..528b855c3b7 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-raw-transaction.tsx @@ -0,0 +1,27 @@ +import { useState } from 'react'; + +import { RawBitcoinContractTransaction } from '@app/common/hooks/use-bitcoin-contracts'; +import { Json } from '@app/components/json'; + +import { BitcoinContractRequestDetailsSectionHeader } from './bitcoin-contract-request-details-section-header'; +import { BitcoinContractRequestDetailsSectionLayout } from './bitcoin-contract-request-details-section-layout'; + +export function BitcoinContractRequestRawTransaction({ + bitcoinContractTransaction, +}: { + bitcoinContractTransaction: RawBitcoinContractTransaction; +}) { + const [showDetails, setShowDetails] = useState(false); + + return ( + + setShowDetails(value)} + showDetails={showDetails} + title="Raw transaction" + /> + {showDetails ? : null} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx index 742077d00d8..eefeeceb161 100644 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx @@ -1,58 +1,49 @@ -import { Box, Button, Stack, color } from '@stacks/ui'; import { BitcoinContractRequestSelectors } from '@tests/selectors/bitcoin-contract-request.selectors'; +import { Box, HStack } from 'leather-styles/jsx'; -import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance'; import { LeatherButton } from '@app/components/button/button'; interface BitcoinContractRequestActionsProps { isLoading: boolean; - bitcoinAddress: string; - requiredAmount: number; + canAccept: boolean; onRejectBitcoinContractOffer(): Promise; onAcceptBitcoinContractOffer(): Promise; } export function BitcoinContractRequestActions({ isLoading, - bitcoinAddress, - requiredAmount, + canAccept, onRejectBitcoinContractOffer, onAcceptBitcoinContractOffer, }: BitcoinContractRequestActionsProps) { - const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress); - const canAccept = btcAvailableAssetBalance.balance.amount.isGreaterThan(requiredAmount); - return ( - - + Cancel + - Accept + Sign - + ); } diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-header.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-header.tsx new file mode 100644 index 00000000000..4a53c2816aa --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-header.tsx @@ -0,0 +1,36 @@ +import { Box, Stack } from '@stacks/ui'; +import { styled } from 'leather-styles/jsx'; +import { token } from 'leather-styles/tokens'; + +import { LockIcon } from '@app/components/icons/lock-icon'; +import { Tooltip } from '@app/components/tooltip'; + +const immutableLabel = + 'Any modification to the transaction, including the fee amount or other inputs/outputs, will invalidate the signature.'; + +export function BitcoinContractRequestDetailsHeader() { + return ( + + Transaction + + + + + + + Certain + + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-header.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-header.tsx new file mode 100644 index 00000000000..8410f3af58b --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-header.tsx @@ -0,0 +1,34 @@ +import { HStack, styled } from 'leather-styles/jsx'; + +import { LeatherButton } from '@app/components/button/button'; +import { ArrowUpIcon } from '@app/components/icons/arrow-up-icon'; + +interface BitcoinContractRequestDetailsSectionHeaderProps { + hasDetails?: boolean; + onSetShowDetails?(value: boolean): void; + showDetails?: boolean; + title: string; +} +export function BitcoinContractRequestDetailsSectionHeader({ + hasDetails, + onSetShowDetails, + showDetails, + title, +}: BitcoinContractRequestDetailsSectionHeaderProps) { + return ( + + {title} + {hasDetails && onSetShowDetails ? ( + onSetShowDetails(!showDetails)} variant="text"> + {showDetails ? ( + + See less + + ) : ( + 'See details' + )} + + ) : null} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-layout.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-layout.tsx new file mode 100644 index 00000000000..b6c0201536b --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details-section-layout.tsx @@ -0,0 +1,14 @@ +import { Stack, StackProps } from 'leather-styles/jsx'; + +import { HasChildren } from '@app/common/has-children'; + +export function BitcoinContractRequestDetailsSectionLayout({ + children, + ...props +}: HasChildren & StackProps) { + return ( + + {children} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details.tsx new file mode 100644 index 00000000000..45229b7dc13 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-details.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'leather-styles/jsx'; + +import { HasChildren } from '@app/common/has-children'; + +export function BitcoinContractRequestDetails({ children }: HasChildren) { + return ( + + {children} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx deleted file mode 100644 index 983c910daea..00000000000 --- a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { BitcoinContractRequestSelectors } from '@tests/selectors/bitcoin-contract-request.selectors'; - -import { WarningLabel } from '@app/components/warning-label'; - -export function BitcoinContractRequestWarningLabel(props: { appName?: string }) { - const { appName } = props; - const title = `Do not proceed unless you trust ${appName ?? 'Unknown'}!`; - - return ( - - By signing the contract YOU AGREE TO LOCK YOUR BITCOIN with {appName} into a contract where it - will remain until a triggering event will release it. - - ); -}