feat: added bitcoin contract request page

This commit is contained in:
Polybius93
2023-06-27 12:14:01 +02:00
committed by kyranjamie
parent abffd71765
commit 83cf0491e4
9 changed files with 399 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
import { useState } from 'react';
import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts';
import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { initialSearchParams } from '@app/common/initial-search-params';
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { BitcoinContractOfferDetailsSimple } from './components/bitcoin-contract-offer/bitcoin-contract-offer-details';
import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions';
import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header';
import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout';
import { BitcoinContractRequestWarningLabel } from './components/bitcoin-contract-request-warning-label';
export function BitcoinContractRequest() {
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const { handleOffer, handleAccept, handleReject } = useBitcoinContracts();
const [bitcoinContractJSON, setBitcoinContractJSON] = useState<string>();
const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] =
useState<BitcoinContractOfferDetails>();
const [bitcoinAddress, setBitcoinAddress] = useState<string>();
const [isLoading, setLoading] = useState(true);
const handleAcceptClick = async () => {
if (!bitcoinContractJSON || !bitcoinContractOfferDetails) return;
await handleAccept(bitcoinContractJSON, bitcoinContractOfferDetails.counterpartyWalletDetails);
};
const handleRejectClick = async () => {
if (!bitcoinContractOfferDetails) return;
await handleReject(bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractId);
};
useOnMount(() => {
const bitcoinContractOfferJSON = initialSearchParams.get('bitcoinContractOffer');
const counterpartyWalletURL = initialSearchParams.get('counterpartyWalletURL');
const counterpartyWalletName = initialSearchParams.get('counterpartyWalletName');
const counterpartyWalletIcon = initialSearchParams.get('counterpartyWalletIcon');
if (
!getNativeSegwitSigner ||
!bitcoinContractOfferJSON ||
!counterpartyWalletURL ||
!counterpartyWalletName ||
!counterpartyWalletIcon
)
return;
const currentBitcoinContractOfferDetails = handleOffer(
bitcoinContractOfferJSON,
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon
);
const currentAddress = getNativeSegwitSigner(0).address;
setBitcoinContractJSON(bitcoinContractOfferJSON);
setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails);
setBitcoinAddress(currentAddress);
setLoading(false);
});
return (
<>
{!isLoading && bitcoinAddress && bitcoinContractOfferDetails && (
<BitcoinContractRequestLayout>
<BitcoinContractRequestHeader
counterpartyWalletName={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName
}
counterpartyWalletIcon={
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletIcon
}
/>
<BitcoinContractRequestWarningLabel
appName={bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName}
/>
<BitcoinContractRequestActions
isLoading={isLoading}
bitcoinAddress={bitcoinAddress}
requiredAmount={
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractCollateralAmount
}
onRejectBitcoinContractOffer={handleRejectClick}
onAcceptBitcoinContractOffer={handleAcceptClick}
/>
<BitcoinContractOfferDetailsSimple
bitcoinAddress={bitcoinAddress}
bitcoinContractOffer={bitcoinContractOfferDetails.simplifiedBitcoinContract}
/>
</BitcoinContractRequestLayout>
)}
</>
);
}

View File

@@ -0,0 +1,24 @@
import { Flex } from '@stacks/ui';
import { SpaceBetween } from '@app/components/layout/space-between';
import { Text } from '@app/components/typography';
interface BitcoinContractExpirationDateProps {
expirationDate: string;
}
export function BitcoinContractExpirationDate({
expirationDate,
}: BitcoinContractExpirationDateProps) {
return (
<Flex flex={1} width={'284.16px'} paddingLeft={'24px'} paddingRight={'24px'}>
<SpaceBetween width={'100%'}>
<Text fontSize={2} fontWeight="500">
Expiration Date
</Text>
<Text fontSize={1} textAlign="right">
{expirationDate}
</Text>
</SpaceBetween>
</Flex>
);
}

View File

@@ -0,0 +1,76 @@
import { FiArrowUpRight, FiCopy } from 'react-icons/fi';
import { Box, Stack, Text, color, useClipboard } from '@stacks/ui';
import { BtcIcon } from '@app/components/icons/btc-icon';
import { Flag } from '@app/components/layout/flag';
import { SpaceBetween } from '@app/components/layout/space-between';
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 (
<Flag align="middle" img={image ? image : <BtcIcon />} my="loose" spacing="base">
<SpaceBetween>
<Text fontSize={2} fontWeight="500">
{title ? title : 'BTC'}
</Text>
<Text fontSize={2} fontWeight="500">
{value}
</Text>
</SpaceBetween>
<SpaceBetween mt="tight">
{subtitle ? (
<Tooltip
disabled={!hoverLabel}
hideOnClick={false}
label={hasCopied ? 'Copied!' : hoverLabel}
labelProps={{ wordWrap: 'break-word' }}
maxWidth="230px"
placement="bottom"
>
<Box
_hover={{ cursor: 'pointer' }}
as="button"
color={color('text-caption')}
display="flex"
onClick={onCopy}
type="button"
>
<Text color={color('text-caption')} fontSize={1} mr="extra-tight">
{subtitle}
</Text>
{hoverLabel ? <FiCopy size="14px" /> : null}
</Box>
</Tooltip>
) : null}
{subValue ? (
<Stack as="button" isInline onClick={subValueAction} spacing="extra-tight" type="button">
<Text color={subValueAction ? color('accent') : color('text-caption')} fontSize={1}>
{subValue}
</Text>
{subValueAction ? <FiArrowUpRight color={color('accent')} /> : null}
</Stack>
) : null}
</SpaceBetween>
</Flag>
);
}

