mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-05-07 01:41:44 +08:00
Merge pull request #4041 from hirosystems/release/dlc-link
Release/dlc link
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ trace*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -141,9 +141,9 @@
|
||||
"@scure/bip32": "1.3.0",
|
||||
"@scure/bip39": "1.2.1",
|
||||
"@scure/btc-signer": "1.0.1",
|
||||
"@segment/analytics-next": "1.53.0",
|
||||
"@sentry/react": "7.57.0",
|
||||
"@sentry/tracing": "7.57.0",
|
||||
"@segment/analytics-next": "1.53.1",
|
||||
"@sentry/react": "7.59.2",
|
||||
"@sentry/tracing": "7.59.2",
|
||||
"@stacks/auth": "6.5.4",
|
||||
"@stacks/blockchain-api-client": "6.3.4",
|
||||
"@stacks/common": "6.5.2",
|
||||
@@ -183,6 +183,7 @@
|
||||
"coinselect": "3.1.13",
|
||||
"compare-versions": "4.1.3",
|
||||
"dayjs": "1.11.8",
|
||||
"dlc-wasm-wallet": "0.4.3",
|
||||
"dompurify": "3.0.4",
|
||||
"downshift": "6.1.7",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
|
||||
@@ -9,6 +9,10 @@ export function useShowFieldError(name: string) {
|
||||
const form = useFormikContext();
|
||||
const [_, meta] = useField(name);
|
||||
const isDirty = useIsFieldDirty(name);
|
||||
const isFieldInFocus = document.activeElement?.getAttribute('name') === name;
|
||||
|
||||
return (form.submitCount > 0 && meta.error) || (meta.touched && isDirty && meta.error);
|
||||
return (
|
||||
(form.submitCount > 0 && meta.error) ||
|
||||
(!isFieldInFocus && meta.touched && isDirty && meta.error)
|
||||
);
|
||||
}
|
||||
|
||||
239
src/app/common/hooks/use-bitcoin-contracts.ts
Normal file
239
src/app/common/hooks/use-bitcoin-contracts.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { bytesToHex } from '@stacks/common';
|
||||
import { JsDLCInterface } from 'dlc-wasm-wallet';
|
||||
|
||||
import { BITCOIN_API_BASE_URL_MAINNET, BITCOIN_API_BASE_URL_TESTNET } from '@shared/constants';
|
||||
import { deriveAddressIndexKeychainFromAccount } from '@shared/crypto/bitcoin/bitcoin.utils';
|
||||
import { createMoneyFromDecimal } from '@shared/models/money.model';
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
|
||||
|
||||
import { sendAcceptedBitcoinContractOfferToProtocolWallet } from '@app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer';
|
||||
import {
|
||||
useCalculateBitcoinFiatValue,
|
||||
useCryptoCurrencyMarketData,
|
||||
} from '@app/query/common/market-data/market-data.hooks';
|
||||
import { useCurrentAccountIndex } from '@app/store/accounts/account';
|
||||
import {
|
||||
useCurrentAccountNativeSegwitSigner,
|
||||
useNativeSegwitActiveNetworkAccountPrivateKeychain,
|
||||
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
|
||||
import { initialSearchParams } from '../initial-search-params';
|
||||
import { i18nFormatCurrency } from '../money/format-money';
|
||||
import { satToBtc } from '../money/unit-conversion';
|
||||
import { whenBitcoinNetwork } from '../utils';
|
||||
import { useDefaultRequestParams } from './use-default-request-search-params';
|
||||
|
||||
export interface SimplifiedBitcoinContract {
|
||||
bitcoinContractId: string;
|
||||
bitcoinContractCollateralAmount: number;
|
||||
bitcoinContractExpirationDate: string;
|
||||
}
|
||||
|
||||
interface CounterpartyWalletDetails {
|
||||
counterpartyWalletURL: string;
|
||||
counterpartyWalletName: string;
|
||||
counterpartyWalletIcon: string;
|
||||
}
|
||||
|
||||
export interface BitcoinContractOfferDetails {
|
||||
simplifiedBitcoinContract: SimplifiedBitcoinContract;
|
||||
counterpartyWalletDetails: CounterpartyWalletDetails;
|
||||
}
|
||||
|
||||
export function useBitcoinContracts() {
|
||||
const navigate = useNavigate();
|
||||
const defaultParams = useDefaultRequestParams();
|
||||
const bitcoinMarketData = useCryptoCurrencyMarketData('BTC');
|
||||
const calculateFiatValue = useCalculateBitcoinFiatValue();
|
||||
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
|
||||
const currentIndex = useCurrentAccountIndex();
|
||||
const nativeSegwitPrivateKeychain =
|
||||
useNativeSegwitActiveNetworkAccountPrivateKeychain()?.(currentIndex);
|
||||
const oracleAPI = 'https://testnet.dlc.link/oracle';
|
||||
|
||||
async function getBitcoinContractInterface(): Promise<JsDLCInterface | undefined> {
|
||||
const bitcoinAccountDetails = getNativeSegwitSigner?.(0);
|
||||
|
||||
if (!nativeSegwitPrivateKeychain || !bitcoinAccountDetails) return;
|
||||
|
||||
const currentBitcoinNetwork = bitcoinAccountDetails.network;
|
||||
const currentAddress = bitcoinAccountDetails.address;
|
||||
const currentAccountIndex = bitcoinAccountDetails.addressIndex;
|
||||
|
||||
const currentAddressPrivateKey = deriveAddressIndexKeychainFromAccount(
|
||||
nativeSegwitPrivateKeychain.keychain
|
||||
)(currentAccountIndex).privateKey;
|
||||
|
||||
if (!currentAddressPrivateKey) return;
|
||||
|
||||
const blockchainAPI = whenBitcoinNetwork(currentBitcoinNetwork)({
|
||||
mainnet: BITCOIN_API_BASE_URL_MAINNET,
|
||||
testnet: BITCOIN_API_BASE_URL_TESTNET,
|
||||
regtest: BITCOIN_API_BASE_URL_TESTNET,
|
||||
signet: BITCOIN_API_BASE_URL_TESTNET,
|
||||
});
|
||||
|
||||
const bitcoinContractInterface = JsDLCInterface.new(
|
||||
bytesToHex(currentAddressPrivateKey),
|
||||
currentAddress,
|
||||
currentBitcoinNetwork,
|
||||
blockchainAPI,
|
||||
oracleAPI
|
||||
);
|
||||
|
||||
return bitcoinContractInterface;
|
||||
}
|
||||
|
||||
function handleOffer(
|
||||
bitcoinContractOfferJSON: string,
|
||||
counterpartyWalletURL: string,
|
||||
counterpartyWalletName: string,
|
||||
counterpartyWalletIcon: string
|
||||
): BitcoinContractOfferDetails {
|
||||
const bitcoinContractOffer = JSON.parse(bitcoinContractOfferJSON);
|
||||
|
||||
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
|
||||
const bitcoinContractCollateralAmount =
|
||||
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
|
||||
const bitcoinContractExpirationDate = new Date(
|
||||
bitcoinContractOffer.cetLocktime * 1000
|
||||
).toLocaleDateString();
|
||||
|
||||
const simplifiedBitcoinContractOffer: SimplifiedBitcoinContract = {
|
||||
bitcoinContractId,
|
||||
bitcoinContractCollateralAmount,
|
||||
bitcoinContractExpirationDate,
|
||||
};
|
||||
|
||||
const counterpartyWalletDetails: CounterpartyWalletDetails = {
|
||||
counterpartyWalletURL,
|
||||
counterpartyWalletName,
|
||||
counterpartyWalletIcon,
|
||||
};
|
||||
|
||||
const bitcoinContractOfferDetails: BitcoinContractOfferDetails = {
|
||||
simplifiedBitcoinContract: simplifiedBitcoinContractOffer,
|
||||
counterpartyWalletDetails,
|
||||
};
|
||||
|
||||
return bitcoinContractOfferDetails;
|
||||
}
|
||||
|
||||
async function handleAccept(
|
||||
bitcoinContractJSON: string,
|
||||
counterpartyWalletDetails: CounterpartyWalletDetails
|
||||
) {
|
||||
let bitcoinContractInterface: JsDLCInterface | undefined;
|
||||
try {
|
||||
bitcoinContractInterface = await getBitcoinContractInterface();
|
||||
} catch (error) {
|
||||
navigate(RouteUrls.BitcoinContractLockError, {
|
||||
state: {
|
||||
error,
|
||||
title: 'There was an error with getting the bitcoin contract interface',
|
||||
body: 'Unable to setup interface',
|
||||
},
|
||||
});
|
||||
sendRpcResponse('none', '', 'failed');
|
||||
}
|
||||
|
||||
if (!bitcoinContractInterface) return;
|
||||
|
||||
const bitcoinContractOffer = JSON.parse(bitcoinContractJSON);
|
||||
|
||||
const bitcoinContractId = bitcoinContractOffer.temporaryContractId;
|
||||
const bitcoinContractCollateralAmount =
|
||||
bitcoinContractOffer.contractInfo.singleContractInfo.totalCollateral;
|
||||
|
||||
await bitcoinContractInterface.get_wallet_balance();
|
||||
|
||||
try {
|
||||
const acceptedBitcoinContract = await bitcoinContractInterface.accept_offer(
|
||||
bitcoinContractJSON
|
||||
);
|
||||
|
||||
const signedBitcoinContract = await sendAcceptedBitcoinContractOfferToProtocolWallet(
|
||||
acceptedBitcoinContract,
|
||||
counterpartyWalletDetails.counterpartyWalletURL
|
||||
);
|
||||
|
||||
const txId = await bitcoinContractInterface.countersign_and_broadcast(
|
||||
JSON.stringify(signedBitcoinContract)
|
||||
);
|
||||
|
||||
const { txMoney, txFiatValue, txFiatValueSymbol, txLink, symbol } = getTransactionDetails(
|
||||
txId,
|
||||
bitcoinContractCollateralAmount
|
||||
);
|
||||
|
||||
navigate(RouteUrls.BitcoinContractLockSuccess, {
|
||||
state: {
|
||||
txId,
|
||||
txMoney,
|
||||
txFiatValue,
|
||||
txFiatValueSymbol,
|
||||
symbol,
|
||||
txLink,
|
||||
},
|
||||
});
|
||||
|
||||
sendRpcResponse(bitcoinContractId, txId, 'accept');
|
||||
} catch (error) {
|
||||
navigate(RouteUrls.BitcoinContractLockError, {
|
||||
state: {
|
||||
error,
|
||||
title: 'There was an error with your Bitcoin Contract',
|
||||
body: 'Unable to lock bitcoin',
|
||||
},
|
||||
});
|
||||
sendRpcResponse(bitcoinContractId, '', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
function handleReject(bitcoinContractId: string) {
|
||||
sendRpcResponse(bitcoinContractId, '', 'reject');
|
||||
close();
|
||||
}
|
||||
|
||||
function getTransactionDetails(txId: string, bitcoinCollateral: number) {
|
||||
const bitcoinValue = satToBtc(bitcoinCollateral);
|
||||
const txMoney = createMoneyFromDecimal(bitcoinValue, 'BTC');
|
||||
const txFiatValue = i18nFormatCurrency(calculateFiatValue(txMoney)).toString();
|
||||
const txFiatValueSymbol = bitcoinMarketData.price.symbol;
|
||||
const txLink = { blockchain: 'bitcoin', txid: txId };
|
||||
|
||||
return {
|
||||
txId,
|
||||
txMoney,
|
||||
txFiatValue,
|
||||
txFiatValueSymbol,
|
||||
symbol: 'BTC',
|
||||
txLink,
|
||||
};
|
||||
}
|
||||
|
||||
function sendRpcResponse(bitcoinContractId: string, txId: string, action: string) {
|
||||
if (!defaultParams.tabId || !initialSearchParams.get('requestId')) return;
|
||||
|
||||
chrome.tabs.sendMessage(
|
||||
defaultParams.tabId,
|
||||
makeRpcSuccessResponse('acceptBitcoinContractOffer', {
|
||||
id: initialSearchParams.get('requestId') as string,
|
||||
result: {
|
||||
contractId: bitcoinContractId,
|
||||
txId,
|
||||
action,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
handleOffer,
|
||||
handleAccept,
|
||||
handleReject,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts';
|
||||
import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts';
|
||||
import { useOnMount } from '@app/common/hooks/use-on-mount';
|
||||
import { initialSearchParams } from '@app/common/initial-search-params';
|
||||
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
|
||||
|
||||
import { BitcoinContractOfferDetailsSimple } from './components/bitcoin-contract-offer/bitcoin-contract-offer-details';
|
||||
import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions';
|
||||
import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header';
|
||||
import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout';
|
||||
import { BitcoinContractRequestWarningLabel } from './components/bitcoin-contract-request-warning-label';
|
||||
|
||||
export function BitcoinContractRequest() {
|
||||
const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
|
||||
|
||||
const { handleOffer, handleAccept, handleReject } = useBitcoinContracts();
|
||||
|
||||
const [bitcoinContractJSON, setBitcoinContractJSON] = useState<string>();
|
||||
const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] =
|
||||
useState<BitcoinContractOfferDetails>();
|
||||
const [bitcoinAddress, setBitcoinAddress] = useState<string>();
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
|
||||
const handleAcceptClick = async () => {
|
||||
if (!bitcoinContractJSON || !bitcoinContractOfferDetails) return;
|
||||
|
||||
await handleAccept(bitcoinContractJSON, bitcoinContractOfferDetails.counterpartyWalletDetails);
|
||||
};
|
||||
|
||||
const handleRejectClick = async () => {
|
||||
if (!bitcoinContractOfferDetails) return;
|
||||
|
||||
await handleReject(bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractId);
|
||||
};
|
||||
|
||||
useOnMount(() => {
|
||||
const bitcoinContractOfferJSON = initialSearchParams.get('bitcoinContractOffer');
|
||||
const counterpartyWalletURL = initialSearchParams.get('counterpartyWalletURL');
|
||||
const counterpartyWalletName = initialSearchParams.get('counterpartyWalletName');
|
||||
const counterpartyWalletIcon = initialSearchParams.get('counterpartyWalletIcon');
|
||||
|
||||
if (
|
||||
!getNativeSegwitSigner ||
|
||||
!bitcoinContractOfferJSON ||
|
||||
!counterpartyWalletURL ||
|
||||
!counterpartyWalletName ||
|
||||
!counterpartyWalletIcon
|
||||
)
|
||||
return;
|
||||
|
||||
const currentBitcoinContractOfferDetails = handleOffer(
|
||||
bitcoinContractOfferJSON,
|
||||
counterpartyWalletURL,
|
||||
counterpartyWalletName,
|
||||
counterpartyWalletIcon
|
||||
);
|
||||
|
||||
const currentAddress = getNativeSegwitSigner(0).address;
|
||||
|
||||
setBitcoinContractJSON(bitcoinContractOfferJSON);
|
||||
setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails);
|
||||
setBitcoinAddress(currentAddress);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoading && bitcoinAddress && bitcoinContractOfferDetails && (
|
||||
<BitcoinContractRequestLayout>
|
||||
<BitcoinContractRequestHeader
|
||||
counterpartyWalletName={
|
||||
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName
|
||||
}
|
||||
counterpartyWalletIcon={
|
||||
bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletIcon
|
||||
}
|
||||
/>
|
||||
<BitcoinContractRequestWarningLabel
|
||||
appName={bitcoinContractOfferDetails.counterpartyWalletDetails.counterpartyWalletName}
|
||||
/>
|
||||
<BitcoinContractRequestActions
|
||||
isLoading={isLoading}
|
||||
bitcoinAddress={bitcoinAddress}
|
||||
requiredAmount={
|
||||
bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractCollateralAmount
|
||||
}
|
||||
onRejectBitcoinContractOffer={handleRejectClick}
|
||||
onAcceptBitcoinContractOffer={handleAcceptClick}
|
||||
/>
|
||||
<BitcoinContractOfferDetailsSimple
|
||||
bitcoinAddress={bitcoinAddress}
|
||||
bitcoinContractOffer={bitcoinContractOfferDetails.simplifiedBitcoinContract}
|
||||
/>
|
||||
</BitcoinContractRequestLayout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Flex } from '@stacks/ui';
|
||||
|
||||
import { Text } from '@app/components/typography';
|
||||
|
||||
interface BitcoinContractExpirationDateProps {
|
||||
expirationDate: string;
|
||||
}
|
||||
export function BitcoinContractExpirationDate({
|
||||
expirationDate,
|
||||
}: BitcoinContractExpirationDateProps) {
|
||||
return (
|
||||
<Flex p="loose" spacing="loose" width="100%" justifyContent="space-between">
|
||||
<Text fontSize={2} fontWeight="bold">
|
||||
Expiration Date
|
||||
</Text>
|
||||
<Text fontSize={2}>{expirationDate}</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { FiArrowUpRight, FiCopy } from 'react-icons/fi';
|
||||
|
||||
import { Box, Stack, Text, color, useClipboard } from '@stacks/ui';
|
||||
|
||||
import { BtcIcon } from '@app/components/icons/btc-icon';
|
||||
import { Flag } from '@app/components/layout/flag';
|
||||
import { SpaceBetween } from '@app/components/layout/space-between';
|
||||
import { Tooltip } from '@app/components/tooltip';
|
||||
|
||||
interface BitcoinContractLockAmountProps {
|
||||
hoverLabel?: string;
|
||||
image?: JSX.Element;
|
||||
subtitle?: string;
|
||||
subValue?: string;
|
||||
subValueAction?(): void;
|
||||
title?: string;
|
||||
value: string;
|
||||
}
|
||||
export function BitcoinContractLockAmount({
|
||||
hoverLabel,
|
||||
image,
|
||||
subtitle,
|
||||
subValue,
|
||||
subValueAction,
|
||||
title,
|
||||
value,
|
||||
}: BitcoinContractLockAmountProps) {
|
||||
const { onCopy, hasCopied } = useClipboard(hoverLabel ?? '');
|
||||
|
||||
return (
|
||||
<Flag img={image || <BtcIcon />} align="middle" width="100%">
|
||||
<SpaceBetween>
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
{title ? title : 'BTC'}
|
||||
</Text>
|
||||
<Text fontSize={2} fontWeight="500">
|
||||
{value}
|
||||
</Text>
|
||||
</SpaceBetween>
|
||||
<SpaceBetween mt="tight">
|
||||
{subtitle ? (
|
||||
<Tooltip
|
||||
disabled={!hoverLabel}
|
||||
hideOnClick={false}
|
||||
label={hasCopied ? 'Copied!' : hoverLabel}
|
||||
labelProps={{ wordWrap: 'break-word' }}
|
||||
maxWidth="230px"
|
||||
placement="bottom"
|
||||
>
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
as="button"
|
||||
color={color('text-caption')}
|
||||
display="flex"
|
||||
onClick={onCopy}
|
||||
type="button"
|
||||
>
|
||||
<Text color={color('text-caption')} fontSize={1} mr="extra-tight">
|
||||
{subtitle}
|
||||
</Text>
|
||||
{hoverLabel ? <FiCopy size="14px" /> : null}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{subValue ? (
|
||||
<Stack as="button" isInline onClick={subValueAction} spacing="extra-tight" type="button">
|
||||
<Text color={subValueAction ? color('accent') : color('text-caption')} fontSize={1}>
|
||||
{subValue}
|
||||
</Text>
|
||||
{subValueAction ? <FiArrowUpRight color={color('accent')} /> : null}
|
||||
</Stack>
|
||||
) : null}
|
||||
</SpaceBetween>
|
||||
</Flag>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts';
|
||||
|
||||
import { BitcoinContractExpirationDate } from './bitcoin-contract-expiration-date';
|
||||
import { BitcoinContractOfferInput } from './bitcoin-contract-offer-input';
|
||||
|
||||
interface BitcoinContractOfferDetailsSimpleProps {
|
||||
bitcoinAddress: string;
|
||||
bitcoinContractOffer: SimplifiedBitcoinContract;
|
||||
}
|
||||
export function BitcoinContractOfferDetailsSimple({
|
||||
bitcoinAddress,
|
||||
bitcoinContractOffer,
|
||||
}: BitcoinContractOfferDetailsSimpleProps) {
|
||||
return (
|
||||
<>
|
||||
<BitcoinContractOfferInput
|
||||
addressNativeSegwit={bitcoinAddress}
|
||||
bitcoinContractOffer={bitcoinContractOffer}
|
||||
/>
|
||||
<BitcoinContractExpirationDate
|
||||
expirationDate={bitcoinContractOffer.bitcoinContractExpirationDate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Stack, Text } from '@stacks/ui';
|
||||
import { truncateMiddle } from '@stacks/ui-utils';
|
||||
|
||||
import { createMoneyFromDecimal } from '@shared/models/money.model';
|
||||
|
||||
import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts';
|
||||
import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money';
|
||||
import { satToBtc } from '@app/common/money/unit-conversion';
|
||||
import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks';
|
||||
|
||||
import { BitcoinContractLockAmount } from './bitcoin-contract-lock-amount';
|
||||
|
||||
interface BitcoinContractOfferInputProps {
|
||||
addressNativeSegwit: string;
|
||||
bitcoinContractOffer: SimplifiedBitcoinContract;
|
||||
}
|
||||
export function BitcoinContractOfferInput({
|
||||
addressNativeSegwit,
|
||||
bitcoinContractOffer,
|
||||
}: BitcoinContractOfferInputProps) {
|
||||
const calculateFiatValue = useCalculateBitcoinFiatValue();
|
||||
|
||||
const bitcoinValue = satToBtc(bitcoinContractOffer.bitcoinContractCollateralAmount);
|
||||
const money = createMoneyFromDecimal(bitcoinValue, 'BTC');
|
||||
const fiatValue = calculateFiatValue(money);
|
||||
const formattedBitcoinValue = formatMoney(money);
|
||||
const formattedFiatValue = i18nFormatCurrency(fiatValue);
|
||||
|
||||
return (
|
||||
<Stack p="loose" spacing="loose" width="100%">
|
||||
<Text fontWeight="bold">Amount</Text>
|
||||
<BitcoinContractLockAmount
|
||||
hoverLabel={addressNativeSegwit}
|
||||
subtitle={truncateMiddle(addressNativeSegwit)}
|
||||
subValue={`${formattedFiatValue} USD`}
|
||||
value={formattedBitcoinValue}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Box, Button, Stack, color } from '@stacks/ui';
|
||||
|
||||
import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance';
|
||||
import { PrimaryButton } from '@app/components/primary-button';
|
||||
|
||||
interface BitcoinContractRequestActionsProps {
|
||||
isLoading: boolean;
|
||||
bitcoinAddress: string;
|
||||
requiredAmount: number;
|
||||
onRejectBitcoinContractOffer(): Promise<void>;
|
||||
onAcceptBitcoinContractOffer(): Promise<void>;
|
||||
}
|
||||
export function BitcoinContractRequestActions({
|
||||
isLoading,
|
||||
bitcoinAddress,
|
||||
requiredAmount,
|
||||
onRejectBitcoinContractOffer,
|
||||
onAcceptBitcoinContractOffer,
|
||||
}: BitcoinContractRequestActionsProps) {
|
||||
const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress);
|
||||
const canAccept = btcAvailableAssetBalance.balance.amount.isGreaterThan(requiredAmount);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={color('bg')}
|
||||
borderTop="1px solid #DCDDE2"
|
||||
bottom="0px"
|
||||
height="96px"
|
||||
position="fixed"
|
||||
px="loose"
|
||||
width="100%"
|
||||
zIndex={999}
|
||||
>
|
||||
<Stack isInline mt="loose" spacing="base">
|
||||
<Button
|
||||
borderRadius="10px"
|
||||
flexGrow={1}
|
||||
mode="tertiary"
|
||||
onClick={onRejectBitcoinContractOffer}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<PrimaryButton
|
||||
borderRadius="10px"
|
||||
flexGrow={1}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!canAccept}
|
||||
onClick={onAcceptBitcoinContractOffer}
|
||||
>
|
||||
Accept
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { Flex } from '@stacks/ui';
|
||||
|
||||
import { Flag } from '@app/components/layout/flag';
|
||||
import { Caption, Title } from '@app/components/typography';
|
||||
|
||||
interface BitcoinContractRequestHeaderBaseProps {
|
||||
counterpartyWalletIcon: string;
|
||||
counterpartyWalletName: string;
|
||||
}
|
||||
|
||||
function BitcoinContractRequestHeaderBase({
|
||||
counterpartyWalletName,
|
||||
counterpartyWalletIcon,
|
||||
}: BitcoinContractRequestHeaderBaseProps) {
|
||||
const caption = `${counterpartyWalletName} is requesting you accept this offer`;
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" my="loose" width="100%">
|
||||
<Title fontSize={4} fontWeight="bold" mb="base">
|
||||
Lock Bitcoin
|
||||
</Title>
|
||||
{caption && (
|
||||
<Flag
|
||||
align="middle"
|
||||
img={<img src={counterpartyWalletIcon} height="32px" width="32px" />}
|
||||
pl="tight"
|
||||
>
|
||||
<Caption>{caption}</Caption>
|
||||
</Flag>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export const BitcoinContractRequestHeader = memo(BitcoinContractRequestHeaderBase);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Stack } from '@stacks/ui';
|
||||
|
||||
interface BitcoinContractRequestLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function BitcoinContractRequestLayout({ children }: BitcoinContractRequestLayoutProps) {
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
maxHeight="calc(100vh - 72px)"
|
||||
overflowY="scroll"
|
||||
pb="120px"
|
||||
px="loose"
|
||||
spacing="tight"
|
||||
width="100%"
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { WarningLabel } from '@app/components/warning-label';
|
||||
|
||||
export function BitcoinContractRequestWarningLabel(props: { appName?: string }) {
|
||||
const { appName } = props;
|
||||
const title = `Do not proceed unless you trust ${appName ?? 'Unknown'}!`;
|
||||
|
||||
return (
|
||||
<WarningLabel title={title} width="100%">
|
||||
By signing the contract YOU AGREE TO LOCK YOUR BITCOIN with {appName} into a contract where it
|
||||
will remain until a triggering event will release it.
|
||||
</WarningLabel>
|
||||
);
|
||||
}
|
||||
@@ -11,16 +11,18 @@ export function BroadcastError() {
|
||||
const { state } = useLocation();
|
||||
const analytics = useAnalytics();
|
||||
const msg = get(state, 'error.message', 'Unknown error response');
|
||||
const title = get(state, 'title', 'There was an error broadcasting your transaction');
|
||||
const body = get(state, 'body', 'Unable to broadcast transaction');
|
||||
|
||||
useOnMount(() => void analytics.track('bitcoin_broadcast_tx_error', { msg }));
|
||||
useOnMount(() => void analytics.track('bitcoin_contract_error', { msg }));
|
||||
|
||||
return (
|
||||
<BroadcastErrorLayout
|
||||
my="loose"
|
||||
textAlign="center"
|
||||
errorPayload={msg}
|
||||
title="There was an error broadcasting your transaction"
|
||||
body="Unable to broadcast transaction"
|
||||
title={title}
|
||||
body={body}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { FiCheck, FiCopy, FiExternalLink } from 'react-icons/fi';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Stack, useClipboard } from '@stacks/ui';
|
||||
import { Text } from '@stacks/ui';
|
||||
|
||||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
|
||||
import { useExplorerLink } from '@app/common/hooks/use-explorer-link';
|
||||
import { useRouteHeader } from '@app/common/hooks/use-route-header';
|
||||
import { satToBtc } from '@app/common/money/unit-conversion';
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardAssetValue,
|
||||
InfoCardBtn,
|
||||
InfoCardFooter,
|
||||
} from '@app/components/info-card/info-card';
|
||||
import { ModalHeader } from '@app/components/modal-header';
|
||||
|
||||
export function LockBitcoinSummary() {
|
||||
const { state } = useLocation();
|
||||
|
||||
const { txId, txMoney, txFiatValue, txFiatValueSymbol, symbol, txLink } = state;
|
||||
|
||||
const { onCopy } = useClipboard(txId);
|
||||
const { handleOpenTxLink } = useExplorerLink();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
function onClickLink() {
|
||||
void analytics.track('view_transaction_confirmation', { symbol: 'BTC' });
|
||||
handleOpenTxLink(txLink);
|
||||
}
|
||||
|
||||
function onClickCopy() {
|
||||
onCopy();
|
||||
toast.success('ID copied!');
|
||||
}
|
||||
|
||||
useRouteHeader(<ModalHeader hideActions defaultClose title="Locked Bitcoin" />);
|
||||
|
||||
return (
|
||||
<InfoCard>
|
||||
<InfoCardAssetValue
|
||||
value={Number(satToBtc(txMoney.amount))}
|
||||
fiatValue={txFiatValue}
|
||||
fiatSymbol={txFiatValueSymbol}
|
||||
symbol={symbol}
|
||||
icon={FiCheck}
|
||||
my="loose"
|
||||
px="loose"
|
||||
/>
|
||||
<Text fontSize={2} fontWeight={200} padding={'25px'} textAlign={'justify'}>
|
||||
<span style={{ fontWeight: 500 }}>Success!</span> Your bitcoin has been locked securely. All
|
||||
that's left is for it to be confirmed on the blockchain. After confirmation, you can proceed
|
||||
with borrowing against it.
|
||||
</Text>
|
||||
<InfoCardFooter>
|
||||
<Stack spacing="base" isInline width="100%">
|
||||
<InfoCardBtn onClick={onClickLink} icon={FiExternalLink} label="View Details" />
|
||||
<InfoCardBtn onClick={onClickCopy} icon={FiCopy} label="Copy ID" />
|
||||
</Stack>
|
||||
</InfoCardFooter>
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export async function sendAcceptedBitcoinContractOfferToProtocolWallet(
|
||||
acceptedBitcoinContractOffer: string,
|
||||
counterpartyWalletURL: string
|
||||
) {
|
||||
return fetch(`${counterpartyWalletURL}/offer/accept`, {
|
||||
method: 'put',
|
||||
body: JSON.stringify({
|
||||
acceptMessage: acceptedBitcoinContractOffer,
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { microStxToStx } from '@app/common/money/unit-conversion';
|
||||
import { useTransactionsById } from '@app/query/stacks/transactions/transactions-by-id.query';
|
||||
import { useCurrentAccountStxAddressState } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
|
||||
|
||||
import { useStacksConfirmedTransactions } from '../transactions/transactions-with-transfers.hooks';
|
||||
import { useAccountMempoolQuery } from './mempool.query';
|
||||
|
||||
const droppedCache = new Map();
|
||||
@@ -65,10 +66,14 @@ export function useCurrentAccountMempool() {
|
||||
export function useCurrentAccountMempoolTransactionsBalance() {
|
||||
const address = useCurrentAccountStxAddressState();
|
||||
const { transactions: pendingTransactions } = useStacksPendingTransactions();
|
||||
const confirmedTxs = useStacksConfirmedTransactions();
|
||||
|
||||
const pendingOutboundTxs = pendingTransactions.filter(
|
||||
tx => tx.tx_type === 'token_transfer' && tx.sender_address === address
|
||||
) as unknown as MempoolTokenTransferTransaction[];
|
||||
const pendingOutboundTxs = pendingTransactions.filter(tx => {
|
||||
if (confirmedTxs.some(confirmedTx => confirmedTx.nonce === tx.nonce)) {
|
||||
return false;
|
||||
}
|
||||
return tx.tx_type === 'token_transfer' && tx.sender_address === address;
|
||||
}) as unknown as MempoolTokenTransferTransaction[];
|
||||
|
||||
const tokenTransferTxsBalance = pendingOutboundTxs.reduce(
|
||||
(acc, tx) => acc.plus(tx.token_transfer.amount),
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AddNetwork } from '@app/features/message-signer/add-network/add-network
|
||||
import { RetrieveTaprootToNativeSegwit } from '@app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit';
|
||||
import { ThemesDrawer } from '@app/features/theme-drawer/theme-drawer';
|
||||
import { AllowDiagnosticsPage } from '@app/pages/allow-diagnostics/allow-diagnostics';
|
||||
import { BitcoinContractRequest } from '@app/pages/bitcoin-contract-request/bitcoin-contract-request';
|
||||
import { ChooseAccount } from '@app/pages/choose-account/choose-account';
|
||||
import { FundPage } from '@app/pages/fund/fund';
|
||||
import { Home } from '@app/pages/home/home';
|
||||
@@ -42,6 +43,7 @@ import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-tra
|
||||
import { RpcSignPsbt } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt';
|
||||
import { SelectNetwork } from '@app/pages/select-network/select-network';
|
||||
import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error';
|
||||
import { LockBitcoinSummary } from '@app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary';
|
||||
import { SendInscriptionContainer } from '@app/pages/send/ordinal-inscription/components/send-inscription-container';
|
||||
import { SendInscriptionChooseFee } from '@app/pages/send/ordinal-inscription/send-inscription-choose-fee';
|
||||
import { SendInscriptionForm } from '@app/pages/send/ordinal-inscription/send-inscription-form';
|
||||
@@ -136,6 +138,18 @@ function useAppRoutes() {
|
||||
{settingsModalRoutes}
|
||||
{ledgerStacksTxSigningRoutes}
|
||||
</Route>
|
||||
<Route
|
||||
path={RouteUrls.RpcReceiveBitcoinContractOffer}
|
||||
element={
|
||||
<AccountGate>
|
||||
<Suspense fallback={<LoadingSpinner height="600px" />}>
|
||||
<BitcoinContractRequest />
|
||||
</Suspense>
|
||||
</AccountGate>
|
||||
}
|
||||
></Route>
|
||||
<Route path={RouteUrls.BitcoinContractLockSuccess} element={<LockBitcoinSummary />} />
|
||||
<Route path={RouteUrls.BitcoinContractLockError} element={<BroadcastError />} />
|
||||
<Route
|
||||
path={RouteUrls.Onboarding}
|
||||
element={
|
||||
|
||||
@@ -52,7 +52,7 @@ const selectNativeSegwitActiveNetworkAccountPrivateKeychain = createSelector(
|
||||
})
|
||||
);
|
||||
|
||||
function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
|
||||
export function useNativeSegwitActiveNetworkAccountPrivateKeychain() {
|
||||
return useSelector(selectNativeSegwitActiveNetworkAccountPrivateKeychain);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { AddressVersion } from '@stacks/transactions';
|
||||
|
||||
import { decryptMnemonic, encryptMnemonic } from '@shared/crypto/mnemonic-encryption';
|
||||
import { logger } from '@shared/logger';
|
||||
import { identifyUser } from '@shared/utils/analytics';
|
||||
|
||||
import { recurseAccountsForActivity } from '@app/common/account-restoration/account-restore';
|
||||
import { checkForLegacyGaiaConfigWithKnownGeneratedAccountIndex } from '@app/common/account-restoration/legacy-gaia-config-lookup';
|
||||
import { mnemonicToRootNode } from '@app/common/keychain/keychain';
|
||||
import { BitcoinClient } from '@app/query/bitcoin/bitcoin-client';
|
||||
import { fetchNamesForAddress } from '@app/query/stacks/bns/bns.utils';
|
||||
import { StacksClient } from '@app/query/stacks/stacks-client';
|
||||
@@ -100,8 +102,12 @@ function unlockWalletAction(password: string): AppThunk {
|
||||
if (!currentKey) return;
|
||||
if (currentKey.type !== 'software') return;
|
||||
const { secretKey, encryptionKey } = await decryptMnemonic({ password, ...currentKey });
|
||||
|
||||
await initalizeWalletSession(encryptionKey, secretKey);
|
||||
|
||||
const rootKey = mnemonicToRootNode(secretKey);
|
||||
if (!rootKey.publicKey) throw new Error('Could not derive root key from mnemonic');
|
||||
void identifyUser(rootKey.publicKey);
|
||||
|
||||
dispatch(inMemoryKeySlice.actions.setKeysInMemory({ default: secretKey }));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RpcErrorCode } from '@btckit/types';
|
||||
import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
|
||||
|
||||
import { getTabIdFromPort } from './messaging-utils';
|
||||
import { rpcAcceptBitcoinContractOffer } from './rpc-methods/accept-bitcoin-contract';
|
||||
import { rpcGetAddresses } from './rpc-methods/get-addresses';
|
||||
import { rpcSendTransfer } from './rpc-methods/send-transfer';
|
||||
import { rpcSignMessage } from './rpc-methods/sign-message';
|
||||
@@ -36,6 +37,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
|
||||
break;
|
||||
}
|
||||
|
||||
case 'acceptBitcoinContractOffer': {
|
||||
await rpcAcceptBitcoinContractOffer(message, port);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
chrome.tabs.sendMessage(
|
||||
getTabIdFromPort(port),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { RpcErrorCode } from '@btckit/types';
|
||||
|
||||
import { RouteUrls } from '@shared/route-urls';
|
||||
import { BitcoinContractRequest } from '@shared/rpc/methods/accept-bitcoin-contract';
|
||||
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
|
||||
|
||||
import {
|
||||
RequestParams,
|
||||
getTabIdFromPort,
|
||||
listenForPopupClose,
|
||||
makeSearchParamsWithDefaults,
|
||||
triggerRequestWindowOpen,
|
||||
} from '../messaging-utils';
|
||||
|
||||
export async function rpcAcceptBitcoinContractOffer(
|
||||
message: BitcoinContractRequest,
|
||||
port: chrome.runtime.Port
|
||||
) {
|
||||
if (!message.params) {
|
||||
chrome.tabs.sendMessage(
|
||||
getTabIdFromPort(port),
|
||||
makeRpcErrorResponse('acceptBitcoinContractOffer', {
|
||||
id: message.id,
|
||||
error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid parameters' },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const params: RequestParams = [
|
||||
['bitcoinContractOffer', message.params.bitcoinContractOffer],
|
||||
['counterpartyWalletURL', message.params.counterpartyWalletURL],
|
||||
['counterpartyWalletName', message.params.counterpartyWalletName],
|
||||
['counterpartyWalletIcon', message.params.counterpartyWalletIcon],
|
||||
['requestId', message.id],
|
||||
];
|
||||
|
||||
if (message.params.bitcoinContractOffer.includes('Invalid state: Not enough fund in utxos')) {
|
||||
chrome.tabs.sendMessage(
|
||||
getTabIdFromPort(port),
|
||||
makeRpcErrorResponse('acceptBitcoinContractOffer', {
|
||||
id: message.id,
|
||||
error: {
|
||||
code: RpcErrorCode.INVALID_REQUEST,
|
||||
message: 'The counterparty does not have enough funds to complete the offer',
|
||||
},
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, params);
|
||||
|
||||
const { id } = await triggerRequestWindowOpen(
|
||||
RouteUrls.RpcReceiveBitcoinContractOffer,
|
||||
urlParams
|
||||
);
|
||||
|
||||
listenForPopupClose({
|
||||
tabId,
|
||||
id,
|
||||
response: makeRpcErrorResponse('acceptBitcoinContractOffer', {
|
||||
id: message.id,
|
||||
error: {
|
||||
code: RpcErrorCode.USER_REJECTION,
|
||||
message: 'User rejected the offer',
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -128,8 +128,9 @@ document.addEventListener(DomEventName.psbtRequest, ((event: PsbtRequestEvent) =
|
||||
});
|
||||
}) as EventListener);
|
||||
|
||||
// Inject inpage script (Stacks Provider)
|
||||
window.addEventListener('load', () => {
|
||||
const inpage = document.createElement('script');
|
||||
inpage.src = chrome.runtime.getURL('inpage.js');
|
||||
inpage.id = 'stacks-wallet-provider';
|
||||
document.body.appendChild(inpage);
|
||||
});
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
export enum DomEventName {
|
||||
request = 'request',
|
||||
authenticationRequest = 'stacksAuthenticationRequest',
|
||||
signatureRequest = 'signatureRequest',
|
||||
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
|
||||
transactionRequest = 'stacksTransactionRequest',
|
||||
profileUpdateRequest = 'profileUpdateRequest',
|
||||
psbtRequest = 'psbtRequest',
|
||||
authenticationRequest = 'hiroWalletStacksAuthenticationRequest',
|
||||
signatureRequest = 'hiroWalletSignatureRequest',
|
||||
structuredDataSignatureRequest = 'hiroWalletStructuredDataSignatureRequest',
|
||||
transactionRequest = 'hiroWalletStacksTransactionRequest',
|
||||
profileUpdateRequest = 'hiroWalletProfileUpdateRequest',
|
||||
psbtRequest = 'hiroWalletPsbtRequest',
|
||||
}
|
||||
|
||||
export interface AuthenticationRequestEventDetails {
|
||||
|
||||
@@ -11,18 +11,18 @@ export const MESSAGE_SOURCE = 'stacks-wallet' as const;
|
||||
export const CONTENT_SCRIPT_PORT = 'content-script' as const;
|
||||
|
||||
export enum ExternalMethods {
|
||||
transactionRequest = 'transactionRequest',
|
||||
transactionResponse = 'transactionResponse',
|
||||
authenticationRequest = 'authenticationRequest',
|
||||
authenticationResponse = 'authenticationResponse',
|
||||
signatureRequest = 'signatureRequest',
|
||||
signatureResponse = 'signatureResponse',
|
||||
structuredDataSignatureRequest = 'structuredDataSignatureRequest',
|
||||
structuredDataSignatureResponse = 'structuredDataSignatureResponse',
|
||||
profileUpdateRequest = 'profileUpdateRequest',
|
||||
profileUpdateResponse = 'profileUpdateResponse',
|
||||
psbtRequest = 'psbtRequest',
|
||||
psbtResponse = 'psbtResponse',
|
||||
transactionRequest = 'hiroWalletTransactionRequest',
|
||||
transactionResponse = 'hiroWalletTransactionResponse',
|
||||
authenticationRequest = 'hiroWalletAuthenticationRequest',
|
||||
authenticationResponse = 'hiroWalletAuthenticationResponse',
|
||||
signatureRequest = 'hiroWalletSignatureRequest',
|
||||
signatureResponse = 'hiroWalletSignatureResponse',
|
||||
structuredDataSignatureRequest = 'hiroWalletStructuredDataSignatureRequest',
|
||||
structuredDataSignatureResponse = 'hiroWalletStructuredDataSignatureResponse',
|
||||
profileUpdateRequest = 'hiroWalletProfileUpdateRequest',
|
||||
profileUpdateResponse = 'hiroWalletProfileUpdateResponse',
|
||||
psbtRequest = 'hiroWalletPsbtRequest',
|
||||
psbtResponse = 'hiroWalletPsbtResponse',
|
||||
}
|
||||
|
||||
export enum InternalMethods {
|
||||
|
||||
@@ -52,6 +52,11 @@ export enum RouteUrls {
|
||||
// Locked wallet route
|
||||
Unlock = '/unlock',
|
||||
|
||||
// Bitcoin Contract routes
|
||||
BitcoinContractLockSuccess = '/bitcoin-contract-lock-success',
|
||||
BitcoinContractLockError = '/bitcoin-contract-lock-error',
|
||||
BitcoinContractList = '/bitcoin-contract-list',
|
||||
|
||||
// Modal routes
|
||||
ChangeTheme = 'change-theme',
|
||||
EditNonce = 'edit-nonce',
|
||||
@@ -92,5 +97,6 @@ export enum RouteUrls {
|
||||
RpcSendTransferChooseFee = '/send-transfer/choose-fee',
|
||||
RpcSendTransferConfirmation = '/send-transfer/confirm',
|
||||
RpcSendTransferSummary = '/send-transfer/summary',
|
||||
RpcReceiveBitcoinContractOffer = '/bitcoin-contract-offer/:bitcoinContractOffer/:counterpartyWalletURL',
|
||||
RpcSignBip322Message = '/sign-bip322-message',
|
||||
}
|
||||
|
||||
26
src/shared/rpc/methods/accept-bitcoin-contract.ts
Normal file
26
src/shared/rpc/methods/accept-bitcoin-contract.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { DefineRpcMethod, RpcRequest, RpcResponse } from '@btckit/types';
|
||||
import { AllowAdditionaProperties } from '@btckit/types/dist/types/utils';
|
||||
|
||||
interface BitcoinContractResponseParams extends AllowAdditionaProperties {
|
||||
bitcoinContractOffer: string;
|
||||
counterpartyWalletURL: string;
|
||||
counterpartyWalletName: string;
|
||||
counterpartyWalletIcon: string;
|
||||
}
|
||||
|
||||
interface BitcoinContractResponseBody extends AllowAdditionaProperties {
|
||||
contractId: string;
|
||||
action: string;
|
||||
txId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type BitcoinContractRequest = RpcRequest<
|
||||
'acceptBitcoinContractOffer',
|
||||
BitcoinContractResponseParams
|
||||
>;
|
||||
type BitcoinContractResponse = RpcResponse<BitcoinContractResponseBody>;
|
||||
export type AcceptBitcoinContract = DefineRpcMethod<
|
||||
BitcoinContractRequest,
|
||||
BitcoinContractResponse
|
||||
>;
|
||||
@@ -2,11 +2,12 @@ import { BtcKitMethodMap, ExtractErrorResponse, ExtractSuccessResponse } from '@
|
||||
|
||||
import { ValueOf } from '@shared/utils/type-utils';
|
||||
|
||||
import { AcceptBitcoinContract } from './methods/accept-bitcoin-contract';
|
||||
import { SignPsbt } from './methods/sign-psbt';
|
||||
import { SupportedMethods } from './methods/supported-methods';
|
||||
|
||||
// Supports BtcKit methods, as well as custom Hiro Wallet methods
|
||||
export type WalletMethodMap = BtcKitMethodMap & SupportedMethods & SignPsbt;
|
||||
export type WalletMethodMap = BtcKitMethodMap & SupportedMethods & SignPsbt & AcceptBitcoinContract;
|
||||
|
||||
export type WalletRequests = ValueOf<WalletMethodMap>['request'];
|
||||
export type WalletResponses = ValueOf<WalletMethodMap>['response'];
|
||||
|
||||
22
src/shared/utils/analytics.spec.ts
Normal file
22
src/shared/utils/analytics.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { deriveAnalyticsIdentifier } from './analytics';
|
||||
|
||||
describe(deriveAnalyticsIdentifier.name, () => {
|
||||
test('the derivation of the users identifier', () =>
|
||||
expect(deriveAnalyticsIdentifier(Uint8Array.from([1, 2, 3]))).toBe('T49D6FxBz57'));
|
||||
|
||||
const expectedLength = 11;
|
||||
|
||||
function createRandomValue() {
|
||||
return window.crypto.getRandomValues(new Uint8Array());
|
||||
}
|
||||
|
||||
test.each([
|
||||
createRandomValue(),
|
||||
createRandomValue(),
|
||||
createRandomValue(),
|
||||
createRandomValue(),
|
||||
createRandomValue(),
|
||||
])('random test value %#', value => {
|
||||
expect(deriveAnalyticsIdentifier(value).length).toBe(expectedLength);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
import { ripemd160 } from '@noble/hashes/ripemd160';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { base58 } from '@scure/base';
|
||||
import { AnalyticsBrowser } from '@segment/analytics-next';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { getStoredState } from 'redux-persist';
|
||||
@@ -33,6 +36,16 @@ export function initAnalytics() {
|
||||
);
|
||||
}
|
||||
|
||||
// Used to create a unique identifier for a user's key in base58.
|
||||
// K = ripemd160(sha256(publicKey))[:8]
|
||||
export function deriveAnalyticsIdentifier(publicKey: Uint8Array) {
|
||||
return base58.encode(ripemd160(sha256(publicKey)).slice(0, 8));
|
||||
}
|
||||
|
||||
export async function identifyUser(publicKey: Uint8Array) {
|
||||
return analytics.identify(deriveAnalyticsIdentifier(publicKey));
|
||||
}
|
||||
|
||||
export function initSentry() {
|
||||
if (IS_TEST_ENV || !SENTRY_DSN) return;
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ const config = {
|
||||
runtimeChunk: false,
|
||||
},
|
||||
module: {
|
||||
noParse: /argon2\.wasm$/,
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
@@ -158,7 +159,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
test: /argon2\.wasm$/,
|
||||
// Tells WebPack that this module should be included as
|
||||
// base64-encoded binary file and not as code
|
||||
loader: 'base64-loader',
|
||||
@@ -168,6 +169,13 @@ const config = {
|
||||
// Error: WebAssembly module is included in initial chunk.
|
||||
type: 'javascript/auto',
|
||||
},
|
||||
{
|
||||
test: /cfddlcjs_wasm\.wasm/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: '[name].wasm',
|
||||
},
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
watch: false,
|
||||
@@ -227,15 +235,16 @@ const config = {
|
||||
new webpack.DefinePlugin({
|
||||
VERSION: JSON.stringify(VERSION),
|
||||
}),
|
||||
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser.js',
|
||||
process: 'process/browser',
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
fetch: 'cross-fetch',
|
||||
}),
|
||||
|
||||
new ProgressBarPlugin(),
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
113
yarn.lock
113
yarn.lock
@@ -3798,10 +3798,10 @@
|
||||
dset "^3.1.2"
|
||||
tslib "^2.4.1"
|
||||
|
||||
"@segment/analytics-next@1.53.0":
|
||||
version "1.53.0"
|
||||
resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.53.0.tgz#05dce27ce20a9abb3f9ea2d6e6d67667c82f2a9d"
|
||||
integrity sha512-7bzUjxGuccQpDx7CrteAx4yM2eDosjheQLySLBi6cz25EwPbBPb2X+ZzBUZIfLA0V0Co0zWaE6xXzVw8X5zSpQ==
|
||||
"@segment/analytics-next@1.53.1":
|
||||
version "1.53.1"
|
||||
resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.53.1.tgz#0ffe13b5af72e6a235dcc489a8bccf687e4d8da4"
|
||||
integrity sha512-Tf5suyN00Qq2irdUP6q2KJje1reFTN5vCrh4mi/E/gTma2by9j5jYYsbHavMaOGM51aYVbWkf6a3HOyTqWOm1A==
|
||||
dependencies:
|
||||
"@lukeed/uuid" "^2.0.0"
|
||||
"@segment/analytics-core" "1.3.0"
|
||||
@@ -3864,26 +3864,26 @@
|
||||
"@sentry/utils" "7.53.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry-internal/tracing@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.57.0.tgz#cb761931b635f8f24c84be0eecfacb8516b20551"
|
||||
integrity sha512-tpViyDd8AhQGYYhI94xi2aaDopXOPfL2Apwrtb3qirWkomIQ2K86W1mPmkce+B0cFOnW2Dxv/ZTFKz6ghjK75A==
|
||||
"@sentry-internal/tracing@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.59.2.tgz#91c44a37151bb81fac0500b4e031f53583dff772"
|
||||
integrity sha512-02gteChV/lMobWU06VlITq+myEWk0MzhnDCm8n/DMigB47I9HkWZFAJ+CYG6Ns0rTL+3+/c2V0bPyQkZwIC+Sg==
|
||||
dependencies:
|
||||
"@sentry/core" "7.57.0"
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/utils" "7.57.0"
|
||||
"@sentry/core" "7.59.2"
|
||||
"@sentry/types" "7.59.2"
|
||||
"@sentry/utils" "7.59.2"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/browser@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.57.0.tgz#6e724c9eac680dba99ced0fdf81be8d1e3b3bceb"
|
||||
integrity sha512-E0HaYYlaqHFiIRZXxcvOO8Odvlt+TR1vFFXzqUWXPOvDRxURglTOCQ3EN/u6bxtAGJ6y/Zc2obgihTtypuel/w==
|
||||
"@sentry/browser@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.59.2.tgz#77f6c7af33448ec375d684442ef51f21f5c26ace"
|
||||
integrity sha512-N1JiBs1VRR5DV0209TZgaMwRGiTYN1C34sFzIW7nuC82X4gHy3tuJjZPlMDTtgFrALBMJ24yQ7D4HJjXrS2+Dw==
|
||||
dependencies:
|
||||
"@sentry-internal/tracing" "7.57.0"
|
||||
"@sentry/core" "7.57.0"
|
||||
"@sentry/replay" "7.57.0"
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/utils" "7.57.0"
|
||||
"@sentry-internal/tracing" "7.59.2"
|
||||
"@sentry/core" "7.59.2"
|
||||
"@sentry/replay" "7.59.2"
|
||||
"@sentry/types" "7.59.2"
|
||||
"@sentry/utils" "7.59.2"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/bundler-plugin-core@2.4.0":
|
||||
@@ -3920,13 +3920,13 @@
|
||||
"@sentry/utils" "7.53.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/core@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.57.0.tgz#65093d739c04f320a54395a21be955fcbe326acb"
|
||||
integrity sha512-l014NudPH0vQlzybtXajPxYFfs9w762NoarjObC3gu76D1jzBBFzhdRelkGpDbSLNTIsKhEDDRpgAjBWJ9icfw==
|
||||
"@sentry/core@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.59.2.tgz#5c69cc7b8321f47c3da353f51084e2f4c4170e7d"
|
||||
integrity sha512-GRhoPw6b6GkvOsa060aREc9yyHjgAKITgITNbzUmn0GqIeWD5SMoCBAcENRHVgUnpQWOpnkEF1/sqxvwx+rf6Q==
|
||||
dependencies:
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/utils" "7.57.0"
|
||||
"@sentry/types" "7.59.2"
|
||||
"@sentry/utils" "7.59.2"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/node@7.53.1":
|
||||
@@ -3943,42 +3943,42 @@
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/react@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.57.0.tgz#cf91f0115bcd2a8306d6c8a39d8e8b53d4b21814"
|
||||
integrity sha512-XGNTjIoCG3naSmCU8qObd+y+CqAB6NQkGWOp2yyBwp2inyKF2ehJvDh6bIQloBYq2TmOJDa4NfXdMrkilxaLFQ==
|
||||
"@sentry/react@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.59.2.tgz#d2ffe0048591253ec0809ab7d69c4fcc1cb103eb"
|
||||
integrity sha512-n7m1GbBfCjvc/tLx8+wRNr/ol7VtGNsqAZ++mPoeMyT8WWkj17rDyTgShB1PQoevAAoZ74XJUEVg3MliOdEIrA==
|
||||
dependencies:
|
||||
"@sentry/browser" "7.57.0"
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/utils" "7.57.0"
|
||||
"@sentry/browser" "7.59.2"
|
||||
"@sentry/types" "7.59.2"
|
||||
"@sentry/utils" "7.59.2"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/replay@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.57.0.tgz#c8f7eae7b7edc9d32c3d2955b337f3b3c76dff39"
|
||||
integrity sha512-pN4ryNS3J5EYbkXvR+O/+hseAJha7XDl8mPFtK0OGTHG10JzCi4tQJazblHQdpb5QBaMMPCeZ+isyfoQLDNXnw==
|
||||
"@sentry/replay@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.59.2.tgz#4fc9c455e0e3b933075afe1729d8ba42953c62d4"
|
||||
integrity sha512-Ma72ofTdtSinBY5GH0zM7k8o/WsdaVdPP/1iyFbcWQDt8dnrcsJVUKK0t9+8gijpiSMUKE+vjFjQNL9/PGYekw==
|
||||
dependencies:
|
||||
"@sentry/core" "7.57.0"
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/utils" "7.57.0"
|
||||
"@sentry/core" "7.59.2"
|
||||
"@sentry/types" "7.59.2"
|
||||
"@sentry/utils" "7.59.2"
|
||||
|
||||
"@sentry/tracing@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.57.0.tgz#8bd07317db7b88ec5ecae48ad680e269af71b54d"
|
||||
integrity sha512-D8eKJMYN529mDP9lsOLyhe0Rf9Qiexo7Ul4+MQwDlwRr9c9tc0AdGwFlnKGvCMDh7ucITzvZkMZDHBapU3WHNQ==
|
||||
"@sentry/tracing@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.59.2.tgz#65b82fde3a336376a80c423354ae207743a3b150"
|
||||
integrity sha512-8HXnlmiES+sMPwq1h4HsVO1QI58WTtXM9z6taPRX/olNkhmN3Pk2o4PQeItLRzYs84K/X4GPk/aofeqMbBX8+g==
|
||||
dependencies:
|
||||
"@sentry-internal/tracing" "7.57.0"
|
||||
"@sentry-internal/tracing" "7.59.2"
|
||||
|
||||
"@sentry/types@7.53.1":
|
||||
version "7.53.1"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.53.1.tgz#3eefbad851f2d0deff67285d7e976d23d7d06a41"
|
||||
integrity sha512-/ijchRIu+jz3+j/zY+7KRPfLSCY14fTx5xujjbOdmEKjmIHQmwPBdszcQm40uwofrR8taV4hbt5MFN+WnjCkCw==
|
||||
|
||||
"@sentry/types@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.57.0.tgz#4fdb80cbd49ba034dd8d9be0c0005a016d5db3ce"
|
||||
integrity sha512-D7ifoUfxuVCUyktIr5Gc+jXUbtcUMmfHdTtTbf1XCZHua5mJceK9wtl3YCg3eq/HK2Ppd52BKnTzEcS5ZKQM+w==
|
||||
"@sentry/types@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.59.2.tgz#25b2ae0b2dc6733ca48621cf9167c9a7965852ae"
|
||||
integrity sha512-rylG7UQ0cC/xbV6trSuaAE/bsruSZy92jxQ1/KSOYKwBBvRFPXJBuiBtA81b8eYa4THZ+mE/ol2qOTJYuuV4Ug==
|
||||
|
||||
"@sentry/utils@7.53.1":
|
||||
version "7.53.1"
|
||||
@@ -3988,12 +3988,12 @@
|
||||
"@sentry/types" "7.53.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/utils@7.57.0":
|
||||
version "7.57.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.57.0.tgz#8253c6fcf35138b4c424234b8da1596e11b98ad8"
|
||||
integrity sha512-YXrkMCiNklqkXctn4mKYkrzNCf/dfVcRUQrkXjeBC+PHXbcpPyaJgInNvztR7Skl8lE3JPGPN4v5XhLxK1bUUg==
|
||||
"@sentry/utils@7.59.2":
|
||||
version "7.59.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.59.2.tgz#c4c76b1ea2a7f9363d1203dedfeafec2387bf634"
|
||||
integrity sha512-uxC8xH9wsB/tZUnjmaQ1uGtsumFOc19KWfedVHXzcNwqdt5uS3EB4+D1d8WwiJyLy2nm61DdmTC9SiB4HS+OSw==
|
||||
dependencies:
|
||||
"@sentry/types" "7.57.0"
|
||||
"@sentry/types" "7.59.2"
|
||||
tslib "^2.4.1 || ^1.9.3"
|
||||
|
||||
"@sentry/webpack-plugin@2.4.0":
|
||||
@@ -9720,6 +9720,11 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dlc-wasm-wallet@0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/dlc-wasm-wallet/-/dlc-wasm-wallet-0.4.3.tgz#1070e3a79edb2330ef912a0e182741b37ce49615"
|
||||
integrity sha512-y8eZu1thQKYgU0EFYTqY0YswYJDenqR4tjMaBymp6vh//YTtsXSyK1LLRSz3oSL60Y1ZwBD9JBWDm1JnQigxlg==
|
||||
|
||||
dlv@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
|
||||
|
||||
Reference in New Issue
Block a user