Merge pull request #4135 from hirosystems/release/receive-modal-updates

Release/receive modal updates
This commit is contained in:
Fara Woolf
2023-08-16 10:23:33 -05:00
committed by GitHub
36 changed files with 414 additions and 485 deletions

View File

@@ -10,7 +10,7 @@ env:
WALLET_ENVIRONMENT: feature
jobs:
pre_run:
pre-run:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
@@ -21,21 +21,21 @@ jobs:
update-pull-request-body:
name: Add links to built extensions
# Don't run on forks
if: github.repository == 'hirosystems/wallet'
if: github.repository == 'hirosystems/wallet' || github.repository == 'hirosystems/wallet-private'
runs-on: ubuntu-latest
needs:
- pre_run
- pre-run
steps:
- uses: kyranjamie/pull-request-fixed-header@v1.0.1
with:
header: '> Try out this version of the Hiro Wallet - download [extension builds](https://github.com/hirosystems/wallet/actions/runs/${{ github.run_id }}).'
header: '> Try out this version of the Hiro Wallet - download [extension builds](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_chrome_extension:
name: Build Chrome extension
name: Build debug Chrome extension
runs-on: ubuntu-latest
needs:
- pre_run
- pre-run
steps:
- uses: actions/checkout@v3
@@ -63,34 +63,29 @@ jobs:
name: stacks-wallet-chromium
path: stacks-wallet-chromium.zip
publish_firefox_beta:
name: Publish beta firefox extension
build_firefox:
name: Build debug Firefox extension
runs-on: ubuntu-latest
needs:
- pre_run
- pre-run
env:
MINIFY_PRODUCTION_BUILD: true
# Disabled as this job isn't running correctly
if: false
TARGET_BROWSER: firefox
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Set Node Version
uses: actions/setup-node@v3
- name: Restore cache
uses: actions/cache@v3
- uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }}
- name: Install yarn dependencies
run: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --frozen-lockfile
- name: Install packages
uses: ./.github/actions/provision
if: steps.cache-node-modules.outputs.cache-hit != 'true'
- name: Build project
run: yarn build
@@ -98,14 +93,10 @@ jobs:
- name: Build extension
run: sh build-ext.sh
- name: Sign Firefox extension
run: yarn web-ext sign --channel=unlisted
env:
WEB_EXT_API_KEY: ${{ secrets.WEB_EXT_API_KEY }}
WEB_EXT_API_SECRET: ${{ secrets.WEB_EXT_API_SECRET }}
- name: Rename file
run: mv stacks-wallet-chromium.zip stacks-wallet-firefox.zip
- uses: actions/upload-artifact@v3
name: Upload Firefox Add-On XPI
with:
name: connect-addon
path: web-ext-artifacts/*.xpi
name: stacks-wallet-firefox
path: stacks-wallet-firefox.zip

View File

@@ -184,7 +184,7 @@
"coinselect": "3.1.13",
"compare-versions": "4.1.3",
"dayjs": "1.11.8",
"dlc-wasm-wallet": "0.4.3",
"dlc-wasm-wallet": "0.4.7",
"dompurify": "3.0.4",
"downshift": "6.1.7",
"ecdsa-sig-formatter": "1.0.11",

View File

@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router-dom';
import { RpcErrorCode } from '@btckit/types';
import { bytesToHex } from '@stacks/common';
import { JsDLCInterface } from 'dlc-wasm-wallet';
@@ -10,7 +11,9 @@ import {
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { createMoneyFromDecimal } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
import {
@@ -54,9 +57,10 @@ export function useBitcoinContracts() {
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const currentIndex = useCurrentAccountIndex();
const nativeSegwitPrivateKeychain = useNativeSegwitAccountBuilder()?.(currentIndex);
const oracleAPI = 'https://testnet.dlc.link/oracle';
async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
async function getBitcoinContractInterface(
attestorURLs: string[]
): Promise<JsDLCInterface | undefined> {
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
if (!nativeSegwitPrivateKeychain || !bitcoinAccountDetails) return;
@@ -83,7 +87,7 @@ export function useBitcoinContracts() {
currentAddress,
currentBitcoinNetwork,
blockchainAPI,
oracleAPI
JSON.stringify(attestorURLs)
);
return bitcoinContractInterface;
@@ -91,11 +95,10 @@ export function useBitcoinContracts() {
function handleOffer(
bitcoinContractOfferJSON: string,
counterpartyWalletURL: string,
counterpartyWalletName: string,
counterpartyWalletIcon: string
counterpartyWalletDetailsJSON: string
): BitcoinContractOfferDetails {
const bitcoinContractOffer = JSON.parse(bitcoinContractOfferJSON);
const counterpartyWalletDetails = JSON.parse(counterpartyWalletDetailsJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
@@ -110,12 +113,6 @@ export function useBitcoinContracts() {
bitcoinContractExpirationDate,
};
const counterpartyWalletDetails: CounterpartyWalletDetails = {
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon,
};
const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
simplifiedBitcoinContract: simplifiedBitcoinContractOffer,
counterpartyWalletDetails,
@@ -126,27 +123,27 @@ export function useBitcoinContracts() {
async function handleAccept(
bitcoinContractJSON: string,
counterpartyWalletDetails: CounterpartyWalletDetails
counterpartyWalletDetails: CounterpartyWalletDetails,
attestorURLs: string[]
) {
let bitcoinContractInterface: JsDLCInterface | undefined;
try {
bitcoinContractInterface = await getBitcoinContractInterface();
bitcoinContractInterface = await getBitcoinContractInterface(attestorURLs);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error,
title: 'There was an error with getting the bitcoin contract interface',
body: 'Unable to setup interface',
title: 'There was an error with getting the Bitcoin Contract Interface',
body: 'Unable to setup Bitcoin Contract Interface',
},
});
sendRpcResponse('none', '', 'failed');
sendRpcResponse(BitcoinContractResponseStatus.INTERFACE_ERROR);
}
if (!bitcoinContractInterface) return;
const bitcoinContractOffer = JSON.parse(bitcoinContractJSON);
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
const bitcoinContractCollateralAmount =
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
@@ -162,6 +159,8 @@ export function useBitcoinContracts() {
counterpartyWalletDetails.counterpartyWalletURL
);
const bitcoinContractId = signedBitcoinContract.contractId;
const txId = await bitcoinContractInterface.countersign_and_broadcast(
JSON.stringify(signedBitcoinContract)
);
@@ -182,7 +181,7 @@ export function useBitcoinContracts() {
},
});
sendRpcResponse(bitcoinContractId, txId, 'accept');
sendRpcResponse(BitcoinContractResponseStatus.SUCCESS, bitcoinContractId, txId);
} catch (error) {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
@@ -191,12 +190,12 @@ export function useBitcoinContracts() {
body: 'Unable to lock bitcoin',
},
});
sendRpcResponse(bitcoinContractId, '', 'failed');
sendRpcResponse(BitcoinContractResponseStatus.BROADCAST_ERROR);
}
}
function handleReject(bitcoinContractId: string) {
sendRpcResponse(bitcoinContractId, '', 'reject');
function handleReject() {
sendRpcResponse(BitcoinContractResponseStatus.REJECTED);
close();
}
@@ -217,25 +216,66 @@ export function useBitcoinContracts() {
};
}
function sendRpcResponse(bitcoinContractId: string, txId: string, action: string) {
function sendRpcResponse(
responseStatus: BitcoinContractResponseStatus,
bitcoinContractId?: string,
txId?: 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,
},
})
);
const requestId = initialSearchParams.get('requestId') as string;
let response;
switch (responseStatus) {
case BitcoinContractResponseStatus.REJECTED:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.USER_REJECTION,
message: responseStatus,
},
});
break;
case BitcoinContractResponseStatus.NETWORK_ERROR:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.INVALID_REQUEST,
message: responseStatus,
},
});
break;
case BitcoinContractResponseStatus.BROADCAST_ERROR:
case BitcoinContractResponseStatus.INTERFACE_ERROR:
response = makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: requestId,
error: {
code: RpcErrorCode.INTERNAL_ERROR,
message: responseStatus,
},
});
break;
default:
response = makeRpcSuccessResponse('acceptBitcoinContractOffer', {
id: requestId,
result: {
contractId: bitcoinContractId,
txId,
},
});
break;
}
chrome.tabs.sendMessage(defaultParams.tabId, response);
}
return {
handleOffer,
handleAccept,
handleReject,
sendRpcResponse,
};
}

View File

@@ -0,0 +1,6 @@
import BitcoinStampImg from '@assets/images/bitcoin-stamp.png';
import { Box } from '@stacks/ui';
export function BtcStampsIcon() {
return <Box as="img" src={BitcoinStampImg} width="36px" />;
}

View File

@@ -1,39 +0,0 @@
import { FiCopy } from 'react-icons/fi';
import { Box, Button, ButtonProps, Flex, Stack } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';
import { Flag } from '../layout/flag';
import { Caption } from '../typography';
interface ReceiveCollectibleItemProps extends ButtonProps {
address: string;
icon: React.JSX.Element;
onCopyAddress(): void;
title: string;
}
export function ReceiveCollectibleItem({
address,
icon,
onCopyAddress,
title,
...rest
}: ReceiveCollectibleItemProps) {
return (
<Flag img={icon} spacing="base">
<Flex justifyContent="space-between">
<Box>
{title}
<Caption mt="2px">{truncateMiddle(address, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button borderRadius="10px" mode="tertiary" onClick={onCopyAddress} {...rest}>
<FiCopy />
</Button>
</Box>
</Stack>
</Flex>
</Flag>
);
}

View File

@@ -1,79 +0,0 @@
import toast from 'react-hot-toast';
import { useLocation, useNavigate } from 'react-router-dom';
import BitcoinStampImg from '@assets/images/bitcoin-stamp.png';
import { Box, Stack, useClipboard } from '@stacks/ui';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import get from 'lodash.get';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { ReceiveCollectibleItem } from './receive-collectible-item';
export function ReceiveCollectible() {
const analytics = useAnalytics();
const location = useLocation();
const navigate = useNavigate();
const accountIndex = get(location.state, 'accountIndex', undefined);
const btcAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const btcAddressTaproot = useZeroIndexTaprootAddress(accountIndex);
// TODO: Reuse later for privacy mode
// const { isLoading, isError, data: btcAddress } = useNextFreshTaprootAddressQuery(accountIndex);
const stxAddress = useCurrentAccountStxAddressState();
const { onCopy: onCopyBitcoin } = useClipboard(btcAddressNativeSegwit);
const { onCopy: onCopyStacks } = useClipboard(stxAddress);
function copyBitcoinAddressToClipboard(copyHandler: () => void) {
void analytics.track('select_stamp_to_add_new_collectible');
toast.success('Copied to clipboard!');
copyHandler();
}
function copyStacksAddressToClipboard(copyHandler: () => void) {
void analytics.track('select_nft_to_add_new_collectible');
toast.success('Copied to clipboard!');
copyHandler();
}
if (!btcAddressTaproot) return null;
return (
<Stack spacing="loose" mt="base" mb="extra-loose">
<ReceiveCollectibleItem
address={btcAddressTaproot}
icon={<OrdinalIcon />}
data-testid={HomePageSelectors.ReceiveBtcTaprootQrCodeBtn}
onCopyAddress={() => {
void analytics.track('select_inscription_to_add_new_collectible');
navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddressTaproot } });
}}
title="Ordinal inscription"
/>
<ReceiveCollectibleItem
address={btcAddressNativeSegwit}
icon={
<Box>
<img src={BitcoinStampImg} width="36px" />
</Box>
}
onCopyAddress={() => copyBitcoinAddressToClipboard(onCopyBitcoin)}
title="Bitcoin Stamp"
/>
<ReceiveCollectibleItem
address={stxAddress}
icon={<StxAvatar />}
onCopyAddress={() => copyStacksAddressToClipboard(onCopyStacks)}
title="Stacks NFT"
/>
</Stack>
);
}

View File

@@ -7,7 +7,7 @@ export function PsbtSignerLayout({ children }: HasChildren) {
<Stack
alignItems="center"
maxHeight="calc(100vh - 72px)"
overflowY="scroll"
overflowY="auto"
pb="120px"
px="loose"
spacing="base-loose"

View File

@@ -1,4 +1,8 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts';
import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts';
@@ -14,48 +18,68 @@ import { BitcoinContractRequestWarningLabel } from './components/bitcoin-contrac
export function BitcoinContractRequest() {
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const navigate = useNavigate();
const { handleOffer, handleAccept, handleReject } = useBitcoinContracts();
const { handleOffer, handleAccept, handleReject, sendRpcResponse } = useBitcoinContracts();
const [bitcoinContractJSON, setBitcoinContractJSON] = useState<string>();
const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] =
useState<BitcoinContractOfferDetails>();
const [bitcoinAddress, setBitcoinAddress] = useState<string>();
const [attestorURLs, setAttestorURLs] = useState<string[]>([]);
const [isLoading, setLoading] = useState(true);
const [isProcessing, setProcessing] = useState(false);
const handleAcceptClick = async () => {
if (!bitcoinContractJSON || !bitcoinContractOfferDetails) return;
await handleAccept(bitcoinContractJSON, bitcoinContractOfferDetails.counterpartyWalletDetails);
setProcessing(true);
await handleAccept(
bitcoinContractJSON,
bitcoinContractOfferDetails.counterpartyWalletDetails,
attestorURLs
);
setProcessing(false);
};
const handleRejectClick = async () => {
if (!bitcoinContractOfferDetails) return;
await handleReject(bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractId);
handleReject();
};
useOnMount(() => {
const bitcoinContractOfferJSON = initialSearchParams.get('bitcoinContractOffer');
const counterpartyWalletURL = initialSearchParams.get('counterpartyWalletURL');
const counterpartyWalletName = initialSearchParams.get('counterpartyWalletName');
const counterpartyWalletIcon = initialSearchParams.get('counterpartyWalletIcon');
const counterpartyWalletDetailsJSON = initialSearchParams.get('counterpartyWalletDetails');
const attestorURLs = initialSearchParams.get('attestorURLs');
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
if (!bitcoinAccountDetails) return;
const currentBitcoinNetwork = bitcoinAccountDetails.network;
if (currentBitcoinNetwork !== 'testnet') {
navigate(RouteUrls.BitcoinContractLockError, {
state: {
error: new Error('Invalid Network'),
title: "Network doesn't support Bitcoin Contracts",
body: "The wallet's current selected network doesn't support Bitcoin Contracts",
},
});
sendRpcResponse(BitcoinContractResponseStatus.NETWORK_ERROR);
}
if (
!getNativeSegwitSigner ||
!bitcoinContractOfferJSON ||
!counterpartyWalletURL ||
!counterpartyWalletName ||
!counterpartyWalletIcon
!counterpartyWalletDetailsJSON ||
!attestorURLs
)
return;
const currentBitcoinContractOfferDetails = handleOffer(
bitcoinContractOfferJSON,
counterpartyWalletURL,
counterpartyWalletName,
counterpartyWalletIcon
counterpartyWalletDetailsJSON
);
const currentAddress = getNativeSegwitSigner(0).address;
@@ -63,6 +87,7 @@ export function BitcoinContractRequest() {
setBitcoinContractJSON(bitcoinContractOfferJSON);
setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails);
setBitcoinAddress(currentAddress);
setAttestorURLs(JSON.parse(attestorURLs));
setLoading(false);
});
@@ -82,7 +107,7 @@ export function BitcoinContractRequest() {
appName={bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName}
/>
<BitcoinContractRequestActions
isLoading={isLoading}
isLoading={isProcessing}
bitcoinAddress={bitcoinAddress}
requiredAmount={
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractCollateralAmount

View File

@@ -1,99 +0,0 @@
import toast from 'react-hot-toast';
import { FiCopy } from 'react-icons/fi';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Flex, Stack, useClipboard } from '@stacks/ui';
import { color, truncateMiddle } from '@stacks/ui-utils';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { BtcIcon } from '@app/components/icons/btc-icon';
import { Flag } from '@app/components/layout/flag';
import { QrCodeIcon } from '@app/components/qr-code-icon';
import { ReceiveCollectible } from '@app/components/receive/receive-collectible';
import { Caption } from '@app/components/typography';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
export function ReceiveModal() {
const analytics = useAnalytics();
const navigate = useNavigate();
const btcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const stxAddress = useCurrentAccountStxAddressState();
const { onCopy: onCopyStacks } = useClipboard(stxAddress);
function copyToClipboard(copyHandler: () => void) {
void analytics.track('copy_address_to_clipboard');
toast.success('Copied to clipboard!');
copyHandler();
}
return (
<BaseDrawer title="Select asset to receive" isShowing onClose={() => navigate('../')}>
<Box mx="extra-loose">
<Caption style={{ fontSize: '14px' }}>Tokens</Caption>
<Stack spacing="loose" mt="base" mb="extra-loose">
<Flag img={<BtcIcon />} spacing="base">
<Flex justifyContent="space-between">
<Box>
Bitcoin
<Caption mt="2px">{truncateMiddle(btcAddress, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button
borderRadius="10px"
data-testid={HomePageSelectors.ReceiveBtcNativeSegwitQrCodeBtn}
mode="tertiary"
onClick={() => navigate(RouteUrls.ReceiveBtc)}
>
<Box color={color('text-caption')} size="14px">
<QrCodeIcon />
</Box>
</Button>
</Box>
</Stack>
</Flex>
</Flag>
<Flag img={<StxAvatar />} spacing="base">
<Flex justifyContent="space-between">
<Box>
Stacks
<Caption mt="2px">{truncateMiddle(stxAddress, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button
borderRadius="10px"
mode="tertiary"
mr="tight"
onClick={() => copyToClipboard(onCopyStacks)}
>
<FiCopy />
</Button>
<Button
borderRadius="10px"
data-testid={HomePageSelectors.ReceiveStxQrCodeBtn}
mode="tertiary"
onClick={() => navigate(RouteUrls.ReceiveStx)}
>
<Box color={color('text-caption')} size="14px">
<QrCodeIcon />
</Box>
</Button>
</Box>
</Stack>
</Flex>
</Flag>
</Stack>
<Caption style={{ fontSize: '14px' }}>Collectibles</Caption>
<ReceiveCollectible />
</Box>
</BaseDrawer>
);
}

View File

@@ -0,0 +1,54 @@
import { FiCopy } from 'react-icons/fi';
import { Box, Button, ButtonProps, Flex, Stack } from '@stacks/ui';
import { color, truncateMiddle } from '@stacks/ui-utils';
import { Caption } from '@app/components//typography';
import { Flag } from '@app/components/layout/flag';
import { QrCodeIcon } from '@app/components/qr-code-icon';
interface ReceiveItemProps extends ButtonProps {
address: string;
dataTestId?: string;
icon: React.JSX.Element;
onCopyAddress(): void;
onClickQrCode?(): void;
title: string;
}
export function ReceiveItem({
address,
dataTestId,
icon,
onCopyAddress,
onClickQrCode,
title,
}: ReceiveItemProps) {
return (
<Flag img={icon} spacing="base">
<Flex justifyContent="space-between">
<Box>
{title}
<Caption mt="2px">{truncateMiddle(address, 6)}</Caption>
</Box>
<Stack>
<Box>
<Button borderRadius="10px" mode="tertiary" onClick={onCopyAddress}>
<FiCopy />
</Button>
{onClickQrCode && (
<Button
borderRadius="10px"
data-testid={dataTestId}
mode="tertiary"
ml="tight"
onClick={onClickQrCode}
>
<QrCodeIcon color={color('text-caption')} size="14px" />
</Button>
)}
</Box>
</Stack>
</Flex>
</Flag>
);
}

View File

@@ -0,0 +1,18 @@
import { ButtonProps, Stack } from '@stacks/ui';
import { Caption } from '@app/components//typography';
interface ReceiveItemListProps extends ButtonProps {
children: React.ReactNode;
title?: string;
}
export function ReceiveItemList({ children, title }: ReceiveItemListProps) {
return (
<>
{title && <Caption>{title}</Caption>}
<Stack spacing="loose" mt="base" mb="extra-loose">
{children}
</Stack>
</>
);
}

View File

@@ -6,7 +6,7 @@ import { useCurrentAccountDisplayName } from '@app/common/hooks/account/use-acco
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { ReceiveTokensLayout } from './components/receive-tokens.layout';
import { ReceiveTokensLayout } from '../components/receive-tokens.layout';
// ts-unused-exports:disable-next-line
export function ReceiveTokens() {

View File

@@ -11,7 +11,11 @@ import { useNativeSegwitAccountIndexAddressIndexZero } from '@app/store/accounts
import { ReceiveBtcModalWarning } from './components/receive-btc-warning';
import { ReceiveTokensLayout } from './components/receive-tokens.layout';
export function ReceiveBtcModal() {
interface ReceiveBtcModalType {
type?: 'btc' | 'btc-stamp';
}
export function ReceiveBtcModal({ type = 'btc' }: ReceiveBtcModalType) {
const analytics = useAnalytics();
const { state } = useLocation();
@@ -33,7 +37,7 @@ export function ReceiveBtcModal() {
<ReceiveTokensLayout
address={btcAddress}
onCopyAddressToClipboard={copyToClipboard}
title="Bitcoin address"
title={type === 'btc-stamp' ? 'Bitcoin Stamps address' : 'Bitcoin address'}
warning={<ReceiveBtcModalWarning accountIndex={accountIndex} />}
hasSubtitle={false}
/>

View File

@@ -1,18 +0,0 @@
import { useNavigate } from 'react-router-dom';
import { Box } from '@stacks/ui';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { ReceiveCollectible } from '@app/components/receive/receive-collectible';
export function ReceiveCollectibleModal() {
const navigate = useNavigate();
return (
<BaseDrawer title="Add collectible" isShowing onClose={() => navigate('../')}>
<Box mx="extra-loose">
<ReceiveCollectible />
</Box>
</BaseDrawer>
);
}

View File

@@ -0,0 +1,105 @@
import toast from 'react-hot-toast';
import { useLocation, useNavigate } from 'react-router-dom';
import { Box, useClipboard } from '@stacks/ui';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import get from 'lodash.get';
import { RouteUrls } from '@shared/route-urls';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { BtcIcon } from '@app/components/icons/btc-icon';
import { BtcStampsIcon } from '@app/components/icons/btc-stamps-icon';
import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { ReceiveItem } from './components/receive-item';
import { ReceiveItemList } from './components/receive-items';
type ReceiveModal = 'full' | 'collectible';
interface ReceiveModalProps {
type?: 'full' | 'collectible';
}
export function ReceiveModal({ type = 'full' }: ReceiveModalProps) {
const analytics = useAnalytics();
const location = useLocation();
const navigate = useNavigate();
const btcAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const stxAddress = useCurrentAccountStxAddressState();
const accountIndex = get(location.state, 'accountIndex', undefined);
const btcAddressTaproot = useZeroIndexTaprootAddress(accountIndex);
const { onCopy: onCopyBtc } = useClipboard(btcAddressNativeSegwit);
const { onCopy: onCopyStx } = useClipboard(stxAddress);
const { onCopy: onCopyOrdinal } = useClipboard(btcAddressTaproot);
function copyToClipboard(copyHandler: () => void, tracker = 'copy_address_to_clipboard') {
void analytics.track(tracker);
toast.success('Copied to clipboard!');
copyHandler();
}
const title = type === 'full' ? 'Select asset to receive' : 'Add collectible';
return (
<BaseDrawer title={title} isShowing onClose={() => navigate('../')}>
<Box mx="extra-loose">
{type === 'full' && (
<ReceiveItemList title="Tokens">
<ReceiveItem
address={btcAddressNativeSegwit}
icon={<BtcIcon />}
dataTestId={HomePageSelectors.ReceiveBtcNativeSegwitQrCodeBtn}
onCopyAddress={() => copyToClipboard(onCopyBtc)}
onClickQrCode={() => navigate(RouteUrls.ReceiveBtc)}
title="Bitcoin"
/>
<ReceiveItem
address={stxAddress}
icon={<StxAvatar />}
dataTestId={HomePageSelectors.ReceiveStxQrCodeBtn}
onCopyAddress={() => copyToClipboard(onCopyStx)}
onClickQrCode={() => navigate(RouteUrls.ReceiveStx)}
title="Stacks"
/>
</ReceiveItemList>
)}
<ReceiveItemList title={type === 'full' ? 'Collectibles' : undefined}>
<ReceiveItem
address={btcAddressTaproot}
icon={<OrdinalIcon />}
dataTestId={HomePageSelectors.ReceiveBtcTaprootQrCodeBtn}
onCopyAddress={() =>
copyToClipboard(onCopyOrdinal, 'select_stamp_to_add_new_collectible')
}
onClickQrCode={() => {
void analytics.track('select_inscription_to_add_new_collectible');
navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddressTaproot } });
}}
title="Ordinal inscription"
/>
<ReceiveItem
address={btcAddressNativeSegwit}
icon={<BtcStampsIcon />}
onClickQrCode={() => navigate(RouteUrls.ReceiveBtcStamp)}
onCopyAddress={() => copyToClipboard(onCopyBtc, 'select_stamp_to_add_new_collectible')}
title="Bitcoin Stamp"
/>
<ReceiveItem
address={stxAddress}
icon={<StxAvatar />}
onCopyAddress={() => copyToClipboard(onCopyStx, 'select_nft_to_add_new_collectible')}
onClickQrCode={() => navigate(RouteUrls.ReceiveStx)}
title="Stacks NFT"
/>
</ReceiveItemList>
</Box>
</BaseDrawer>
);
}

View File

@@ -6,11 +6,14 @@ import get from 'lodash.get';
import { AverageBitcoinFeeRates, BtcFeeType } from '@shared/models/fees/bitcoin-fees.model';
import { SupportedInscription } from '@shared/models/inscription.model';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useSendInscriptionRouteState } from '../hooks/use-send-inscription-route-state';
import { createUtxoFromInscription } from './create-utxo-from-inscription';
import { SendInscriptionLoader } from './send-inscription-loader';
export interface SendInscriptionContextState {
interface SendInscriptionContextState {
feeRates: AverageBitcoinFeeRates;
inscription: SupportedInscription;
selectedFeeType: BtcFeeType;
@@ -25,10 +28,22 @@ export function useSendInscriptionState() {
export function SendInscriptionContainer() {
const [selectedFeeType, setSelectedFeeType] = useState<BtcFeeType | null>(null);
const [inscription, setInscription] = useState<SupportedInscription | null>(null);
const [utxo, setUtxo] = useState<TaprootUtxo | null>(null);
const routeState = useSendInscriptionRouteState();
useOnMount(() => {
if (!routeState.inscription) return;
setInscription(routeState.inscription);
setUtxo(createUtxoFromInscription(routeState.inscription));
});
if (!inscription || !utxo) return null;
return (
<SendInscriptionLoader>
{({ feeRates, inscription, utxo }) => (
{({ feeRates }) => (
<Outlet context={{ feeRates, inscription, selectedFeeType, setSelectedFeeType, utxo }} />
)}
</SendInscriptionLoader>

View File

@@ -1,25 +1,12 @@
import { Navigate } from 'react-router-dom';
import { RouteUrls } from '@shared/route-urls';
import { AverageBitcoinFeeRates } from '@shared/models/fees/bitcoin-fees.model';
import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useSendInscriptionRouteState } from '../hooks/use-send-inscription-route-state';
import { createUtxoFromInscription } from './create-utxo-from-inscription';
import { SendInscriptionContextState } from './send-inscription-container';
interface SendInscriptionLoaderProps {
children(data: Partial<SendInscriptionContextState>): React.JSX.Element;
children(data: { feeRates: AverageBitcoinFeeRates }): React.JSX.Element;
}
export function SendInscriptionLoader({ children }: SendInscriptionLoaderProps) {
const { inscription } = useSendInscriptionRouteState();
const { data: feeRates } = useAverageBitcoinFeeRates();
if (!feeRates) return null;
if (!inscription) return <Navigate to={RouteUrls.Home} />;
const utxo = createUtxoFromInscription(inscription);
return children({ inscription, feeRates, utxo });
return children({ feeRates });
}

View File

@@ -4,12 +4,9 @@ import get from 'lodash.get';
import { SupportedInscription } from '@shared/models/inscription.model';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
export function useSendInscriptionRouteState() {
const location = useLocation();
return {
inscription: get(location.state, 'inscription', null) as SupportedInscription | null,
utxo: get(location.state, 'utxo', null) as TaprootUtxo | null,
};
}

View File

@@ -30,6 +30,7 @@ function PageTopBase() {
data-testid={TransactionSigningSelectors.TxSigningPageContainer}
mb="loose"
spacing="base"
width="100%"
>
<Title as="h1" fontWeight="bold">
{pageTitle}

View File

@@ -2,11 +2,17 @@ export async function sendAcceptedBitcoinContractOfferToProtocolWallet(
acceptedBitcoinContractOffer: string,
counterpartyWalletURL: string
) {
return fetch(`${counterpartyWalletURL}/offer/accept`, {
const response = await fetch(`${counterpartyWalletURL}/offer/accept`, {
method: 'put',
body: JSON.stringify({
acceptMessage: acceptedBitcoinContractOffer,
}),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json());
});
if (!response.ok) {
throw new Error('The counterparty was unable to process the Bitcoin Contract.');
}
return response.json();
}

View File

@@ -35,11 +35,10 @@ import { BackUpSecretKeyPage } from '@app/pages/onboarding/back-up-secret-key/ba
import { SignIn } from '@app/pages/onboarding/sign-in/sign-in';
import { WelcomePage } from '@app/pages/onboarding/welcome/welcome';
import { PsbtRequest } from '@app/pages/psbt-request/psbt-request';
import { ReceiveBtcModal } from '@app/pages/receive-tokens/receive-btc';
import { ReceiveModal } from '@app/pages/receive-tokens/receive-modal';
import { ReceiveStxModal } from '@app/pages/receive-tokens/receive-stx';
import { ReceiveCollectibleModal } from '@app/pages/receive/receive-collectible/receive-collectible-modal';
import { ReceiveCollectibleOrdinal } from '@app/pages/receive/receive-collectible/receive-collectible-oridinal';
import { ReceiveBtcModal } from '@app/pages/receive/receive-btc';
import { ReceiveCollectibleOrdinal } from '@app/pages/receive/receive-collectible-oridinal';
import { ReceiveModal } from '@app/pages/receive/receive-modal';
import { ReceiveStxModal } from '@app/pages/receive/receive-stx';
import { RequestError } from '@app/pages/request-error/request-error';
import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses';
import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-transfer.routes';
@@ -213,13 +212,17 @@ function useAppRoutes() {
<Route path={RouteUrls.IncreaseFeeSent} element={<IncreaseFeeSentDrawer />} />
<Route path={RouteUrls.Receive} element={<ReceiveModal />} />
<Route path={RouteUrls.ReceiveCollectible} element={<ReceiveCollectibleModal />} />
<Route
path={RouteUrls.ReceiveCollectible}
element={<ReceiveModal type="collectible" />}
/>
<Route
path={RouteUrls.ReceiveCollectibleOrdinal}
element={<ReceiveCollectibleOrdinal />}
/>
<Route path={RouteUrls.ReceiveStx} element={<ReceiveStxModal />} />
<Route path={RouteUrls.ReceiveBtc} element={<ReceiveBtcModal />} />
<Route path={RouteUrls.ReceiveBtcStamp} element={<ReceiveBtcModal type="btc-stamp" />} />
<Route path={RouteUrls.SendOrdinalInscription} element={<SendInscriptionContainer />}>
<Route index element={<SendInscriptionForm />} />

View File

@@ -37,7 +37,7 @@ function setWalletEncryptionPassword(args: {
password,
});
await initalizeWalletSession(encryptionKey, secretKey);
await initalizeWalletSession(encryptionKey);
const legacyAccountActivityLookup =
await checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex(secretKey);
@@ -102,7 +102,7 @@ function unlockWalletAction(password: string): AppThunk {
if (!currentKey) return;
if (currentKey.type !== 'software') return;
const { secretKey, encryptionKey } = await decryptMnemonic({ password, ...currentKey });
await initalizeWalletSession(encryptionKey, secretKey);
await initalizeWalletSession(encryptionKey);
const rootKey = mnemonicToRootNode(secretKey);
if (!rootKey.publicKey) throw new Error('Could not derive root key from mnemonic');

View File

@@ -1,72 +1,32 @@
import { decrypt } from '@stacks/wallet-sdk';
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { whenBrowserRuntime } from '@shared/utils/get-browser-runtime';
import { logger } from '@shared/logger';
import { store } from '@app/store';
import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions';
import { selectCurrentKey } from '@app/store/keys/key.selectors';
import { defaultKeyId } from '@app/store/keys/key.slice';
export async function initalizeWalletSession(encryptionKey: string, secretKey: string) {
return await whenBrowserRuntime({
async chromium() {
return chrome.storage.session.set({ encryptionKey });
},
async firefox() {
return sendMessage({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey, keyId: defaultKeyId },
});
},
})();
export async function initalizeWalletSession(encryptionKey: string) {
return chrome.storage.session.set({ encryptionKey });
}
export async function clearWalletSession() {
return await whenBrowserRuntime({
async chromium() {
return chrome.storage.session.remove('encryptionKey');
},
async firefox() {
return chrome.runtime.sendMessage({ method: InternalMethods.RemoveInMemoryKeys });
},
})();
return chrome.storage.session.remove('encryptionKey');
}
export async function restoreWalletSession() {
return whenBrowserRuntime({
async chromium() {
const key = await chrome.storage.session.get(['encryptionKey']);
if (!key.encryptionKey) return false;
const key = await chrome.storage.session.get(['encryptionKey']);
try {
const currentKey = selectCurrentKey(store.getState());
if (!key.encryptionKey) return;
if (currentKey?.type === 'software') {
const secretKey = await decrypt(currentKey.encryptedSecretKey, key.encryptionKey);
store.dispatch(inMemoryKeyActions.setKeysInMemory({ default: secretKey }));
return true;
}
} catch (e) {
return false;
}
try {
const currentKey = selectCurrentKey(store.getState());
return false;
},
async firefox() {
return checkForInMemoryKeys();
},
})();
}
async function checkForInMemoryKeys() {
return new Promise(resolve =>
chrome.runtime.sendMessage({ method: InternalMethods.RequestInMemoryKeys }, resp => {
if (!resp) resolve(false);
if (Object.keys(resp).length === 0) return resolve(false);
store.dispatch(inMemoryKeyActions.setKeysInMemory(resp));
resolve(true);
})
);
if (currentKey?.type === 'software') {
const secretKey = await decrypt(currentKey.encryptedSecretKey, key.encryptionKey);
store.dispatch(inMemoryKeyActions.setKeysInMemory({ default: secretKey }));
}
} catch (e) {
logger.error('Failed to decrypt secret key');
}
}

View File

@@ -1,5 +1,4 @@
import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { BackgroundMessages } from '@shared/messages';
function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) {
@@ -7,8 +6,6 @@ function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender)
return sender.url?.startsWith(chrome.runtime.getURL(''));
}
const inMemoryKeys = new Map();
function makeFormStateKey(tabId: number) {
return 'form-state-' + tabId.toString();
}
@@ -31,23 +28,5 @@ export async function internalBackgroundMessageHandler(
return;
}
logger.debug('Internal message', message);
switch (message.method) {
case InternalMethods.ShareInMemoryKeyToBackground: {
const { keyId, secretKey } = message.payload;
inMemoryKeys.set(keyId, secretKey);
sendResponse();
break;
}
case InternalMethods.RequestInMemoryKeys: {
sendResponse(Object.fromEntries(inMemoryKeys));
break;
}
case InternalMethods.RemoveInMemoryKeys: {
inMemoryKeys.clear();
sendResponse();
break;
}
}
sendResponse();
}

View File

@@ -2,6 +2,7 @@ import { RpcErrorCode } from '@btckit/types';
import { RouteUrls } from '@shared/route-urls';
import { BitcoinContractRequest } from '@shared/rpc/methods/accept-bitcoin-contract';
import { BitcoinContractResponseStatus } from '@shared/rpc/methods/accept-bitcoin-contract';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
import {
@@ -16,12 +17,20 @@ export async function rpcAcceptBitcoinContractOffer(
message: BitcoinContractRequest,
port: chrome.runtime.Port
) {
if (!message.params) {
if (
!message.params ||
!message.params.bitcoinContractOffer ||
!message.params.attestorURLs ||
!message.params.counterpartyWalletDetails
) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('acceptBitcoinContractOffer', {
id: message.id,
error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid parameters' },
error: {
code: RpcErrorCode.INVALID_PARAMS,
message: 'The provided parameters are not valid.',
},
})
);
return;
@@ -29,26 +38,11 @@ export async function rpcAcceptBitcoinContractOffer(
const params: RequestParams = [
['bitcoinContractOffer', message.params.bitcoinContractOffer],
['counterpartyWalletURL', message.params.counterpartyWalletURL],
['counterpartyWalletName', message.params.counterpartyWalletName],
['counterpartyWalletIcon', message.params.counterpartyWalletIcon],
['attestorURLs', message.params.attestorURLs],
['counterpartyWalletDetails', message.params.counterpartyWalletDetails],
['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(
@@ -63,7 +57,7 @@ export async function rpcAcceptBitcoinContractOffer(
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected the offer',
message: BitcoinContractResponseStatus.REJECTED,
},
}),
});

View File

@@ -27,9 +27,6 @@ export enum ExternalMethods {
export enum InternalMethods {
RequestDerivedStxAccounts = 'RequestDerivedStxAccounts',
ShareInMemoryKeyToBackground = 'ShareInMemoryKeyToBackground',
RequestInMemoryKeys = 'RequestInMemoryKeys',
RemoveInMemoryKeys = 'RemoveInMemoryKeys',
OriginatingTabClosed = 'OriginatingTabClosed',
}

View File

@@ -8,25 +8,12 @@ type BackgroundMessage<Msg extends ExtensionMethods, Payload = undefined> = Omit
'source'
>;
type FirefoxShareInMemoryKeyToBackground = BackgroundMessage<
InternalMethods.ShareInMemoryKeyToBackground,
{ secretKey: string; keyId: string }
>;
type FirefoxRequestInMemoryKeys = BackgroundMessage<InternalMethods.RequestInMemoryKeys>;
type FirefoxRemoveInMemoryKeys = BackgroundMessage<InternalMethods.RemoveInMemoryKeys>;
type OriginatingTabClosed = BackgroundMessage<
InternalMethods.OriginatingTabClosed,
{ tabId: number }
>;
export type BackgroundMessages =
| FirefoxShareInMemoryKeyToBackground
| FirefoxRequestInMemoryKeys
| FirefoxRemoveInMemoryKeys
| OriginatingTabClosed;
export type BackgroundMessages = OriginatingTabClosed;
export function sendMessage(message: BackgroundMessages) {
return chrome.runtime.sendMessage(message);

View File

@@ -40,6 +40,7 @@ export enum RouteUrls {
ReceiveCollectibleOrdinal = '/receive/collectible/ordinal',
ReceiveStx = '/receive/stx',
ReceiveBtc = '/receive/btc',
ReceiveBtcStamp = '/receive/btc-stamp',
Send = '/send-transaction',
ViewSecretKey = '/view-secret-key',

View File

@@ -3,16 +3,21 @@ import { AllowAdditionalProperties } from '@btckit/types/dist/types/utils';
interface BitcoinContractResponseParams extends AllowAdditionalProperties {
bitcoinContractOffer: string;
counterpartyWalletURL: string;
counterpartyWalletName: string;
counterpartyWalletIcon: string;
attestorURLs: string;
counterpartyWalletDetails: string;
}
interface BitcoinContractResponseBody extends AllowAdditionalProperties {
contractId: string;
action: string;
contractId?: string;
txId?: string;
error?: string;
}
export enum BitcoinContractResponseStatus {
SUCCESS = 'Accepting Bitcoin Contract offer was successful',
BROADCAST_ERROR = 'There was an error while broadcasting the Bitcoin Contract transaction',
INTERFACE_ERROR = 'There was an error while interacting with the Bitcoin Contract interface',
NETWORK_ERROR = "The wallet's current selected network is not supported",
REJECTED = 'Bitcoin Contract offer was rejected',
}
export type BitcoinContractRequest = RpcRequest<

View File

@@ -1,11 +0,0 @@
type BrowserRuntime = 'chromium' | 'firefox';
function getBrowserRuntime(): BrowserRuntime {
return chrome.runtime.getURL('').startsWith('moz-extension://') ? 'firefox' : 'chromium';
}
type WhenBrowserRuntimeMap<T> = Record<BrowserRuntime, T>;
export function whenBrowserRuntime<T>(runtimeMap: WhenBrowserRuntimeMap<T>) {
return runtimeMap[getBrowserRuntime()];
}

View File

@@ -9763,10 +9763,10 @@ 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==
dlc-wasm-wallet@0.4.7:
version "0.4.7"
resolved "https://registry.yarnpkg.com/dlc-wasm-wallet/-/dlc-wasm-wallet-0.4.7.tgz#5c726ab83bf549d09e4ae8c7827ca63a9cff5060"
integrity sha512-Q5ZW3jknqFf99/pNFPE/ETK/Kvoo3M98mACkZJ+s8XVQ4rxRjmNw+9m9HumItdGDLGBNFzCsew1joNrwfkl9Ow==
dlv@^1.1.3:
version "1.1.3"