Merge pull request #4041 from hirosystems/release/dlc-link

Release/dlc link
This commit is contained in:
Fara Woolf
2023-07-25 08:41:18 -05:00
committed by GitHub
33 changed files with 992 additions and 107 deletions

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ trace*
/test-results/
/playwright-report/
/playwright/.cache/
# Local Netlify folder
.netlify

File diff suppressed because one or more lines are too long

View File

@@ -141,9 +141,9 @@
"@scure/bip32": "1.3.0",
"@scure/bip39": "1.2.1",
"@scure/btc-signer": "1.0.1",
"@segment/analytics-next": "1.53.0",
"@sentry/react": "7.57.0",
"@sentry/tracing": "7.57.0",
"@segment/analytics-next": "1.53.1",
"@sentry/react": "7.59.2",
"@sentry/tracing": "7.59.2",
"@stacks/auth": "6.5.4",
"@stacks/blockchain-api-client": "6.3.4",
"@stacks/common": "6.5.2",
@@ -183,6 +183,7 @@
"coinselect": "3.1.13",
"compare-versions": "4.1.3",
"dayjs": "1.11.8",
"dlc-wasm-wallet": "0.4.3",
"dompurify": "3.0.4",
"downshift": "6.1.7",
"ecdsa-sig-formatter": "1.0.11",

View File

@@ -9,6 +9,10 @@ export function useShowFieldError(name: string) {
const form = useFormikContext();
const [_, meta] = useField(name);
const isDirty = useIsFieldDirty(name);
const isFieldInFocus = document.activeElement?.getAttribute('name') === name;
return (form.submitCount > 0 && meta.error) || (meta.touched && isDirty && meta.error);
return (
(form.submitCount > 0 && meta.error) ||
(!isFieldInFocus && meta.touched && isDirty && meta.error)
);
}

View File

