mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 22:53:27 +08:00
feat: added bitcoin contract request page
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user