View File

@@ -0,0 +1,25 @@
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 (
<>
<BitcoinContractOfferInput
addressNativeSegwit={bitcoinAddress}
bitcoinContractOffer={bitcoinContractOffer}
/>
<BitcoinContractExpirationDate
expirationDate={bitcoinContractOffer.bitcoinContractExpirationDate}
></BitcoinContractExpirationDate>
</>
);
}

View File

@@ -0,0 +1,48 @@
import { Box, 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 (
<Box
background="white"
borderBottomLeftRadius={'16px'}
borderBottomRightRadius={'16px'}
borderTopLeftRadius={'16px'}
borderTopRightRadius={'16px'}
p="loose"
>
<Text fontWeight={500}>Amount</Text>
<BitcoinContractLockAmount
hoverLabel={addressNativeSegwit}
subtitle={truncateMiddle(addressNativeSegwit)}
subValue={`${formattedFiatValue} USD`}
value={formattedBitcoinValue}
/>
<hr />
</Box>
);
}

View File

@@ -0,0 +1,55 @@
import { Box, Button, Stack, color } from '@stacks/ui';
import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance';
import { PrimaryButton } from '@app/components/primary-button';
interface BitcoinContractRequestActionsProps {
isLoading: boolean;
bitcoinAddress: string;
requiredAmount: number;
onRejectBitcoinContractOffer(): Promise<void>;
onAcceptBitcoinContractOffer(): Promise<void>;
}
export function BitcoinContractRequestActions({
isLoading,
bitcoinAddress,
requiredAmount,
onRejectBitcoinContractOffer,
onAcceptBitcoinContractOffer,
}: BitcoinContractRequestActionsProps) {
const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress);
const canAccept = btcAvailableAssetBalance.balance.amount.isGreaterThan(requiredAmount);
return (
<Box
bg={color('bg')}
borderTop="1px solid #DCDDE2"
bottom="0px"
height="96px"
position="absolute"
px="loose"
width="100%"
zIndex={999}
>
<Stack isInline mt="loose" spacing="base">
<Button
borderRadius="10px"
flexGrow={1}
mode="tertiary"
onClick={onRejectBitcoinContractOffer}
>
Reject
</Button>
<PrimaryButton
borderRadius="10px"
flexGrow={1}
isLoading={isLoading}
isDisabled={!canAccept}
onClick={onAcceptBitcoinContractOffer}
>
Accept
</PrimaryButton>
</Stack>
</Box>
);
}

View File

@@ -0,0 +1,37 @@
import { memo } from 'react';
import { Flex } from '@stacks/ui';
import { Flag } from '@app/components/layout/flag';
import { Caption, Title } from '@app/components/typography';
interface BitcoinContractRequestHeaderBaseProps {
counterpartyWalletIcon: string;
counterpartyWalletName: string;
}
function BitcoinContractRequestHeaderBase({
counterpartyWalletName,
counterpartyWalletIcon,
}: BitcoinContractRequestHeaderBaseProps) {
const caption = `${counterpartyWalletName} is requesting you accept this offer`;
return (
<Flex flexDirection="column" my="loose" width="100%">
<Title fontSize={3} fontWeight={500} mb="base">
Lock Bitcoin
</Title>
{caption && (
<Flag
align="middle"
img={<img src={counterpartyWalletIcon} height="32px" width="32px" />}
pl="tight"
>
<Caption wordBreak="break-word">{caption}</Caption>
</Flag>
)}
</Flex>
);
}
export const BitcoinContractRequestHeader = memo(BitcoinContractRequestHeaderBase);

View File

@@ -0,0 +1,20 @@
import { Stack } from '@stacks/ui';
interface BitcoinContractRequestLayoutProps {
children: React.ReactNode;
}
export function BitcoinContractRequestLayout({ children }: BitcoinContractRequestLayoutProps) {
return (
<Stack
alignItems="center"
maxHeight="calc(100vh - 72px)"
overflowY="scroll"
pb="120px"
px="loose"
spacing="tight"
width="100%"
>
{children}
</Stack>
);
}

View File

@@ -0,0 +1,13 @@
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 (
<WarningLabel title={title} width="100%">
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.
</WarningLabel>
);
}