@@ -0,0 +1,239 @@
import { useNavigate } from 'react-router-dom';
import { bytesToHex } from '@stacks/common';
import { JsDLCInterface } from 'dlc-wasm-wallet';
import { BITCOIN_API_BASE_URL_MAINNET, BITCOIN_API_BASE_URL_TESTNET } from '@shared/constants';
import { deriveAddressIndexKeychainFromAccount } from '@shared/crypto/bitcoin/bitcoin.utils';
import { createMoneyFromDecimal } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
import {
useCalculateBitcoinFiatValue,
useCryptoCurrencyMarketData,
} from '@app/query/common/market-data/market-data.hooks';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import {
useCurrentAccountNativeSegwitSigner,
useNativeSegwitActiveNetworkAccountPrivateKeychain,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { initialSearchParams } from '../initial-search-params';
import { i18nFormatCurrency } from '../money/format-money';
import { satToBtc } from '../money/unit-conversion';
import { whenBitcoinNetwork } from '../utils';
import { useDefaultRequestParams } from './use-default-request-search-params';
export interface SimplifiedBitcoinContract {
bitcoinContractId: string;
bitcoinContractCollateralAmount: number;
bitcoinContractExpirationDate: string;
}
interface CounterpartyWalletDetails {
counterpartyWalletURL: string;
counterpartyWalletName: string;
counterpartyWalletIcon: string;
}
export interface BitcoinContractOfferDetails {
simplifiedBitcoinContract: SimplifiedBitcoinContract;
counterpartyWalletDetails: CounterpartyWalletDetails;
}
export function useBitcoinContracts() {
const navigate = useNavigate();
const defaultParams = useDefaultRequestParams();
const bitcoinMarketData = useCryptoCurrencyMarketData('BTC');
const calculateFiatValue = useCalculateBitcoinFiatValue();
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const currentIndex = useCurrentAccountIndex();
const nativeSegwitPrivateKeychain =
useNativeSegwitActiveNetworkAccountPrivateKeychain()?.(currentIndex);
const oracleAPI = 'https://testnet.dlc.link/oracle';
async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
if (!nativeSegwitPrivateKeychain || !bitcoinAccountDetails) return;
const currentBitcoinNetwork = bitcoinAccountDetails.network;
const currentAddress = bitcoinAccountDetails.address;
const currentAccountIndex = bitcoinAccountDetails.addressIndex;
const currentAddressPrivateKey = deriveAddressIndexKeychainFromAccount(
nativeSegwitPrivateKeychain.keychain
)(currentAccountIndex).privateKey;
if (!currentAddressPrivateKey) return;
const blockchainAPI = whenBitcoinNetwork(currentBitcoinNetwork)({
mainnet: BITCOIN_API_BASE_URL_MAINNET,
testnet: BITCOIN_API_BASE_URL_TESTNET,
regtest: BITCOIN_API_BASE_URL_TESTNET,
signet: BITCOIN_API_BASE_URL_TESTNET,
});
const bitcoinContractInterface = JsDLCInterface.new(
bytesToHex(currentAddressPrivateKey),
currentAddress,
currentBitcoinNetwork,
blockchainAPI,
oracleAPI
);
return bitcoinContractInterface;
}
function handleOffer(
bitcoinContractOfferJSON: string,
counterpartyWalletURL: string,
counterpartyWalletName: string,
counterpartyWalletIcon: string
): BitcoinContractOfferDetails {
const bitcoinContractOffer = JSON.parse(bitcoinContractOfferJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
const bitcoinContractExpirationDate = new Date(
bitcoinContractOffer.cetLocktime * 1000
).toLocaleDateString();
const simplifiedBitcoinContractOffer: SimplifiedBitcoinContract = {
bitcoinContractId,
bitcoinContractCollateralAmount,
bitcoinContractExpirationDate,
};
const counterpartyWalletDetails: CounterpartyWalletDetails = {
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon,
};
const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
simplifiedBitcoinContract: simplifiedBitcoinContractOffer,
counterpartyWalletDetails,
};
return bitcoinContractOfferDetails;
}
async function handleAccept(
bitcoinContractJSON: string,
counterpartyWalletDetails: CounterpartyWalletDetails
) {
let bitcoinContractInterface: JsDLCInterface | undefined;
try {
bitcoinContractInterface = await getBitcoinContractInterface();
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with getting the bitcoin contract interface',
body: 'Unable to setup interface',
},
});
sendRpcResponse('none', '', 'failed');
}
if (!bitcoinContractInterface) return;
const bitcoinContractOffer = JSON.parse(bitcoinContractJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
await bitcoinContractInterface.get_wallet_balance();
try {
const acceptedBitcoinContract = await bitcoinContractInterface.accept_offer(
bitcoinContractJSON
);
const signedBitcoinContract = await sendAcceptedBitcoinContractOfferToProtocolWallet(
acceptedBitcoinContract,
counterpartyWalletDetails.counterpartyWalletURL
);
const txId = await bitcoinContractInterface.countersign_and_broadcast(
JSON.stringify(signedBitcoinContract)
);
const { txMoney, txFiatValue, txFiatValueSymbol, txLink, symbol } = getTransactionDetails(
txId,
bitcoinContractCollateralAmount
);
navigate(RouteUrls.BitcoinContractLockSuccess, {
state: {
txId,
txMoney,
txFiatValue,
txFiatValueSymbol,
symbol,
txLink,
},
});
sendRpcResponse(bitcoinContractId, txId, 'accept');
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with your Bitcoin Contract',
body: 'Unable to lock bitcoin',
},
});
sendRpcResponse(bitcoinContractId, '', 'failed');
}
}
function handleReject(bitcoinContractId: string) {
sendRpcResponse(bitcoinContractId, '', 'reject');
close();
}
function getTransactionDetails(txId: string, bitcoinCollateral: number) {
const bitcoinValue = satToBtc(bitcoinCollateral);
const txMoney = createMoneyFromDecimal(bitcoinValue, 'BTC');
const txFiatValue = i18nFormatCurrency(calculateFiatValue(txMoney)).toString();
const txFiatValueSymbol = bitcoinMarketData.price.symbol;
const txLink = { blockchain: 'bitcoin', txid: txId };
return {
txId,
txMoney,
txFiatValue,
txFiatValueSymbol,
symbol: 'BTC',
txLink,
};
}
function sendRpcResponse(bitcoinContractId: string, txId: string, action: string) {
if (!defaultParams.tabId || !initialSearchParams.get('requestId')) return;
chrome.tabs.sendMessage(
defaultParams.tabId,
makeRpcSuccessResponse('acceptBitcoinContractOffer', {
id: initialSearchParams.get('requestId') as string,
result: {
contractId: bitcoinContractId,
txId,
action,
},
})
);
}
return {
handleOffer,
handleAccept,
handleReject,
};
}

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,19 @@
import { Flex } from '@stacks/ui';
import { Text } from '@app/components/typography';
interface BitcoinContractExpirationDateProps {
expirationDate: string;
}
export function BitcoinContractExpirationDate({
expirationDate,
}: BitcoinContractExpirationDateProps) {
return (
<Flex p="loose" spacing="loose" width="100%" justifyContent="space-between">
<Text fontSize={2} fontWeight="bold">
Expiration Date
</Text>
<Text fontSize={2}>{expirationDate}</Text>
</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 img={image || <BtcIcon />} align="middle" width="100%">
<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}
/>
</>
);
}

View File

