mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 09:34:37 +08:00
Merge pull request #4135 from hirosystems/release/receive-modal-updates
Release/receive modal updates
This commit is contained in:
51
.github/workflows/build-extension.yml
vendored
51
.github/workflows/build-extension.yml
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
6
src/app/components/icons/btc-stamps-icon.tsx
Normal file
6
src/app/components/icons/btc-stamps-icon.tsx
Normal 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" />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
54
src/app/pages/receive/components/receive-item.tsx
Normal file
54
src/app/pages/receive/components/receive-item.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
18
src/app/pages/receive/components/receive-items.tsx
Normal file
18
src/app/pages/receive/components/receive-items.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
105
src/app/pages/receive/receive-modal.tsx
Normal file
105
src/app/pages/receive/receive-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ function PageTopBase() {
|
||||
data-testid={TransactionSigningSelectors.TxSigningPageContainer}
|
||||
mb="loose"
|
||||
spacing="base"
|
||||
width="100%"
|
||||
>
|
||||
<Title as="h1" fontWeight="bold">
|
||||
{pageTitle}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -27,9 +27,6 @@ export enum ExternalMethods {
|
||||
|
||||
export enum InternalMethods {
|
||||
RequestDerivedStxAccounts = 'RequestDerivedStxAccounts',
|
||||
ShareInMemoryKeyToBackground = 'ShareInMemoryKeyToBackground',
|
||||
RequestInMemoryKeys = 'RequestInMemoryKeys',
|
||||
RemoveInMemoryKeys = 'RemoveInMemoryKeys',
|
||||
OriginatingTabClosed = 'OriginatingTabClosed',
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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()];
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user