@@ -0,0 +1,40 @@
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 (
<Stack p="loose" spacing="loose" width="100%">
<Text fontWeight="bold">Amount</Text>
<BitcoinContractLockAmount
hoverLabel={addressNativeSegwit}
subtitle={truncateMiddle(addressNativeSegwit)}
subValue={`${formattedFiatValue} USD`}
value={formattedBitcoinValue}
/>
</Stack>
);
}

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="fixed"
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={4} fontWeight="bold" mb="base">
Lock Bitcoin
</Title>
{caption && (
<Flag
align="middle"
img={<img src={counterpartyWalletIcon} height="32px" width="32px" />}
pl="tight"
>
<Caption>{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>
);
}

View File

@@ -11,16 +11,18 @@ export function BroadcastError() {
const { state } = useLocation();
const analytics = useAnalytics();
const msg = get(state, 'error.message', 'Unknown error response');
const title = get(state, 'title', 'There was an error broadcasting your transaction');
const body = get(state, 'body', 'Unable to broadcast transaction');
useOnMount(() => void analytics.track('bitcoin_broadcast_tx_error', { msg }));
useOnMount(() => void analytics.track('bitcoin_contract_error', { msg }));
return (
<BroadcastErrorLayout
my="loose"
textAlign="center"
errorPayload={msg}
title="There was an error broadcasting your transaction"
body="Unable to broadcast transaction"
title={title}
body={body}
/>
);
}

View File

@@ -0,0 +1,65 @@
import { toast } from 'react-hot-toast';
import { FiCheck, FiCopy, FiExternalLink } from 'react-icons/fi';
import { useLocation } from 'react-router-dom';
import { Stack, useClipboard } from '@stacks/ui';
import { Text } from '@stacks/ui';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { satToBtc } from '@app/common/money/unit-conversion';
import {
InfoCard,
InfoCardAssetValue,
InfoCardBtn,
InfoCardFooter,
} from '@app/components/info-card/info-card';
import { ModalHeader } from '@app/components/modal-header';
export function LockBitcoinSummary() {
const { state } = useLocation();
const { txId, txMoney, txFiatValue, txFiatValueSymbol, symbol, txLink } = state;
const { onCopy } = useClipboard(txId);
const { handleOpenTxLink } = useExplorerLink();
const analytics = useAnalytics();
function onClickLink() {
void analytics.track('view_transaction_confirmation', { symbol: 'BTC' });
handleOpenTxLink(txLink);
}
function onClickCopy() {
onCopy();
toast.success('ID copied!');
}
useRouteHeader(<ModalHeader hideActions defaultClose title="Locked Bitcoin" />);
return (
<InfoCard>
<InfoCardAssetValue
value={Number(satToBtc(txMoney.amount))}
fiatValue={txFiatValue}
fiatSymbol={txFiatValueSymbol}
symbol={symbol}
icon={FiCheck}
my="loose"
px="loose"
/>
<Text fontSize={2} fontWeight={200} padding={'25px'} textAlign={'justify'}>
<span style={{ fontWeight: 500 }}>Success!</span> Your bitcoin has been locked securely. All
that's left is for it to be confirmed on the blockchain. After confirmation, you can proceed
with borrowing against it.
</Text>
<InfoCardFooter>
<Stack spacing="base" isInline width="100%">
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
</Stack>
</InfoCardFooter>
</InfoCard>
);
}

View File

@@ -0,0 +1,12 @@
export async function sendAcceptedBitcoinContractOfferToProtocolWallet(
acceptedBitcoinContractOffer: string,
counterpartyWalletURL: string
) {
return fetch(`${counterpartyWalletURL}/offer/accept`, {
method: 'put',
body: JSON.stringify({
acceptMessage: acceptedBitcoinContractOffer,
}),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json());
}

View File

@@ -14,6 +14,7 @@ import { microStxToStx } from '@app/common/money/unit-conversion';
import { useTransactionsById } from '@app/query/stacks/transactions/transactions-by-id.query';
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useStacksConfirmedTransactions } from '../transactions/transactions-with-transfers.hooks';
import { useAccountMempoolQuery } from './mempool.query';
const droppedCache = new Map();
@@ -65,10 +66,14 @@ export function useCurrentAccountMempool() {
export function useCurrentAccountMempoolTransactionsBalance() {
const address = useCurrentAccountStxAddressState();
const { transactions: pendingTransactions } = useStacksPendingTransactions();
const confirmedTxs = useStacksConfirmedTransactions();
const pendingOutboundTxs = pendingTransactions.filter(
tx => tx.tx_type === 'token_transfer' && tx.sender_address === address
) as unknown as MempoolTokenTransferTransaction[];
const pendingOutboundTxs = pendingTransactions.filter(tx => {
if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) {
return false;
}
return tx.tx_type === 'token_transfer' && tx.sender_address === address;
}) as unknown as MempoolTokenTransferTransaction[];
const tokenTransferTxsBalance = pendingOutboundTxs.reduce(
(acc, tx) => acc.plus(tx.token_transfer.amount),

View File

@@ -24,6 +24,7 @@ import { AddNetwork } from '@app/features/message-signer/add-network/add-network
import { RetrieveTaprootToNativeSegwit } from '@app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit';
import { ThemesDrawer } from '@app/features/theme-drawer/theme-drawer';
import { AllowDiagnosticsPage } from '@app/pages/allow-diagnostics/allow-diagnostics';
import { BitcoinContractRequest } from '@app/pages/bitcoin-contract-request/bitcoin-contract-request';
import { ChooseAccount } from '@app/pages/choose-account/choose-account';
import { FundPage } from '@app/pages/fund/fund';
import { Home } from '@app/pages/home/home';
@@ -42,6 +43,7 @@ import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-tra
import { RpcSignPsbt } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt';
import { SelectNetwork } from '@app/pages/select-network/select-network';
import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error';
import { LockBitcoinSummary } from '@app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary';
import { SendInscriptionContainer } from '@app/pages/send/ordinal-inscription/components/send-inscription-container';
import { SendInscriptionChooseFee } from '@app/pages/send/ordinal-inscription/send-inscription-choose-fee';
import { SendInscriptionForm } from '@app/pages/send/ordinal-inscription/send-inscription-form';
@@ -136,6 +138,18 @@ function useAppRoutes() {
{settingsModalRoutes}
{ledgerStacksTxSigningRoutes}
</Route>
<Route
path={RouteUrls.RpcReceiveBitcoinContractOffer}
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<BitcoinContractRequest />
</Suspense>
</AccountGate>
}
></Route>
<Route path={RouteUrls.BitcoinContractLockSuccess} element={<LockBitcoinSummary />} />
<Route path={RouteUrls.BitcoinContractLockError} element={<BroadcastError />} />
<Route
path={RouteUrls.Onboarding}
element={

View File

@@ -52,7 +52,7 @@ const selectNativeSegwitActiveNetworkAccountPrivateKeychain = createSelector(
})
);
function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
export function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
return useSelector(selectNativeSegwitActiveNetworkAccountPrivateKeychain);
}

View File

@@ -2,9 +2,11 @@ import { AddressVersion } from '@stacks/transactions';
import { decryptMnemonic, encryptMnemonic } from '@shared/crypto/mnemonic-encryption';
import { logger } from '@shared/logger';
import { identifyUser } from '@shared/utils/analytics';
import { recurseAccountsForActivity } from '@app/common/account-restoration/account-restore';
import { checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex } from '@app/common/account-restoration/legacy-gaia-config-lookup';
import { mnemonicToRootNode } from '@app/common/keychain/keychain';
import { BitcoinClient } from '@app/query/bitcoin/bitcoin-client';
import { fetchNamesForAddress } from '@app/query/stacks/bns/bns.utils';
import { StacksClient } from '@app/query/stacks/stacks-client';
@@ -100,8 +102,12 @@ function unlockWalletAction(password: string): AppThunk {
if (!currentKey) return;
if (currentKey.type !== 'software') return;
const { secretKey, encryptionKey } = await decryptMnemonic({ password, ...currentKey });
await initalizeWalletSession(encryptionKey, secretKey);
const rootKey = mnemonicToRootNode(secretKey);
if (!rootKey.publicKey) throw new Error('Could not derive root key from mnemonic');
void identifyUser(rootKey.publicKey);
dispatch(inMemoryKeySlice.actions.setKeysInMemory({ default: secretKey }));
};
}

View File

@@ -3,6 +3,7 @@ import { RpcErrorCode } from '@btckit/types';
import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { getTabIdFromPort } from './messaging-utils';
import { rpcAcceptBitcoinContractOffer } from './rpc-methods/accept-bitcoin-contract';
import { rpcGetAddresses } from './rpc-methods/get-addresses';
import { rpcSendTransfer } from './rpc-methods/send-transfer';
import { rpcSignMessage } from './rpc-methods/sign-message';
@@ -36,6 +37,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
break;
}
case 'acceptBitcoinContractOffer': {
await rpcAcceptBitcoinContractOffer(message, port);
break;
}
default:
chrome.tabs.sendMessage(
getTabIdFromPort(port),

View File

@@ -0,0 +1,70 @@
import { RpcErrorCode } from '@btckit/types';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractRequest } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import {
RequestParams,
getTabIdFromPort,
listenForPopupClose,
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';
export async function rpcAcceptBitcoinContractOffer(
message: BitcoinContractRequest,
port: chrome.runtime.Port
) {
if (!message.params) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid parameters' },
})
);
return;
}
const params: RequestParams = [
['bitcoinContractOffer', message.params.bitcoinContractOffer],
['counterpartyWalletURL', message.params.counterpartyWalletURL],
['counterpartyWalletName', message.params.counterpartyWalletName],
['counterpartyWalletIcon', message.params.counterpartyWalletIcon],
['requestId', message.id],
];
if (message.params.bitcoinContractOffer.includes('Invalid state: Not enough fund in utxos')) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_REQUEST,
message: 'The counterparty does not have enough funds to complete the offer',
},
})
);
return;
}
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, params);
const { id } = await triggerRequestWindowOpen(
RouteUrls.RpcReceiveBitcoinContractOffer,
urlParams
);
listenForPopupClose({
tabId,
id,
response: makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected the offer',
},
}),
});
}

View File

@@ -128,8 +128,9 @@ document.addEventListener(DomEventName.psbtRequest, ((event: PsbtRequestEvent) =
});
}) as EventListener);
// Inject inpage script (Stacks Provider)
window.addEventListener('load', () => {
const inpage = document.createElement('script');
inpage.src = chrome.runtime.getURL('inpage.js');
inpage.id = 'stacks-wallet-provider';
document.body.appendChild(inpage);
});

View File

@@ -3,12 +3,12 @@
*/
export enum DomEventName {
request = 'request',
authenticationRequest = 'stacksAuthenticationRequest',
signatureRequest = 'signatureRequest',
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
transactionRequest = 'stacksTransactionRequest',
profileUpdateRequest = 'profileUpdateRequest',
psbtRequest = 'psbtRequest',
authenticationRequest = 'hiroWalletStacksAuthenticationRequest',
signatureRequest = 'hiroWalletSignatureRequest',
structuredDataSignatureRequest = 'hiroWalletStructuredDataSignatureRequest',
transactionRequest = 'hiroWalletStacksTransactionRequest',
profileUpdateRequest = 'hiroWalletProfileUpdateRequest',
psbtRequest = 'hiroWalletPsbtRequest',
}
export interface AuthenticationRequestEventDetails {

View File

@@ -11,18 +11,18 @@ export const MESSAGE_SOURCE = 'stacks-wallet' as const;
export const CONTENT_SCRIPT_PORT = 'content-script' as const;
export enum ExternalMethods {
transactionRequest = 'transactionRequest',
transactionResponse = 'transactionResponse',
authenticationRequest = 'authenticationRequest',
authenticationResponse = 'authenticationResponse',
signatureRequest = 'signatureRequest',
signatureResponse = 'signatureResponse',
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
structuredDataSignatureResponse = 'structuredDataSignatureResponse',
profileUpdateRequest = 'profileUpdateRequest',
profileUpdateResponse = 'profileUpdateResponse',
psbtRequest = 'psbtRequest',
psbtResponse = 'psbtResponse',
transactionRequest = 'hiroWalletTransactionRequest',
transactionResponse = 'hiroWalletTransactionResponse',
authenticationRequest = 'hiroWalletAuthenticationRequest',
authenticationResponse = 'hiroWalletAuthenticationResponse',
signatureRequest = 'hiroWalletSignatureRequest',
signatureResponse = 'hiroWalletSignatureResponse',
structuredDataSignatureRequest = 'hiroWalletStructuredDataSignatureRequest',
structuredDataSignatureResponse = 'hiroWalletStructuredDataSignatureResponse',
profileUpdateRequest = 'hiroWalletProfileUpdateRequest',
profileUpdateResponse = 'hiroWalletProfileUpdateResponse',
psbtRequest = 'hiroWalletPsbtRequest',
psbtResponse = 'hiroWalletPsbtResponse',
}
export enum InternalMethods {

View File

@@ -52,6 +52,11 @@ export enum RouteUrls {
// Locked wallet route
Unlock = '/unlock',
// Bitcoin Contract routes
BitcoinContractLockSuccess = '/bitcoin-contract-lock-success',
BitcoinContractLockError = '/bitcoin-contract-lock-error',
BitcoinContractList = '/bitcoin-contract-list',
// Modal routes
ChangeTheme = 'change-theme',
EditNonce = 'edit-nonce',
@@ -92,5 +97,6 @@ export enum RouteUrls {
RpcSendTransferChooseFee = '/send-transfer/choose-fee',
RpcSendTransferConfirmation = '/send-transfer/confirm',
RpcSendTransferSummary = '/send-transfer/summary',
RpcReceiveBitcoinContractOffer = '/bitcoin-contract-offer/:bitcoinContractOffer/:counterpartyWalletURL',
RpcSignBip322Message = '/sign-bip322-message',
}

View File

@@ -0,0 +1,26 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '@btckit/types';
import { AllowAdditionaProperties } from '@btckit/types/dist/types/utils';
interface BitcoinContractResponseParams extends AllowAdditionaProperties {
bitcoinContractOffer: string;
counterpartyWalletURL: string;
counterpartyWalletName: string;
counterpartyWalletIcon: string;
}
interface BitcoinContractResponseBody extends AllowAdditionaProperties {
contractId: string;
action: string;
txId?: string;
error?: string;
}
export type BitcoinContractRequest = RpcRequest<
'acceptBitcoinContractOffer',
BitcoinContractResponseParams
>;
type BitcoinContractResponse = RpcResponse<BitcoinContractResponseBody>;
export type AcceptBitcoinContract = DefineRpcMethod<
BitcoinContractRequest,
BitcoinContractResponse
>;

View File

@@ -2,11 +2,12 @@ import { BtcKitMethodMap, ExtractErrorResponse, ExtractSuccessResponse } from '@
import { ValueOf } from '@shared/utils/type-utils';
import { AcceptBitcoinContract } from './methods/accept-bitcoin-contract';
import { SignPsbt } from './methods/sign-psbt';
import { SupportedMethods } from './methods/supported-methods';
// Supports BtcKit methods, as well as custom Hiro Wallet methods
export type WalletMethodMap = BtcKitMethodMap & SupportedMethods & SignPsbt;
export type WalletMethodMap = BtcKitMethodMap & SupportedMethods & SignPsbt & AcceptBitcoinContract;
export type WalletRequests = ValueOf<WalletMethodMap>['request'];
export type WalletResponses = ValueOf<WalletMethodMap>['response'];

View File

@@ -0,0 +1,22 @@
import { deriveAnalyticsIdentifier } from './analytics';
describe(deriveAnalyticsIdentifier.name, () => {
test('the derivation of the users identifier', () =>
expect(deriveAnalyticsIdentifier(Uint8Array.from([1, 2, 3]))).toBe('T49D6FxBz57'));
const expectedLength = 11;
function createRandomValue() {
return window.crypto.getRandomValues(new Uint8Array());
}
test.each([
createRandomValue(),
createRandomValue(),
createRandomValue(),
createRandomValue(),
createRandomValue(),
])('random test value %#', value => {
expect(deriveAnalyticsIdentifier(value).length).toBe(expectedLength);
});
});

View File

@@ -1,3 +1,6 @@
import { ripemd160 } from '@noble/hashes/ripemd160';
import { sha256 } from '@noble/hashes/sha256';
import { base58 } from '@scure/base';
import { AnalyticsBrowser } from '@segment/analytics-next';
import * as Sentry from '@sentry/react';
import { getStoredState } from 'redux-persist';
@@ -33,6 +36,16 @@ export function initAnalytics() {
);
}
// Used to create a unique identifier for a user's key in base58.
// K = ripemd160(sha256(publicKey))[:8]
export function deriveAnalyticsIdentifier(publicKey: Uint8Array) {
return base58.encode(ripemd160(sha256(publicKey)).slice(0, 8));
}
export async function identifyUser(publicKey: Uint8Array) {
return analytics.identify(deriveAnalyticsIdentifier(publicKey));
}
export function initSentry() {
if (IS_TEST_ENV || !SENTRY_DSN) return;

View File

@@ -117,6 +117,7 @@ const config = {
runtimeChunk: false,
},
module: {
noParse: /argon2\.wasm$/,
rules: [
{
test: /\.(ts|tsx)$/,
@@ -158,7 +159,7 @@ const config = {
],
},
{
test: /\.wasm$/,
test: /argon2\.wasm$/,
// Tells WebPack that this module should be included as
// base64-encoded binary file and not as code
loader: 'base64-loader',
@@ -168,6 +169,13 @@ const config = {
// Error: WebAssembly module is included in initial chunk.
type: 'javascript/auto',
},
{
test: /cfddlcjs_wasm\.wasm/,
type: 'asset/resource',
generator: {
filename: '[name].wasm',
},
},
].filter(Boolean),
},
watch: false,
@@ -227,15 +235,16 @@ const config = {
new webpack.DefinePlugin({
VERSION: JSON.stringify(VERSION),
}),
new webpack.ProvidePlugin({
process: 'process/browser.js',
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
fetch: 'cross-fetch',
}),
new ProgressBarPlugin(),
],
experiments: {
asyncWebAssembly: true,
},
};
module.exports = config;

113
yarn.lock
View File

@@ -3798,10 +3798,10 @@
dset "^3.1.2"
tslib "^2.4.1"
"@segment/analytics-next@1.53.0":
version "1.53.0"
resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.53.0.tgz#05dce27ce20a9abb3f9ea2d6e6d67667c82f2a9d"
integrity sha512-7bzUjxGuccQpDx7CrteAx4yM2eDosjheQLySLBi6cz25EwPbBPb2X+ZzBUZIfLA0V0Co0zWaE6xXzVw8X5zSpQ==
"@segment/analytics-next@1.53.1":
version "1.53.1"
resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.53.1.tgz#0ffe13b5af72e6a235dcc489a8bccf687e4d8da4"
integrity sha512-Tf5suyN00Qq2irdUP6q2KJje1reFTN5vCrh4mi/E/gTma2by9j5jYYsbHavMaOGM51aYVbWkf6a3HOyTqWOm1A==
dependencies:
"@lukeed/uuid" "^2.0.0"
"@segment/analytics-core" "1.3.0"
@@ -3864,26 +3864,26 @@
"@sentry/utils" "7.53.1"
tslib "^1.9.3"
"@sentry-internal/tracing@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.57.0.tgz#cb761931b635f8f24c84be0eecfacb8516b20551"
integrity sha512-tpViyDd8AhQGYYhI94xi2aaDopXOPfL2Apwrtb3qirWkomIQ2K86W1mPmkce+B0cFOnW2Dxv/ZTFKz6ghjK75A==
"@sentry-internal/tracing@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.59.2.tgz#91c44a37151bb81fac0500b4e031f53583dff772"
integrity sha512-02gteChV/lMobWU06VlITq+myEWk0MzhnDCm8n/DMigB47I9HkWZFAJ+CYG6Ns0rTL+3+/c2V0bPyQkZwIC+Sg==
dependencies:
"@sentry/core" "7.57.0"
"@sentry/types" "7.57.0"
"@sentry/utils" "7.57.0"
"@sentry/core" "7.59.2"
"@sentry/types" "7.59.2"
"@sentry/utils" "7.59.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/browser@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.57.0.tgz#6e724c9eac680dba99ced0fdf81be8d1e3b3bceb"
integrity sha512-E0HaYYlaqHFiIRZXxcvOO8Odvlt+TR1vFFXzqUWXPOvDRxURglTOCQ3EN/u6bxtAGJ6y/Zc2obgihTtypuel/w==
"@sentry/browser@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.59.2.tgz#77f6c7af33448ec375d684442ef51f21f5c26ace"
integrity sha512-N1JiBs1VRR5DV0209TZgaMwRGiTYN1C34sFzIW7nuC82X4gHy3tuJjZPlMDTtgFrALBMJ24yQ7D4HJjXrS2+Dw==
dependencies:
"@sentry-internal/tracing" "7.57.0"
"@sentry/core" "7.57.0"
"@sentry/replay" "7.57.0"
"@sentry/types" "7.57.0"
"@sentry/utils" "7.57.0"
"@sentry-internal/tracing" "7.59.2"
"@sentry/core" "7.59.2"
"@sentry/replay" "7.59.2"
"@sentry/types" "7.59.2"
"@sentry/utils" "7.59.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/bundler-plugin-core@2.4.0":
@@ -3920,13 +3920,13 @@
"@sentry/utils" "7.53.1"
tslib "^1.9.3"
"@sentry/core@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.57.0.tgz#65093d739c04f320a54395a21be955fcbe326acb"
integrity sha512-l014NudPH0vQlzybtXajPxYFfs9w762NoarjObC3gu76D1jzBBFzhdRelkGpDbSLNTIsKhEDDRpgAjBWJ9icfw==
"@sentry/core@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.59.2.tgz#5c69cc7b8321f47c3da353f51084e2f4c4170e7d"
integrity sha512-GRhoPw6b6GkvOsa060aREc9yyHjgAKITgITNbzUmn0GqIeWD5SMoCBAcENRHVgUnpQWOpnkEF1/sqxvwx+rf6Q==
dependencies:
"@sentry/types" "7.57.0"
"@sentry/utils" "7.57.0"
"@sentry/types" "7.59.2"
"@sentry/utils" "7.59.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/node@7.53.1":
@@ -3943,42 +3943,42 @@
lru_map "^0.3.3"
tslib "^1.9.3"
"@sentry/react@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.57.0.tgz#cf91f0115bcd2a8306d6c8a39d8e8b53d4b21814"
integrity sha512-XGNTjIoCG3naSmCU8qObd+y+CqAB6NQkGWOp2yyBwp2inyKF2ehJvDh6bIQloBYq2TmOJDa4NfXdMrkilxaLFQ==
"@sentry/react@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.59.2.tgz#d2ffe0048591253ec0809ab7d69c4fcc1cb103eb"
integrity sha512-n7m1GbBfCjvc/tLx8+wRNr/ol7VtGNsqAZ++mPoeMyT8WWkj17rDyTgShB1PQoevAAoZ74XJUEVg3MliOdEIrA==
dependencies:
"@sentry/browser" "7.57.0"
"@sentry/types" "7.57.0"
"@sentry/utils" "7.57.0"
"@sentry/browser" "7.59.2"
"@sentry/types" "7.59.2"
"@sentry/utils" "7.59.2"
hoist-non-react-statics "^3.3.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/replay@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.57.0.tgz#c8f7eae7b7edc9d32c3d2955b337f3b3c76dff39"
integrity sha512-pN4ryNS3J5EYbkXvR+O/+hseAJha7XDl8mPFtK0OGTHG10JzCi4tQJazblHQdpb5QBaMMPCeZ+isyfoQLDNXnw==
"@sentry/replay@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.59.2.tgz#4fc9c455e0e3b933075afe1729d8ba42953c62d4"
integrity sha512-Ma72ofTdtSinBY5GH0zM7k8o/WsdaVdPP/1iyFbcWQDt8dnrcsJVUKK0t9+8gijpiSMUKE+vjFjQNL9/PGYekw==
dependencies:
"@sentry/core" "7.57.0"
"@sentry/types" "7.57.0"
"@sentry/utils" "7.57.0"
"@sentry/core" "7.59.2"
"@sentry/types" "7.59.2"
"@sentry/utils" "7.59.2"
"@sentry/tracing@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.57.0.tgz#8bd07317db7b88ec5ecae48ad680e269af71b54d"
integrity sha512-D8eKJMYN529mDP9lsOLyhe0Rf9Qiexo7Ul4+MQwDlwRr9c9tc0AdGwFlnKGvCMDh7ucITzvZkMZDHBapU3WHNQ==
"@sentry/tracing@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.59.2.tgz#65b82fde3a336376a80c423354ae207743a3b150"
integrity sha512-8HXnlmiES+sMPwq1h4HsVO1QI58WTtXM9z6taPRX/olNkhmN3Pk2o4PQeItLRzYs84K/X4GPk/aofeqMbBX8+g==
dependencies:
"@sentry-internal/tracing" "7.57.0"
"@sentry-internal/tracing" "7.59.2"
"@sentry/types@7.53.1":
version "7.53.1"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.53.1.tgz#3eefbad851f2d0deff67285d7e976d23d7d06a41"
integrity sha512-/ijchRIu+jz3+j/zY+7KRPfLSCY14fTx5xujjbOdmEKjmIHQmwPBdszcQm40uwofrR8taV4hbt5MFN+WnjCkCw==
"@sentry/types@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.57.0.tgz#4fdb80cbd49ba034dd8d9be0c0005a016d5db3ce"
integrity sha512-D7ifoUfxuVCUyktIr5Gc+jXUbtcUMmfHdTtTbf1XCZHua5mJceK9wtl3YCg3eq/HK2Ppd52BKnTzEcS5ZKQM+w==
"@sentry/types@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.59.2.tgz#25b2ae0b2dc6733ca48621cf9167c9a7965852ae"
integrity sha512-rylG7UQ0cC/xbV6trSuaAE/bsruSZy92jxQ1/KSOYKwBBvRFPXJBuiBtA81b8eYa4THZ+mE/ol2qOTJYuuV4Ug==
"@sentry/utils@7.53.1":
version "7.53.1"
@@ -3988,12 +3988,12 @@
"@sentry/types" "7.53.1"
tslib "^1.9.3"
"@sentry/utils@7.57.0":
version "7.57.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.57.0.tgz#8253c6fcf35138b4c424234b8da1596e11b98ad8"
integrity sha512-YXrkMCiNklqkXctn4mKYkrzNCf/dfVcRUQrkXjeBC+PHXbcpPyaJgInNvztR7Skl8lE3JPGPN4v5XhLxK1bUUg==
"@sentry/utils@7.59.2":
version "7.59.2"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.59.2.tgz#c4c76b1ea2a7f9363d1203dedfeafec2387bf634"
integrity sha512-uxC8xH9wsB/tZUnjmaQ1uGtsumFOc19KWfedVHXzcNwqdt5uS3EB4+D1d8WwiJyLy2nm61DdmTC9SiB4HS+OSw==
dependencies:
"@sentry/types" "7.57.0"
"@sentry/types" "7.59.2"
tslib "^2.4.1 || ^1.9.3"
"@sentry/webpack-plugin@2.4.0":
@@ -9720,6 +9720,11 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dlc-wasm-wallet@0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/dlc-wasm-wallet/-/dlc-wasm-wallet-0.4.3.tgz#1070e3a79edb2330ef912a0e182741b37ce49615"
integrity sha512-y8eZu1thQKYgU0EFYTqY0YswYJDenqR4tjMaBymp6vh//YTtsXSyK1LLRSz3oSL60Y1ZwBD9JBWDm1JnQigxlg==
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"