feat: bip322, closes #3386

This commit is contained in:
kyranjamie
2023-03-23 14:10:51 +01:00
committed by kyranjamie
parent bc7b42ff53
commit ad8ed1b207
58 changed files with 4139 additions and 2765 deletions

View File

@@ -49,7 +49,7 @@ module.exports = {
],
'@typescript-eslint/no-floating-promises': ['warn'],
'@typescript-eslint/no-unnecessary-type-assertion': ['warn'],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unsafe-assignment': [0],
'@typescript-eslint/no-unsafe-return': [0],
'@typescript-eslint/no-unsafe-call': [0],

View File

@@ -124,6 +124,7 @@
]
},
"dependencies": {
"@bitcoinerlab/secp256k1": "1.0.2",
"@coinbase/cbpay-js": "1.0.2",
"@emotion/core": "11.0.0",
"@emotion/css": "11.10.5",
@@ -148,11 +149,11 @@
"@stacks/connect": "7.2.1",
"@stacks/connect-ui": "6.0.0",
"@stacks/encryption": "6.1.1",
"@stacks/network": "6.1.1",
"@stacks/network": "6.5.2",
"@stacks/profile": "6.1.1",
"@stacks/rpc-client": "1.0.3",
"@stacks/storage": "6.1.1",
"@stacks/transactions": "6.1.1",
"@stacks/transactions": "6.5.2",
"@stacks/ui": "7.10.0",
"@stacks/ui-core": "7.3.0",
"@stacks/ui-theme": "7.5.0",
@@ -183,6 +184,7 @@
"dompurify": "3.0.1",
"downshift": "6.1.7",
"ecdsa-sig-formatter": "1.0.11",
"ecpair": "2.1.0",
"formik": "2.2.9",
"jotai": "1.13.1",
"jotai-redux": "0.1.0",
@@ -212,6 +214,7 @@
"use-events": "1.4.2",
"use-latest": "1.2.1",
"valid-url": "1.0.9",
"varuint-bitcoin": "1.1.2",
"webextension-polyfill": "0.10.0",
"yup": "0.32.11",
"zxcvbn": "4.4.2"
@@ -270,8 +273,8 @@
"audit-ci": "6.6.1",
"babel-loader": "9.1.2",
"base64-loader": "1.0.0",
"bip32": "3.1.0",
"bip39": "3.0.4",
"bip32": "4.0.0",
"bip39": "3.1.0",
"blns": "2.0.4",
"browserslist": "4.21.5",
"chrome-webstore-upload-cli": "2.1.0",
@@ -307,7 +310,6 @@
"speed-measure-webpack-plugin": "1.5.0",
"stream-browserify": "3.0.0",
"svg-url-loader": "8.0.0",
"tiny-secp256k1": "2.2.1",
"ts-jest": "29.0.5",
"ts-node": "10.9.1",
"ts-unused-exports": "7.0.3",

View File

@@ -8,7 +8,7 @@ import { Money } from '@shared/models/money.model';
import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-address.query';
import { useIsStampedTx } from '@app/query/bitcoin/stamps/use-is-stamped-tx';
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import {
useCurrentAccountNativeSegwitSigner,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
@@ -27,7 +27,7 @@ export function useGenerateSignedBitcoinTx() {
const createSigner = useCurrentAccountNativeSegwitSigner();
const isStamped = useIsStampedTx();
const networkMode = useBitcoinLibNetworkConfig();
const networkMode = useBitcoinScureLibNetworkConfig();
return useCallback(
(values: GenerateBitcoinTxValues, feeRate: number) => {

View File

@@ -1,16 +1,16 @@
import { Box } from '@stacks/ui';
import { Box, BoxProps } from '@stacks/ui';
import { openInNewTab } from '@app/common/utils/open-in-new-tab';
import { Link } from '@app/components/link';
import { Caption } from '@app/components/typography';
interface DisclaimerProps {
interface DisclaimerProps extends BoxProps {
disclaimerText: string;
learnMoreUrl?: string;
}
export function DisclaimerLayout({ disclaimerText, learnMoreUrl }: DisclaimerProps) {
export function Disclaimer({ disclaimerText, learnMoreUrl, ...props }: DisclaimerProps) {
return (
<Box>
<Box lineHeight="1.4" {...props}>
<Caption>
{disclaimerText}
{learnMoreUrl ? (

View File

@@ -5,15 +5,15 @@ import { whenStacksChainId } from '@app/common/utils';
import { SpaceBetween } from '@app/components/layout/space-between';
import { Caption } from '@app/components/typography';
interface NetworkRowProps {
interface NoFeesWarningRowProps {
chainId: ChainID;
}
export function NetworkRow({ chainId }: NetworkRowProps) {
export function NoFeesWarningRow({ chainId }: NoFeesWarningRowProps) {
return (
<Box spacing="base">
<SpaceBetween position="relative">
<Box alignItems="center">
<Caption>No fees will be incurred</Caption>
<Caption>No fees are incurred</Caption>
</Box>
<Caption>
<span>

View File

@@ -23,7 +23,8 @@ interface BalancesListProps extends StackProps {
address: string;
}
export function BalancesList({ address, ...props }: BalancesListProps) {
const { data: stxUnachoredAssetBalance } = useStacksUnanchoredCryptoCurrencyAssetBalance(address);
const { data: stxUnanchoredAssetBalance } =
useStacksUnanchoredCryptoCurrencyAssetBalance(address);
const stacksFtAssetBalances = useStacksFungibleTokenAssetBalancesAnchoredWithMetadata(address);
const isBitcoinEnabled = useConfigBitcoinEnabled();
const { stxEffectiveBalance, stxEffectiveUsdBalance } = useStxBalance();
@@ -31,7 +32,7 @@ export function BalancesList({ address, ...props }: BalancesListProps) {
const { whenWallet } = useWalletType();
// Better handle loading state
if (!stxUnachoredAssetBalance) return <LoadingSpinner />;
if (!stxEffectiveBalance || !stxUnanchoredAssetBalance) return <LoadingSpinner />;
return (
<Stack
@@ -51,6 +52,7 @@ export function BalancesList({ address, ...props }: BalancesListProps) {
<CryptoCurrencyAssetItem
assetBalance={stxEffectiveBalance}
usdBalance={stxEffectiveUsdBalance}
assetSubBalance={stxUnanchoredAssetBalance}
address={address}
icon={<StxAvatar {...props} />}
/>

View File

@@ -8,7 +8,7 @@ import get from 'lodash.get';
import { finalizeMessageSignature } from '@shared/actions/finalize-message-signature';
import { logger } from '@shared/logger';
import { SignedMessage, whenSignedMessageOfType } from '@shared/signature/signature-types';
import { UnsignedMessage, whenSignableMessageOfType } from '@shared/signature/signature-types';
import { useScrollLock } from '@app/common/hooks/use-scroll-lock';
import { delay } from '@app/common/utils';
@@ -33,7 +33,7 @@ import { useSignedMessageType } from './use-message-type';
interface LedgerSignMsgData {
account: StacksAccount;
unsignedMessage: SignedMessage;
unsignedMessage: UnsignedMessage;
}
interface LedgerSignMsgDataProps {
children({ account, unsignedMessage }: LedgerSignMsgData): JSX.Element;
@@ -89,7 +89,7 @@ function LedgerSignMsg({ account, unsignedMessage }: LedgerSignMsgProps) {
await delay(1000);
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });
const resp = await whenSignedMessageOfType(unsignedMessage)({
const resp = await whenSignableMessageOfType(unsignedMessage)({
async utf8(msg) {
return signLedgerUtf8Message(stacksApp)(msg, account.index);
},

View File

@@ -1,12 +1,12 @@
import { createContext } from 'react';
import { SignedMessage } from '@shared/signature/signature-types';
import { UnsignedMessage } from '@shared/signature/signature-types';
import { noop } from '@shared/utils';
import { BaseLedgerOperationContext } from '../../ledger-utils';
export interface LedgerMessageSigningContext extends BaseLedgerOperationContext {
message: SignedMessage | undefined;
message: UnsignedMessage | undefined;
signMessage(): Promise<void> | void;
}

View File

@@ -8,7 +8,7 @@ import {
encodeStructuredData,
} from '@stacks/transactions';
import { SignedMessageStructured } from '@shared/signature/signature-types';
import { UnsignedMessageStructured } from '@shared/signature/signature-types';
import { whenStacksChainId } from '@app/common/utils';
@@ -31,6 +31,6 @@ export function chainIdToDisplay(chainIdCv: ClarityValue): string {
export function deriveStructuredMessageHash({
domain,
message,
}: Omit<SignedMessageStructured, 'messageType'>) {
}: Omit<UnsignedMessageStructured, 'messageType'>) {
return bytesToHex(sha256(encodeStructuredData({ message, domain })));
}

View File

@@ -1,7 +1,7 @@
import { useContext } from 'react';
import { logger } from '@shared/logger';
import { whenSignedMessageOfType } from '@shared/signature/signature-types';
import { whenSignableMessageOfType } from '@shared/signature/signature-types';
import { ApproveLedgerOperationLayout } from '../../../generic-steps';
import { useHasApprovedOperation } from '../../../hooks/use-has-approved-transaction';
@@ -17,7 +17,7 @@ export function SignLedgerMessage() {
return null;
}
return whenSignedMessageOfType(message)({
return whenSignableMessageOfType(message)({
utf8: msg => (
<ApproveLedgerOperationLayout
description="Sign message on your Ledger"

View File

@@ -1,11 +1,11 @@
import { ClarityValue } from '@stacks/transactions';
import { SignedMessage, StructuredMessageDataDomain } from '@shared/signature/signature-types';
import { StructuredMessageDataDomain, UnsignedMessage } from '@shared/signature/signature-types';
import { isString } from '@shared/utils';
import { useLocationStateWithCache } from '@app/common/hooks/use-location-state';
export function useSignedMessageType(): SignedMessage | null {
export function useSignedMessageType(): UnsignedMessage | null {
const message = useLocationStateWithCache<string | ClarityValue>('message');
const domain = useLocationStateWithCache<StructuredMessageDataDomain>('domain');

View File

@@ -1,32 +1,14 @@
import { useEffect, useState } from 'react';
import { bytesToHex } from '@stacks/common';
import { hashMessage } from '@stacks/encryption';
import { Box, Stack, Text, color } from '@stacks/ui';
import { HashDrawer } from './hash-drawer';
interface MessageBoxProps {
message: string;
hash?: string;
}
export function MessageBox({ message }: MessageBoxProps) {
const [hash, setHash] = useState<string | undefined>();
const [displayMessage, setDisplayMessage] = useState<string[] | undefined>();
useEffect(() => {
setDisplayMessage(message.split(/\r?\n/));
}, [message]);
useEffect(() => {
if (!message) return;
const messageHash = bytesToHex(hashMessage(message));
setHash(messageHash);
}, [message]);
if (!message) return null;
export function MessagePreviewBox({ message, hash }: MessageBoxProps) {
return (
<Box minHeight="260px">
<Box minHeight="190px">
<Stack
border="4px solid"
paddingBottom="8px"
@@ -42,9 +24,9 @@ export function MessageBox({ message }: MessageBoxProps) {
px="loose"
py="loose"
spacing="tight"
overflowX="scroll"
overflowX="auto"
>
{displayMessage?.map(line => (
{message.split(/\r?\n/).map(line => (
<Text key={line}>{line}</Text>
))}
</Stack>

View File

@@ -0,0 +1,33 @@
import { Stack } from '@stacks/ui';
import { addPortSuffix, getUrlHostname } from '@app/common/utils';
import { Caption, Title } from '@app/components/typography';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
interface MessageSigningHeaderProps {
name?: string;
origin: string | null;
}
export function MessageSigningHeader({ name, origin }: MessageSigningHeaderProps) {
const { chain, isTestnet } = useCurrentNetworkState();
const originAddition = origin ? ` (${getUrlHostname(origin)})` : '';
const testnetAddition = isTestnet
? ` using ${getUrlHostname(chain.stacks.url)}${addPortSuffix(chain.stacks.url)}`
: '';
const displayName = name ?? origin;
const caption = displayName
? `Requested by ${displayName}${originAddition}${testnetAddition}`
: null;
return (
<Stack pt="extra-loose" spacing="base">
<Title fontWeight="bold" as="h1">
Sign Message
</Title>
{caption && <Caption wordBreak="break-word">{caption}</Caption>}
</Stack>
);
}

View File

@@ -1,14 +1,11 @@
import { Stack } from '@stacks/ui';
import { PageTop } from './page-top';
interface MessageSigningRequestLayoutProps {
children: React.ReactNode;
}
export function MessageSigningRequestLayout({ children }: MessageSigningRequestLayoutProps) {
return (
<Stack px={['loose', 'unset']} spacing="loose" width="100%">
<PageTop />
{children}
</Stack>
);

View File

@@ -0,0 +1,30 @@
import { Button, Stack } from '@stacks/ui';
import { useWalletType } from '@app/common/use-wallet-type';
interface StacksSignMessageActionsProps {
onSignMessage(): void;
onSignMessageCancel(): void;
isLoading: boolean;
}
export function SignMessageActions(props: StacksSignMessageActionsProps) {
const { onSignMessage, onSignMessageCancel, isLoading } = props;
const { whenWallet } = useWalletType();
return (
<Stack isInline>
<Button onClick={onSignMessageCancel} flexGrow={1} borderRadius="10px" mode="tertiary">
Cancel
</Button>
<Button
type="button"
flexGrow={1}
borderRadius="10px"
onClick={onSignMessage}
isLoading={isLoading}
>
{whenWallet({ software: 'Sign', ledger: 'Sign on Ledger' })}
</Button>
</Stack>
);
}

View File

@@ -9,11 +9,11 @@ import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-s
import { useCurrentTaprootAccountUninscribedUtxos } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { getNumberOfInscriptionOnUtxo } from '@app/query/bitcoin/ordinals/ordinals-aware-utxo.query';
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
export function useGenerateRetrieveTaprootFundsTx() {
const networkMode = useBitcoinLibNetworkConfig();
const networkMode = useBitcoinScureLibNetworkConfig();
const uninscribedUtxos = useCurrentTaprootAccountUninscribedUtxos();
const createSigner = useCurrentAccountTaprootSigner();
const { data: feeRate } = useBitcoinFeeRate();

View File

@@ -1,28 +0,0 @@
import { ChainID } from '@stacks/common';
import { getSignaturePayloadFromToken } from '@app/common/signature/requests';
import { NetworkRow } from '@app/components/network-row';
import { MessageBox } from './message-box';
import { Disclaimer } from './message-signing-disclaimer';
import { SignAction } from './sign-action';
interface SignatureRequestMessageContentProps {
requestToken: string;
}
export function SignatureRequestMessageContent(props: SignatureRequestMessageContentProps) {
const { requestToken } = props;
const signatureRequest = getSignaturePayloadFromToken(requestToken);
const { message, network } = signatureRequest;
const appName = signatureRequest.appDetails?.name;
return (
<>
<MessageBox message={message} />
<NetworkRow chainId={network?.chainId ?? ChainID.Testnet} />
<SignAction message={message} messageType="utf8" />
<hr />
<Disclaimer appName={appName} />
</>
);
}

View File

@@ -1,38 +0,0 @@
import { memo } from 'react';
import { Stack } from '@stacks/ui';
import { isSignedMessageType } from '@shared/signature/signature-types';
import { getSignaturePayloadFromToken } from '@app/common/signature/requests';
import { addPortSuffix, getUrlHostname } from '@app/common/utils';
import { Caption, Title } from '@app/components/typography';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { useSignatureRequestSearchParams } from '@app/store/signatures/requests.hooks';
function PageTopBase() {
const { chain, isTestnet } = useCurrentNetworkState();
const { origin, requestToken, messageType } = useSignatureRequestSearchParams();
if (!requestToken) return null;
if (!isSignedMessageType(messageType)) return null;
const signatureRequest = getSignaturePayloadFromToken(requestToken);
if (!signatureRequest) return null;
const appName = signatureRequest?.appDetails?.name;
const originAddition = origin ? ` (${getUrlHostname(origin)})` : '';
const testnetAddition = isTestnet
? ` using ${getUrlHostname(chain.stacks.url)}${addPortSuffix(chain.stacks.url)}`
: '';
const caption = appName ? `Requested by "${appName}"${originAddition}${testnetAddition}` : null;
return (
<Stack pt="extra-loose" spacing="base">
<Title fontWeight="bold" as="h1">
Sign Message
</Title>
{caption && <Caption wordBreak="break-word">{caption}</Caption>}
</Stack>
);
}
export const PageTop = memo(PageTopBase);

View File

@@ -1,76 +0,0 @@
import { useState } from 'react';
import { Button, Stack } from '@stacks/ui';
import { finalizeMessageSignature } from '@shared/actions/finalize-message-signature';
import { logger } from '@shared/logger';
import { SignedMessage, whenSignedMessageOfType } from '@shared/signature/signature-types';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useWalletType } from '@app/common/use-wallet-type';
import { createDelay } from '@app/common/utils';
import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate';
import { useSignatureRequestSearchParams } from '@app/store/signatures/requests.hooks';
import { useMessageSignerSoftwareWallet } from '../message-signing.utils';
const improveUxWithShortDelayAsSigningIsSoFast = createDelay(1000);
export function SignAction(message: SignedMessage) {
const analytics = useAnalytics();
const ledgerNavigate = useLedgerNavigate();
const [isLoading, setIsLoading] = useState(false);
const signSoftwareWalletMessage = useMessageSignerSoftwareWallet();
const { tabId, requestToken } = useSignatureRequestSearchParams();
const { whenWallet } = useWalletType();
if (!requestToken || !tabId) return null;
const sign = whenWallet({
async software() {
setIsLoading(true);
void analytics.track('request_signature_sign', { type: 'software' });
const messageSignature = signSoftwareWalletMessage({ ...message });
if (!messageSignature) {
logger.error('Cannot sign message, no account in state');
void analytics.track('request_signature_cannot_sign_message_no_account');
return;
}
await improveUxWithShortDelayAsSigningIsSoFast();
setIsLoading(false);
finalizeMessageSignature({ requestPayload: requestToken, tabId, data: messageSignature });
},
async ledger() {
void analytics.track('request_signature_sign', { type: 'ledger' });
whenSignedMessageOfType(message)({
utf8(msg) {
ledgerNavigate.toConnectAndSignUtf8MessageStep(msg);
},
structured(domain, msg) {
ledgerNavigate.toConnectAndSignStructuredMessageStep(domain, msg);
},
});
},
});
const cancel = () => {
void analytics.track('request_signature_cancel');
finalizeMessageSignature({ requestPayload: requestToken, tabId, data: 'cancel' });
};
return (
<Stack isInline>
<Button onClick={cancel} flexGrow={1} borderRadius="10px" mode="tertiary">
Cancel
</Button>
<Button type="button" flexGrow={1} borderRadius="10px" onClick={sign} isLoading={isLoading}>
{whenWallet({ software: 'Sign', ledger: 'Sign on Ledger' })}
</Button>
</Stack>
);
}

View File

@@ -1,28 +0,0 @@
import { ChainID } from '@stacks/common';
import { getStructuredDataPayloadFromToken } from '@app/common/signature/requests';
import { NetworkRow } from '@app/components/network-row';
import { Disclaimer } from './message-signing-disclaimer';
import { SignAction } from './sign-action';
import { StructuredDataBox } from './structured-data-box';
interface SignatureRequestStructuredDataContentProps {
requestToken: string;
}
export function SignatureRequestStructuredDataContent({
requestToken,
}: SignatureRequestStructuredDataContentProps) {
const signatureRequest = getStructuredDataPayloadFromToken(requestToken);
const { domain, message, network } = signatureRequest;
const appName = signatureRequest.appDetails?.name;
return (
<>
<StructuredDataBox message={message} domain={domain} />
<NetworkRow chainId={network?.chainId ?? ChainID.Testnet} />
<SignAction message={message} messageType="structured" domain={domain} />
<hr />
<Disclaimer appName={appName} />
</>
);
}

View File

@@ -1,26 +0,0 @@
import { useCallback } from 'react';
import { ClarityValue, TupleCV, createStacksPrivateKey } from '@stacks/transactions';
import { signMessage, signStructuredDataMessage } from '@shared/crypto/sign-message';
import { isString } from '@shared/utils';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
export function useMessageSignerSoftwareWallet() {
const account = useCurrentStacksAccount();
return useCallback(
({ message, domain }: { message: string | ClarityValue; domain?: TupleCV }) => {
if (!account || account.type === 'ledger') return null;
const privateKey = createStacksPrivateKey(account.stxPrivateKey);
if (isString(message)) {
return signMessage(message, privateKey);
} else {
if (!domain) throw new Error('Domain is required for structured messages');
// returns signature in RSV format
return signStructuredDataMessage(message, domain, privateKey);
}
},
[account]
);
}

View File

@@ -0,0 +1,48 @@
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { Disclaimer } from '@app/components/disclaimer';
import { NoFeesWarningRow } from '@app/components/no-fees-warning-row';
import { MessagePreviewBox } from '@app/features/message-signer/message-preview-box';
import { MessageSigningRequestLayout } from '@app/features/message-signer/message-signing-request.layout';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { MessageSigningHeader } from '../../features/message-signer/message-signing-header';
import { SignMessageActions } from '../../features/message-signer/stacks-sign-message-action';
import { useSignBip322Message } from './use-sign-bip322-message';
export function RpcSignBip322Message() {
useRouteHeader(<></>);
const {
origin,
message,
isLoading,
onUserApproveBip322MessageSigningRequest,
onUserRejectBip322MessageSigningRequest,
} = useSignBip322Message();
const { chain } = useCurrentNetwork();
if (origin === null) {
window.close();
throw new Error('Origin is null');
}
return (
<MessageSigningRequestLayout>
<MessageSigningHeader origin={origin} />
<MessagePreviewBox message={message} />
<NoFeesWarningRow chainId={chain.stacks.chainId} />
<SignMessageActions
isLoading={isLoading}
onSignMessage={() => onUserApproveBip322MessageSigningRequest()}
onSignMessageCancel={() => onUserRejectBip322MessageSigningRequest()}
/>
<hr />
<Disclaimer
disclaimerText="By signing this message, you prove that you own this address"
learnMoreUrl="https://docs.hiro.so/build-apps/message-signing"
mb="loose"
/>
</MessageSigningRequestLayout>
);
}

View File

@@ -0,0 +1,160 @@
import { useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import { RpcErrorCode } from '@btckit/types';
import * as bitcoin from 'bitcoinjs-lib';
import {
createNativeSegwitBitcoinJsSigner,
createTaprootBitcoinJsSigner,
signBip322MessageSimple,
} from '@shared/crypto/bitcoin/bip322/sign-message-bip322-bitcoinjs';
import { deriveAddressIndexZeroFromAccount } from '@shared/crypto/bitcoin/bitcoin.utils';
import { logger } from '@shared/logger';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import { createDelay } from '@app/common/utils';
import {
useCurrentAccountNativeSegwitSigner,
useNativeSegwitCurrentAccountPrivateKeychain,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import {
useCurrentAccountTaprootSigner,
useCurrentTaprootAccountKeychain,
} from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
function useRpcSignBitcoinMessage() {
const defaultParams = useDefaultRequestParams();
return useMemo(
() => ({
...defaultParams,
requestId: initialSearchParams.get('requestId') ?? '',
message: initialSearchParams.get('message') ?? '',
paymentType: initialSearchParams.get('paymentType') ?? 'p2wpkh',
}),
[defaultParams]
);
}
const shortPauseBeforeToast = createDelay(250);
const allowTimeForUserToReadToast = createDelay(1200);
export function useSignBip322Message() {
const network = useCurrentNetwork();
const analytics = useAnalytics();
const [isLoading, setIsLoading] = useState(false);
const { tabId, origin, requestId, message, paymentType } = useRpcSignBitcoinMessage();
const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const createTaprootSigner = useCurrentAccountTaprootSigner();
const currentAccountNativeSegwitKeychain = useNativeSegwitCurrentAccountPrivateKeychain();
if (!currentAccountNativeSegwitKeychain) throw new Error('No keychain for current account');
const nativeSegwitIndexZeroKeychain = deriveAddressIndexZeroFromAccount(
currentAccountNativeSegwitKeychain
);
const currentAccountTaprootKeychain = useCurrentTaprootAccountKeychain();
const taprootIndexZeroKeychain = deriveAddressIndexZeroFromAccount(
currentAccountTaprootKeychain!
);
return {
origin,
message,
isLoading,
formattedOrigin: new URL(origin ?? '').host,
onUserRejectBip322MessageSigningRequest() {
if (!tabId) return;
chrome.tabs.sendMessage(
tabId,
makeRpcErrorResponse('signMessage', {
id: requestId,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected message signing request',
},
})
);
window.close();
},
async onUserApproveBip322MessageSigningRequest() {
setIsLoading(true);
const nativeSegwitSigner = createNativeSegwitSigner?.(0);
const taprootSigner = createTaprootSigner?.(0);
if (!tabId || !origin) {
logger.error('Cannot give app accounts: missing tabId, origin');
return;
}
void analytics.track('user_approved_message_signing', { origin });
switch (paymentType) {
case 'p2wpkh': {
const nativeSegwitAddress = nativeSegwitSigner?.payment.address;
if (!nativeSegwitAddress) throw new Error('Cannot sign message: no address');
function signPsbt(psbt: bitcoin.Psbt) {
psbt.signAllInputs(
createNativeSegwitBitcoinJsSigner(
Buffer.from(nativeSegwitIndexZeroKeychain?.privateKey!)
)
);
}
const { signature } = signBip322MessageSimple({
message,
address: nativeSegwitAddress,
signPsbt,
network: network.chain.bitcoin.network,
});
chrome.tabs.sendMessage(
tabId,
makeRpcSuccessResponse('signMessage', {
id: requestId,
result: { signature, address: nativeSegwitAddress } as any,
})
);
break;
}
case 'p2tr': {
function signPsbt(psbt: bitcoin.Psbt) {
psbt.data.inputs.forEach(
input => (input.tapInternalKey = Buffer.from(taprootSigner?.payment.tapInternalKey!))
);
psbt.signAllInputs(
createTaprootBitcoinJsSigner(Buffer.from(taprootIndexZeroKeychain?.privateKey!))
);
}
const { signature } = signBip322MessageSimple({
message,
address: taprootSigner?.payment.address!,
signPsbt,
network: network.chain.bitcoin.network,
});
chrome.tabs.sendMessage(
tabId,
makeRpcSuccessResponse('signMessage', {
id: requestId,
result: { signature, address: taprootSigner?.payment.address! } as any,
})
);
}
}
await shortPauseBeforeToast();
toast.success('Message signed successfully');
await allowTimeForUserToReadToast();
window.close();
},
};
}

View File

@@ -6,7 +6,7 @@ import { OrdinalSendFormValues } from '@shared/models/form.model';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/address.hooks';
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
@@ -15,7 +15,7 @@ import { selectInscriptionTransferCoins } from './select-inscription-coins';
export function useGenerateSignedOrdinalTx(trInput: TaprootUtxo) {
const createTapRootSigner = useCurrentAccountTaprootSigner();
const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const networkMode = useBitcoinLibNetworkConfig();
const networkMode = useBitcoinScureLibNetworkConfig();
const { data: feeRate } = useBitcoinFeeRate();
const { data: nativeSegwitUtxos } = useCurrentNativeSegwitUtxos();

View File

@@ -1,17 +1,18 @@
import { DisclaimerLayout } from '@app/components/disclaimer';
import { Disclaimer } from '@app/components/disclaimer';
interface DisclaimerProps {
appName?: string;
}
export function Disclaimer({ appName }: DisclaimerProps) {
export function StacksMessageSigningDisclaimer({ appName }: DisclaimerProps) {
return (
<DisclaimerLayout
<Disclaimer
disclaimerText={`By signing this message, you are authorizing ${
appName ?? 'the app'
} to do something specific
on your behalf. Only sign messages that you understand from apps that you trust.
`}
learnMoreUrl="https://docs.hiro.so/build-apps/message-signing"
mb="loose"
/>
);
}

View File

@@ -0,0 +1,34 @@
import { ChainID, bytesToHex } from '@stacks/common';
import { hashMessage } from '@stacks/encryption';
import { getSignaturePayloadFromToken } from '@app/common/signature/requests';
import { NoFeesWarningRow } from '@app/components/no-fees-warning-row';
import { MessagePreviewBox } from '../../../features/message-signer/message-preview-box';
import { SignMessageActions } from '../../../features/message-signer/stacks-sign-message-action';
import { useStacksMessageSigner } from '../stacks-message-signing.utils';
import { StacksMessageSigningDisclaimer } from './message-signing-disclaimer';
interface SignatureRequestMessageContentProps {
requestToken: string;
}
export function StacksSignatureRequestMessageContent(props: SignatureRequestMessageContentProps) {
const { requestToken } = props;
const { isLoading, cancelMessageSigning, signMessage } = useStacksMessageSigner();
const signatureRequest = getSignaturePayloadFromToken(requestToken);
const { message, network } = signatureRequest;
const appName = signatureRequest.appDetails?.name;
return (
<>
<MessagePreviewBox message={message} hash={bytesToHex(hashMessage(message))} />
<NoFeesWarningRow chainId={network?.chainId ?? ChainID.Testnet} />
<SignMessageActions
isLoading={isLoading}
onSignMessageCancel={cancelMessageSigning}
onSignMessage={() => signMessage({ messageType: 'utf8', message })}
/>
<hr />
<StacksMessageSigningDisclaimer appName={appName} />
</>
);
}

View File

@@ -10,9 +10,9 @@ import {
cvToDisplay,
deriveStructuredMessageHash,
} from '@app/features/ledger/flows/message-signing/message-signing.utils';
import { HashDrawer } from '@app/features/message-signer/hash-drawer';
import { ClarityValueListDisplayer } from './clarity-value-list';
import { HashDrawer } from './hash-drawer';
export function StructuredDataBox(props: {
message: ClarityValue;

View File

@@ -0,0 +1,34 @@
import { ChainID } from '@stacks/common';
import { getStructuredDataPayloadFromToken } from '@app/common/signature/requests';
import { NoFeesWarningRow } from '@app/components/no-fees-warning-row';
import { SignMessageActions } from '@app/features/message-signer/stacks-sign-message-action';
import { useStacksMessageSigner } from '../stacks-message-signing.utils';
import { StacksMessageSigningDisclaimer } from './message-signing-disclaimer';
import { StructuredDataBox } from './structured-data-box';
interface SignatureRequestStructuredDataContentProps {
requestToken: string;
}
export function SignatureRequestStructuredDataContent({
requestToken,
}: SignatureRequestStructuredDataContentProps) {
const { isLoading, signMessage, cancelMessageSigning } = useStacksMessageSigner();
const signatureRequest = getStructuredDataPayloadFromToken(requestToken);
const { domain, message, network } = signatureRequest;
const appName = signatureRequest.appDetails?.name;
return (
<>
<StructuredDataBox message={message} domain={domain} />
<NoFeesWarningRow chainId={network?.chainId ?? ChainID.Testnet} />
<SignMessageActions
isLoading={isLoading}
onSignMessageCancel={cancelMessageSigning}
onSignMessage={() => signMessage({ messageType: 'structured', message, domain })}
/>
<hr />
<StacksMessageSigningDisclaimer appName={appName} />
</>
);
}

View File

@@ -1,7 +1,7 @@
import { Outlet } from 'react-router-dom';
import {
isSignedMessageType,
isSignableMessageType,
isStructuredMessageType,
isUtf8MessageType,
} from '@shared/signature/signature-types';
@@ -9,30 +9,32 @@ import {
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { WarningLabel } from '@app/components/warning-label';
import { PopupHeader } from '@app/features/current-account/popup-header';
import { MessageSigningHeader } from '@app/features/message-signer/message-signing-header';
import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed';
import {
useIsSignatureRequestValid,
useSignatureRequestSearchParams,
} from '@app/store/signatures/requests.hooks';
import { SignatureRequestMessageContent } from './components/message-content';
import { MessageSigningRequestLayout } from './components/signature-request.layout';
import { MessageSigningRequestLayout } from '../../features/message-signer/message-signing-request.layout';
import { StacksSignatureRequestMessageContent } from './components/stacks-signature-message-content';
import { SignatureRequestStructuredDataContent } from './components/structured-data-content';
export function MessageSigningRequest() {
const validSignatureRequest = useIsSignatureRequestValid();
const { requestToken, messageType } = useSignatureRequestSearchParams();
export function StacksMessageSigningRequest() {
useRouteHeader(<PopupHeader />);
useOnOriginTabClose(() => window.close());
if (!isSignedMessageType(messageType)) return null;
const validSignatureRequest = useIsSignatureRequestValid();
const { requestToken, messageType, tabId, origin } = useSignatureRequestSearchParams();
if (!requestToken || !messageType) return null;
if (!requestToken || !tabId) return null;
if (!isSignableMessageType(messageType)) return null;
if (!origin) return null;
return (
<MessageSigningRequestLayout>
<MessageSigningHeader name={origin} origin={origin} />
{!validSignatureRequest && (
<WarningLabel>
Signing messages can have unintended consequences. Only sign messages from trusted
@@ -40,7 +42,7 @@ export function MessageSigningRequest() {
</WarningLabel>
)}
{isUtf8MessageType(messageType) && (
<SignatureRequestMessageContent requestToken={requestToken} />
<StacksSignatureRequestMessageContent requestToken={requestToken} />
)}
{isStructuredMessageType(messageType) && (
<SignatureRequestStructuredDataContent requestToken={requestToken} />

View File

@@ -0,0 +1,95 @@
import { useCallback, useState } from 'react';
import { ClarityValue, TupleCV, createStacksPrivateKey } from '@stacks/transactions';
import { finalizeMessageSignature } from '@shared/actions/finalize-message-signature';
import { signMessage, signStructuredDataMessage } from '@shared/crypto/sign-message';
import { logger } from '@shared/logger';
import { UnsignedMessage, whenSignableMessageOfType } from '@shared/signature/signature-types';
import { isDefined, isString } from '@shared/utils';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useWalletType } from '@app/common/use-wallet-type';
import { createDelay } from '@app/common/utils';
import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import {
useIsSignatureRequestValid,
useSignatureRequestSearchParams,
} from '@app/store/signatures/requests.hooks';
const improveUxWithShortDelayAsSigningIsSoFast = createDelay(1000);
function useMessageSignerStacksSoftwareWallet() {
const account = useCurrentStacksAccount();
return useCallback(
({ message, domain }: { message: string | ClarityValue; domain?: TupleCV }) => {
if (!account || account.type === 'ledger') return null;
const privateKey = createStacksPrivateKey(account.stxPrivateKey);
if (isString(message)) {
return signMessage(message, privateKey);
} else {
if (!domain) throw new Error('Domain is required for structured messages');
// returns signature in RSV format
return signStructuredDataMessage(message, domain, privateKey);
}
},
[account]
);
}
export function useStacksMessageSigner() {
const analytics = useAnalytics();
const signSoftwareWalletMessage = useMessageSignerStacksSoftwareWallet();
const validSignatureRequest = useIsSignatureRequestValid();
const { whenWallet } = useWalletType();
const ledgerNavigate = useLedgerNavigate();
const [isLoading, setIsLoading] = useState(false);
if (isDefined(validSignatureRequest) && !validSignatureRequest)
throw new Error('Invalid request');
const { requestToken, tabId } = useSignatureRequestSearchParams();
if (!tabId) throw new Error('Requests can only be made with corresponding tab');
if (!requestToken) throw new Error('Missing request token');
const signMessage = whenWallet({
async software(unsignedMessage: UnsignedMessage) {
setIsLoading(true);
void analytics.track('request_signature_sign', { type: 'software' });
const messageSignature = signSoftwareWalletMessage(unsignedMessage);
if (!messageSignature) {
logger.error('Cannot sign message, no account in state');
void analytics.track('request_signature_cannot_sign_message_no_account');
return;
}
await improveUxWithShortDelayAsSigningIsSoFast();
setIsLoading(false);
finalizeMessageSignature({ requestPayload: requestToken, tabId, data: messageSignature });
},
async ledger(unsignedMessage: UnsignedMessage) {
void analytics.track('request_signature_sign', { type: 'ledger' });
whenSignableMessageOfType(unsignedMessage)({
utf8(msg) {
ledgerNavigate.toConnectAndSignUtf8MessageStep(msg);
},
structured(domain, msg) {
ledgerNavigate.toConnectAndSignStructuredMessageStep(domain, msg);
},
});
},
});
function cancelMessageSigning() {
if (!requestToken || !tabId) return;
void analytics.track('request_signature_cancel');
finalizeMessageSignature({ requestPayload: requestToken, tabId, data: 'cancel' });
}
return { isLoading, signMessage, cancelMessageSigning };
}

View File

@@ -2,8 +2,8 @@ import { ChainID } from '@stacks/common';
import { Person } from '@stacks/profile';
import { getProfileDataContentFromToken } from '@app/common/profiles/requests';
import { DisclaimerLayout } from '@app/components/disclaimer';
import { NetworkRow } from '@app/components/network-row';
import { Disclaimer } from '@app/components/disclaimer';
import { NoFeesWarningRow } from '@app/components/no-fees-warning-row';
import { ProfileBox } from './profile-box';
import { UpdateAction } from './update-action';
@@ -20,10 +20,10 @@ export function ProfileDataContent(props: ProfileDataContentProps) {
return (
<>
<ProfileBox profile={person} />
<NetworkRow chainId={network?.chainId ?? ChainID.Testnet} />
<NoFeesWarningRow chainId={network?.chainId ?? ChainID.Testnet} />
<UpdateAction profileUpdaterPayload={profileUpdateRequest} />
<hr />
<DisclaimerLayout
<Disclaimer
disclaimerText={`This update will overwrite any values in your public profile with the included property names.`}
learnMoreUrl="https://docs.hiro.so/build-apps/transaction-signing#get-the-users-stacks-address"
/>

View File

@@ -13,14 +13,13 @@ import { ledgerJwtSigningRoutes } from '@app/features/ledger/flows/jwt-signing/l
import { ledgerMessageSigningRoutes } from '@app/features/ledger/flows/message-signing/ledger-sign-msg.routes';
import { ledgerRequestKeysRoutes } from '@app/features/ledger/flows/request-keys/ledger-request-keys.routes';
import { ledgerTxSigningRoutes } from '@app/features/ledger/flows/tx-signing/ledger-sign-tx.routes';
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 { AddNetwork } from '@app/pages/add-network/add-network';
import { AllowDiagnosticsPage } from '@app/pages/allow-diagnostics/allow-diagnostics';
import { ChooseAccount } from '@app/pages/choose-account/choose-account';
import { FundPage } from '@app/pages/fund/fund';
import { Home } from '@app/pages/home/home';
import { MessageSigningRequest } from '@app/pages/message-signing-request/message-signing-request';
import { BackUpSecretKeyPage } from '@app/pages/onboarding/back-up-secret-key/back-up-secret-key';
import { MagicRecoveryCode } from '@app/pages/onboarding/magic-recovery-code/magic-recovery-code';
import { SetPasswordPage } from '@app/pages/onboarding/set-password/set-password';
@@ -34,6 +33,7 @@ import { ReceiveCollectibleModal } from '@app/pages/receive/receive-collectible/
import { ReceiveCollectibleOrdinal } from '@app/pages/receive/receive-collectible/receive-collectible-oridinal';
import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses';
import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-transfer.routes';
import { RpcSignBip322Message } from '@app/pages/rpc-sign-bip322-message/rpc-sign-bip322-message';
import { SelectNetwork } from '@app/pages/select-network/select-network';
import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error';
import { SendInscriptionChooseFee } from '@app/pages/send/ordinal-inscription/send-inscription-choose-fee';
@@ -43,6 +43,7 @@ import { SendInscriptionReview } from '@app/pages/send/ordinal-inscription/send-
import { SendInscriptionSummary } from '@app/pages/send/ordinal-inscription/sent-inscription-summary';
import { sendCryptoAssetFormRoutes } from '@app/pages/send/send-crypto-asset-form/send-crypto-asset-form.routes';
import { SignOutConfirmDrawer } from '@app/pages/sign-out-confirm/sign-out-confirm';
import { StacksMessageSigningRequest } from '@app/pages/stacks-message-signing-request/stacks-message-signing-request';
import { TransactionRequest } from '@app/pages/transaction-request/transaction-request';
import { UnauthorizedRequest } from '@app/pages/unauthorized-request/unauthorized-request';
import { Unlock } from '@app/pages/unlock';
@@ -211,7 +212,7 @@ function AppRoutesAfterUserHasConsented() {
element={
<AccountGate>
<Suspense fallback={<LoadingSpinner height="600px" />}>
<MessageSigningRequest />
<StacksMessageSigningRequest />
</Suspense>
</AccountGate>
}
@@ -262,6 +263,14 @@ function AppRoutesAfterUserHasConsented() {
}
/>
{rpcSendTransferRoutes}
<Route
path={RouteUrls.RpcSignBip322Message}
element={
<AccountGate>
<RpcSignBip322Message />
</AccountGate>
}
/>
{/* Catch-all route redirects to onboarding */}
<Route path="*" element={<Navigate replace to={RouteUrls.Onboarding} />} />

View File

@@ -66,7 +66,12 @@ export const selectTestnetTaprootKeychain = bitcoinKeychainSelectorFactory(
'testnet'
);
export function useBitcoinLibNetworkConfig() {
export function useBitcoinScureLibNetworkConfig() {
const network = useCurrentNetwork();
return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network);
}
export function useBitcoinJsLibNetworkConfig() {
const network = useCurrentNetwork();
return getBtcSignerLibNetworkConfigByMode(network.chain.bitcoin.network);
}

View File

@@ -27,7 +27,7 @@ import {
selectTestnetNativeSegWitKeychain,
} from './bitcoin-keychain';
function useNativeSegWitCurrentNetworkAccountKeychain() {
function useNativeSegwitAccountKeychain() {
const network = useCurrentNetwork();
return useSelector(
whenNetwork(bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network))({
@@ -36,6 +36,11 @@ function useNativeSegWitCurrentNetworkAccountKeychain() {
})
);
}
export function useNativeSegwitCurrentAccountPrivateKeychain() {
const keychain = useNativeSegwitAccountKeychain();
const currentAccountIndex = useCurrentAccountIndex();
return keychain?.(currentAccountIndex);
}
function useCurrentBitcoinNativeSegwitAccountPublicKeychain() {
const { xpub } = useCurrentBitcoinNativeSegwitAccountInfo();
@@ -81,7 +86,7 @@ export function useAllBitcoinNativeSegWitNetworksByAccount() {
}
function useBitcoinNativeSegwitAccountInfo(index: number) {
const keychain = useNativeSegWitCurrentNetworkAccountKeychain();
const keychain = useNativeSegwitAccountKeychain();
return useMemo(() => {
// TODO: Remove with bitcoin Ledger integration
if (isUndefined(keychain)) return tempHardwareAccountForTesting;
@@ -119,7 +124,7 @@ export function useBtcNativeSegwitAccountIndexAddressIndexZero(accountIndex: num
export function useCurrentAccountNativeSegwitSigner() {
const network = useCurrentNetwork();
const index = useCurrentAccountIndex();
const accountKeychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index);
const accountKeychain = useNativeSegwitAccountKeychain()?.(index);
if (!accountKeychain) return;
const addressIndexKeychainFn = deriveAddressIndexKeychainFromAccount(accountKeychain);

View File

@@ -7,6 +7,7 @@ import { RootState } from '@app/store';
const selectSettings = (state: RootState) => state.settings;
const selectUserSelectedTheme = createSelector(selectSettings, state => state.userSelectedTheme);
export function useUserSelectedTheme() {
return useSelector(selectUserSelectedTheme);
}

View File

@@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useAsync } from 'react-async-hook';
import { SignedMessageType } from '@shared/signature/signature-types';
import { isString } from '@shared/utils';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
@@ -16,7 +17,7 @@ export function useSignatureRequestSearchParams() {
return useMemo(() => {
const requestToken = initialSearchParams.get('request');
const messageType = initialSearchParams.get('messageType');
const messageType = initialSearchParams.get('messageType') as SignedMessageType;
return {
tabId: isString(tabId) ? parseInt(tabId, 10) : tabId,

View File

@@ -58,7 +58,7 @@ export function makeSearchParamsWithDefaults(
const origin = getOriginFromPort(port);
const tabId = getTabIdFromPort(port);
urlParams.set('origin', origin ?? '');
urlParams.set('tabId', tabId?.toString() ?? '');
urlParams.set('tabId', tabId.toString());
otherParams.forEach(([key, value]) => urlParams.set(key, value));
return { urlParams, origin, tabId };
}

View File

@@ -1,5 +1,6 @@
import { RpcErrorCode } from '@btckit/types';
import { isSupportedMessageSigningPaymentType } from '@shared/crypto/bitcoin/bip322/bip322-utils';
import { RouteUrls } from '@shared/route-urls';
import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
@@ -15,9 +16,73 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
case 'getAddresses': {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcGetAddresses, urlParams);
listenForPopupClose({ tabId, id, response: { id: message.id, result: null } });
listenForPopupClose({
tabId,
id,
response: {
id: message.id,
result: makeRpcErrorResponse('getAddresses', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected request to get addresses',
},
}),
},
});
break;
}
case 'signMessage': {
if (!message.params || !message.params.message) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signMessage', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_PARAMS,
message:
'Invalid parameters. Message signing requires a message. See the btckit spec for more information: https://btckit.org/docs/spec',
},
})
);
break;
}
const paymentType = (message.params as any).paymentType ?? 'p2wpkh';
if (!isSupportedMessageSigningPaymentType(paymentType)) {
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('signMessage', {
id: message.id,
error: {
code: RpcErrorCode.INVALID_PARAMS,
message:
'Unsupported payment type. Hiro Wallet only supports signing messages for Native Segwit (p2wpkh) and Taproot (p2tr) addresses.',
},
})
);
return;
}
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [
['message', message.params.message],
['requestId', message.id],
['paymentType', paymentType],
]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcSignBip322Message, urlParams);
listenForPopupClose({
tabId,
id,
response: makeRpcErrorResponse('signMessage', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected the message signature',
},
}),
});
break;
}
case 'sendTransfer': {
if (!message.params) {
chrome.tabs.sendMessage(
@@ -52,5 +117,18 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
});
break;
}
default:
chrome.tabs.sendMessage(
getTabIdFromPort(port),
makeRpcErrorResponse('' as any, {
id: message.id,
error: {
code: RpcErrorCode.METHOD_NOT_FOUND,
message: `${message.method} is not supported`,
},
})
);
break;
}
}

View File

@@ -67,7 +67,13 @@ function isValidEvent(event: MessageEvent, method: LegacyMessageToContentScript[
return correctSource && correctMethod && !!data.payload;
}
const provider: StacksProvider = {
interface HiroWalletProviderOverrides extends StacksProvider {
isHiroWallet: true;
}
const provider: HiroWalletProviderOverrides = {
isHiroWallet: true,
getURL: async () => {
const { url } = await callAndReceive('getURL');
return url;
@@ -236,11 +242,9 @@ const provider: StacksProvider = {
return new Promise((resolve, reject) => {
function handleMessage(event: MessageEvent<WalletResponses>) {
const response = event.data;
if (!response || response.id !== id) return;
if (response.id !== id) return;
window.removeEventListener('message', handleMessage);
if ('error' in response) {
return reject(response);
}
if ('error' in response) return reject(response);
return resolve(response);
}
window.addEventListener('message', handleMessage);

View File

@@ -0,0 +1,18 @@
import { hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
import { hashBip322Message } from './bip322-utils';
describe('Message hashing', () => {
test('empty string', () =>
expect(hashBip322Message(utf8ToBytes(''))).toEqual(
hexToBytes('c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1')
));
const helloWorld = 'Hello World';
test(helloWorld, () =>
expect(hashBip322Message(utf8ToBytes(helloWorld))).toEqual(
hexToBytes('f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a')
)
);
});

View File

@@ -0,0 +1,89 @@
// Compatible package that isn't tiny-secp256k1
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import ecc from '@bitcoinerlab/secp256k1';
import { PaymentTypes } from '@btckit/types';
import { sha256 } from '@noble/hashes/sha256';
import { hexToBytes, utf8ToBytes } from '@stacks/common';
import * as bitcoin from 'bitcoinjs-lib';
import ECPairFactory from 'ecpair';
import { encode } from 'varuint-bitcoin';
import { isString } from '@shared/utils';
import { toXOnly } from '../bitcoin.utils';
const bip322MessageTag = 'BIP0322-signed-message';
const ECPair = ECPairFactory(ecc);
bitcoin.initEccLib(ecc);
export function ecPairFromPrivateKey(key: Uint8Array) {
return ECPair.fromPrivateKey(Buffer.from(key));
}
// See tagged hashes section of BIP-340
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#design
const messageTagHash = Uint8Array.from([
...sha256(utf8ToBytes(bip322MessageTag)),
...sha256(utf8ToBytes(bip322MessageTag)),
]);
export function hashBip322Message(message: Uint8Array | string) {
return sha256(
Uint8Array.from([...messageTagHash, ...(isString(message) ? utf8ToBytes(message) : message)])
);
}
export const bip322TransactionToSignValues = {
prevoutHash: hexToBytes('0000000000000000000000000000000000000000000000000000000000000000'),
prevoutIndex: 0xffffffff,
sequence: 0,
};
function encodeVarString(b: Buffer) {
return Buffer.concat([encode(b.byteLength), b]);
}
const supportedMessageSigningPaymentTypes: PaymentTypes[] = ['p2wpkh', 'p2tr'];
export function isSupportedMessageSigningPaymentType(paymentType: string) {
return supportedMessageSigningPaymentTypes.includes(paymentType as PaymentTypes);
}
/**
* Encode witness data for a BIP322 message
* TODO: Refactor to remove `Buffer` use
*/
export function encodeMessageWitnessData(witnessArray: Buffer[]) {
const len = encode(witnessArray.length);
return Buffer.concat([len, ...witnessArray.map(witness => encodeVarString(witness))]);
}
function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
return bitcoin.crypto.taggedHash('TapTweak', Buffer.concat(h ? [pubKey, h] : [pubKey]));
}
export function tweakSigner(signer: bitcoin.Signer, opts: any = {}): bitcoin.Signer {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let privateKey: Uint8Array | undefined = signer.privateKey!;
if (!privateKey) {
throw new Error('Private key is required for tweaking signer!');
}
if (signer.publicKey[0] === 3) {
privateKey = ecc.privateNegate(privateKey);
}
const tweakedPrivateKey = ecc.privateAdd(
privateKey,
tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash)
);
if (!tweakedPrivateKey) {
throw new Error('Invalid tweaked private key!');
}
return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
network: opts.network,
});
}

View File

@@ -0,0 +1,90 @@
import { base64 } from '@scure/base';
import * as bitcoin from 'bitcoinjs-lib';
import { BitcoinNetworkModes } from '@shared/constants';
import { getBitcoinJsLibNetworkConfigByMode } from '../bitcoin.network';
import {
bip322TransactionToSignValues,
ecPairFromPrivateKey,
encodeMessageWitnessData,
hashBip322Message,
tweakSigner,
} from './bip322-utils';
export function createNativeSegwitBitcoinJsSigner(privateKey: Buffer) {
return ecPairFromPrivateKey(privateKey);
}
export function createTaprootBitcoinJsSigner(privateKey: Buffer) {
return tweakSigner(ecPairFromPrivateKey(privateKey));
}
export function createToSpendTx(address: string, message: string, network: BitcoinNetworkModes) {
const { prevoutHash, prevoutIndex, sequence } = bip322TransactionToSignValues;
const script = bitcoin.address.toOutputScript(
address,
getBitcoinJsLibNetworkConfigByMode(network)
);
const hash = hashBip322Message(message);
const commands = [0, Buffer.from(hash)];
const scriptSig = bitcoin.script.compile(commands);
const virtualToSpend = new bitcoin.Transaction();
virtualToSpend.version = 0;
virtualToSpend.addInput(Buffer.from(prevoutHash), prevoutIndex, sequence, scriptSig);
virtualToSpend.addOutput(script, 0);
return { virtualToSpend, script };
}
function createToSignTx(toSpendTxHex: Buffer, script: Buffer, network: BitcoinNetworkModes) {
const virtualToSign = new bitcoin.Psbt({ network: getBitcoinJsLibNetworkConfigByMode(network) });
virtualToSign.setVersion(0);
const prevTxHash = toSpendTxHex;
const prevOutIndex = 0;
const toSignScriptSig = bitcoin.script.compile([106]);
virtualToSign.addInput({
hash: prevTxHash,
index: prevOutIndex,
sequence: 0,
witnessUtxo: { script, value: 0 },
});
virtualToSign.addOutput({ script: toSignScriptSig, value: 0 });
return virtualToSign;
}
interface SignBip322MessageSimple {
address: string;
message: string;
network: BitcoinNetworkModes;
signPsbt(psbt: bitcoin.Psbt): void;
}
export function signBip322MessageSimple(args: SignBip322MessageSimple) {
const { address, message, network, signPsbt } = args;
const { virtualToSpend, script } = createToSpendTx(address, message, network);
const virtualToSign = createToSignTx(virtualToSpend.getHash(), script, network);
signPsbt(virtualToSign);
virtualToSign.finalizeInput(0);
// sign the tx
// section 5.1
// github.com/LegReq/bip0322-signatures/blob/master/BIP0322_signing.ipynb
const toSignTx = virtualToSign.extractTransaction();
const result = encodeMessageWitnessData(toSignTx.ins[0].witness);
return {
virtualToSpend,
virtualToSign: toSignTx,
unencodedSig: result,
signature: base64.encode(result),
};
}

View File

@@ -0,0 +1,166 @@
import * as secp from '@noble/secp256k1';
import * as btc from '@scure/btc-signer';
import { bytesToHex } from '@stacks/common';
import * as bitcoin from 'bitcoinjs-lib';
import { ecdsaPublicKeyToSchnorr } from '../bitcoin.utils';
import {
createNativeSegwitBitcoinJsSigner,
createTaprootBitcoinJsSigner,
createToSpendTx,
signBip322MessageSimple,
} from './sign-message-bip322-bitcoinjs';
describe(createToSpendTx.name, () => {
test('bitcoinjs example', () => {
const result = createToSpendTx(
'bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l',
'generatedWithBitcoinJs',
'mainnet'
);
expect(result.script.toString('hex')).toEqual('00142b05d564e6a7a33c087f16e0f730d1440123799d');
expect(result.virtualToSpend.toHex()).toEqual(
'00000000010000000000000000000000000000000000000000000000000000000000000000ffffffff220020093bbd44da65116318b960749b3d6172ab9775b5d1923a7c71e18845c6524852000000000100000000000000001600142b05d564e6a7a33c087f16e0f730d1440123799d00000000'
);
});
});
//
// https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki#test-vectors
describe(signBip322MessageSimple.name, () => {
const testVectorKey = btc.WIF().decode('L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k');
describe('Message signing, Native Segwit', () => {
const nativeSegwitAddress = btc.getAddress('wpkh', testVectorKey);
const payment = btc.p2wpkh(secp.getPublicKey(testVectorKey, true));
function signPsbt(psbt: bitcoin.Psbt) {
psbt.signAllInputs(createNativeSegwitBitcoinJsSigner(Buffer.from(testVectorKey)));
}
if (!nativeSegwitAddress) throw new Error('nativeSegwitAddress is undefined');
test('Addresses against native segwit test vectors', () => {
expect(nativeSegwitAddress).toEqual('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
expect(payment.address).toEqual('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
});
test('Signature: "" (empty string)', () => {
const {
virtualToSpend: emptyStringToSpend,
virtualToSign: emptyStringToSign,
signature: emptyStringSig,
} = signBip322MessageSimple({
address: nativeSegwitAddress,
message: '',
signPsbt,
network: 'mainnet',
});
expect(emptyStringToSpend.getId()).toEqual(
'c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7'
);
expect(emptyStringToSign.getId()).toEqual(
'1e9654e951a5ba44c8604c4de6c67fd78a27e81dcadcfe1edf638ba3aaebaed6'
);
// Signature
// Bip322 says:
// AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=
expect(emptyStringSig).toEqual(
'AkgwRQIhAPkJ1Q4oYS0htvyuSFHLxRQpFAY56b70UvE7Dxazen0ZAiAtZfFz1S6T6I23MWI2lK/pcNTWncuyL8UL+oMdydVgzAEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy'
);
});
const helloWorld = 'Hello World';
test(`Signature: "${helloWorld}"`, () => {
const { virtualToSpend, virtualToSign, unencodedSig, signature } = signBip322MessageSimple({
address: nativeSegwitAddress,
message: helloWorld,
signPsbt,
network: 'mainnet',
});
// section 3
expect(virtualToSpend.getId()).toEqual(
'b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b'
);
// sectuion 4.3 expectedid
expect(virtualToSign.getId()).toEqual(
'88737ae86f2077145f93cc4b153ae9a1cb8d56afa511988c149c5c8c9d93bddf'
);
// sectioun 5.2 witness
expect(virtualToSign.ins[0].witness.map(bytesToHex).join(' ')).toEqual(
'3045022100ecf2ca796ab7dde538a26bfb09a6c487a7b3fff33f397db6a20eb9af77c0ee8c022062e67e44c8070f49c3a37f5940a8850842daf7cca35e6af61a6c7c91f1e1a1a301 02c7f12003196442943d8588e01aee840423cc54fc1521526a3b85c2b0cbd58872'
);
expect(unencodedSig.toString('hex')).toEqual(
'02483045022100ecf2ca796ab7dde538a26bfb09a6c487a7b3fff33f397db6a20eb9af77c0ee8c022062e67e44c8070f49c3a37f5940a8850842daf7cca35e6af61a6c7c91f1e1a1a3012102c7f12003196442943d8588e01aee840423cc54fc1521526a3b85c2b0cbd58872'
);
// Signature
// Bip322 says:
// AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=
expect(signature).toEqual(
'AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy'
);
});
});
describe('Message Signing, Taproot', () => {
const taprootAddress = btc.getAddress('tr', testVectorKey);
console.log('taprootAddress', taprootAddress);
if (!taprootAddress) throw new Error('Could not generate taproot address');
const payment = btc.p2tr(
ecdsaPublicKeyToSchnorr(secp.getPublicKey(Buffer.from(testVectorKey), true))
);
function signPsbt(psbt: bitcoin.Psbt) {
psbt.data.inputs.forEach(
input => (input.tapInternalKey = Buffer.from(payment.tapInternalKey))
);
psbt.signAllInputs(createTaprootBitcoinJsSigner(Buffer.from(testVectorKey)));
}
test('Addresses against taproot test vectors', () => {
expect(taprootAddress).toEqual(
'bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3'
);
expect(payment.address).toEqual(
'bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3'
);
});
// Taproot signatures verified with verifymessage request to node
test('Signature: "" (empty string)', () => {
const { signature } = signBip322MessageSimple({
address: taprootAddress,
message: '',
network: 'mainnet',
signPsbt,
});
expect(signature).toEqual(
'AUD4DxC7li8RxVkoC/H27LIZnaBD/ZCyZOjjVTzyQf7wa1kYMuyv1uX7XshysTXVR05HbexLSChXuGSoZcqJl6zF'
);
});
test('Signature: "HiroWalletIsTheBest"', () => {
const { signature } = signBip322MessageSimple({
address: taprootAddress,
message: 'HiroWalletIsTheBest',
network: 'mainnet',
signPsbt,
});
expect(signature).toEqual(
'AUAJNdp5SEAFCDYrIR8TQRkgbNo6P+dUeL97eadGjPWC8iPrhngZSBZvcImVk/HEb3zq3xuyGqyP0dqR7CH2HCa7'
);
});
});
});

View File

@@ -0,0 +1,54 @@
import * as btc from '@scure/btc-signer';
import { hexToBytes } from '@stacks/common';
import { hashBip322Message } from './bip322-utils';
// TODO: Complete this fn
// Ran into difficiulties with btc-signer vs bitcoinjs-lib
// Using that library to unblock for now, but we should go
// back and replace it when possible.
// ts-unused-exports:disable-next-line
export function signBip322MessageSimple(script: Uint8Array, message: string) {
// nVersion = 0
// nLockTime = 0
// vin[0].prevout.hash = 0000...000
// vin[0].prevout.n = 0xFFFFFFFF
// vin[0].nSequence = 0
// vin[0].scriptSig = OP_0 PUSH32[ message_hash ]
// vin[0].scriptWitness = []
// vout[0].nValue = 0
// vout[0].scriptPubKey = message_challenge
const prevoutHash = hexToBytes(
'0000000000000000000000000000000000000000000000000000000000000000'
);
const prevoutIndex = 0xffffffff;
const sequence = 0;
const hash = hashBip322Message(message);
const commands = [btc.OP.OP_0, hash];
const virtualToSpend = new btc.Transaction({
version: 0,
lockTime: 0,
allowUnknowInput: true,
allowUnknowOutput: true,
disableScriptCheck: true,
allowLegacyWitnessUtxo: true,
});
virtualToSpend.addInput({
txid: prevoutHash,
index: prevoutIndex,
sequence,
witnessScript: btc.Script.encode(commands),
});
virtualToSpend.addOutput({
script,
amount: 0n,
});
return { virtualToSpend };
}

View File

@@ -1,43 +1,45 @@
import * as bitcoinJs from 'bitcoinjs-lib';
import { BitcoinNetworkModes } from '@shared/constants';
// See this PR https://github.com/paulmillr/@scure/btc-signer/pull/15
// Atttempting to add these directly to the library
export interface BitcoinNetwork {
export interface BtcSignerNetwork {
bech32: string;
pubKeyHash: number;
scriptHash: number;
wif: number;
}
const bitcoinMainnet: BitcoinNetwork = {
const bitcoinMainnet: BtcSignerNetwork = {
bech32: 'bc',
pubKeyHash: 0x00,
scriptHash: 0x05,
wif: 0x80,
};
const bitcoinTestnet: BitcoinNetwork = {
const bitcoinTestnet: BtcSignerNetwork = {
bech32: 'tb',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
const bitcoinRegtest: BitcoinNetwork = {
const bitcoinRegtest: BtcSignerNetwork = {
bech32: 'bcrt',
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
const bitcoinSignet: BitcoinNetwork = {
const bitcoinSignet: BtcSignerNetwork = {
bech32: 'sb',
pubKeyHash: 0x3f,
scriptHash: 0x7f,
wif: 0x80,
};
const bitcoinNetworks: Record<BitcoinNetworkModes, BitcoinNetwork> = {
const btcSignerLibNetworks: Record<BitcoinNetworkModes, BtcSignerNetwork> = {
mainnet: bitcoinMainnet,
testnet: bitcoinTestnet,
regtest: bitcoinRegtest,
@@ -45,5 +47,17 @@ const bitcoinNetworks: Record<BitcoinNetworkModes, BitcoinNetwork> = {
};
export function getBtcSignerLibNetworkConfigByMode(network: BitcoinNetworkModes) {
return bitcoinNetworks[network];
return btcSignerLibNetworks[network];
}
const bitcoinJsLibNetworks: Record<BitcoinNetworkModes, bitcoinJs.Network> = {
mainnet: bitcoinJs.networks.bitcoin,
testnet: bitcoinJs.networks.testnet,
regtest: bitcoinJs.networks.regtest,
// Signet doesn't exist in bitcoinjs-lib
signet: bitcoinJs.networks.regtest,
};
export function getBitcoinJsLibNetworkConfigByMode(network: BitcoinNetworkModes) {
return bitcoinJsLibNetworks[network];
}

View File

@@ -7,7 +7,7 @@ import { BitcoinNetworkModes, NetworkModes } from '@shared/constants';
import { logger } from '@shared/logger';
import { DerivationPathDepth } from '../derivation-path.utils';
import { BitcoinNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
import { BtcSignerNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
const coinTypeMap: Record<NetworkModes, 0 | 1> = {
mainnet: 0,
@@ -36,6 +36,9 @@ export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
return pubKey.slice(1);
}
// basically same as above, to remov
export const toXOnly = (pubKey: Buffer) => (pubKey.length === 32 ? pubKey : pubKey.slice(1, 33));
export function decodeBitcoinTx(tx: string) {
return btc.RawTx.decode(hexToBytes(tx));
}
@@ -46,15 +49,15 @@ function formatKey(hashed: Uint8Array, prefix: number[]): string {
return btc.base58check.encode(concat(Uint8Array.from(prefix), hashed));
}
function getAddressFromWshOutScript(script: Uint8Array, network: BitcoinNetwork) {
function getAddressFromWshOutScript(script: Uint8Array, network: BtcSignerNetwork) {
return btc.programToWitness(0, script.slice(2), network);
}
function getAddressFromWpkhOutScript(script: Uint8Array, network: BitcoinNetwork) {
function getAddressFromWpkhOutScript(script: Uint8Array, network: BtcSignerNetwork) {
return btc.programToWitness(0, script.slice(2), network);
}
function getAddressFromTrOutScript(script: Uint8Array, network: BitcoinNetwork) {
function getAddressFromTrOutScript(script: Uint8Array, network: BtcSignerNetwork) {
return btc.programToWitness(1, script.slice(2), network);
}

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import ecc from '@bitcoinerlab/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { base58check } from '@scure/base';
import { HDKey } from '@scure/bip32';
@@ -5,7 +8,6 @@ import { hashP2WPKH } from '@stacks/transactions';
import { BIP32Factory } from 'bip32';
import * as bip39 from 'bip39';
import * as bitcoin from 'bitcoinjs-lib';
import * as ecc from 'tiny-secp256k1';
import {
decodeCompressedWifPrivateKey,

View File

@@ -85,4 +85,6 @@ export enum RouteUrls {
RpcSendTransferChooseFee = '/send-transfer/choose-fee',
RpcSendTransferConfirmation = '/send-transfer/confirm',
RpcSendTransferSummary = '/send-transfer/summary',
RpcSignBip322Message = '/sign-bip322-message',
}

View File

@@ -19,7 +19,6 @@ export function makeRpcSuccessResponse<T extends WalletMethodNames>(
return { jsonrpc: '2.0', ...response } as WalletMethodMap[T]['response'];
}
// ts-unused-exports:disable-next-line
export function makeRpcErrorResponse<T extends WalletMethodNames>(
_method: T,
response: Omit<ExtractErrorResponse<WalletMethodMap[T]['response']>, 'jsonrpc'>

View File

@@ -1,6 +1,6 @@
import { ClarityValue, StringAsciiCV, TupleCV, UIntCV } from '@stacks/transactions';
type SignedMessageType = 'utf8' | 'structured';
export type SignedMessageType = 'utf8' | 'structured';
export type StructuredMessageDataDomain = TupleCV<{
name: StringAsciiCV;
@@ -8,22 +8,22 @@ export type StructuredMessageDataDomain = TupleCV<{
'chain-id': UIntCV;
}>;
interface AbstractSignedMessage {
interface AbstractUnsignedMessage {
messageType: SignedMessageType;
}
interface SignedMessageUtf8 extends AbstractSignedMessage {
interface UnsignedMessageUtf8 extends AbstractUnsignedMessage {
messageType: 'utf8';
message: string;
}
export interface SignedMessageStructured extends AbstractSignedMessage {
export interface UnsignedMessageStructured extends AbstractUnsignedMessage {
messageType: 'structured';
message: ClarityValue;
domain: StructuredMessageDataDomain;
}
export type SignedMessage = SignedMessageUtf8 | SignedMessageStructured;
export type UnsignedMessage = UnsignedMessageUtf8 | UnsignedMessageStructured;
export function isStructuredMessageType(
messageType: SignedMessageType
@@ -35,18 +35,17 @@ export function isUtf8MessageType(messageType: SignedMessageType): messageType i
return messageType === 'utf8';
}
export function isSignedMessageType(messageType: unknown): messageType is SignedMessageType {
export function isSignableMessageType(messageType: unknown): messageType is SignedMessageType {
return typeof messageType === 'string' && ['utf8', 'structured'].includes(messageType);
}
interface WhenSignedMessageOfType<T> {
interface WhenSignableMessageOfType<T> {
utf8(message: string): T;
structured(domain: StructuredMessageDataDomain, message: ClarityValue): T;
}
export function whenSignedMessageOfType(msg: SignedMessage) {
return <T>({ utf8, structured }: WhenSignedMessageOfType<T>) => {
export function whenSignableMessageOfType(msg: UnsignedMessage) {
return <T>({ utf8, structured }: WhenSignableMessageOfType<T>) => {
if (msg.messageType === 'utf8') return utf8(msg.message);
if (msg.messageType === 'structured') return structured(msg.domain, msg.message);
throw new Error('Message can only be either `utf8` or `structured`');
return structured(msg.domain, msg.message);
};
}

View File

@@ -30,7 +30,7 @@ const TEST_TESTNET_ACCOUNT_1_PUBKEY_TR =
'03cf7525b9d94fd35eaf6b4ac4c570f718d1df142606ba3a64e2407ea01a37778f';
const TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS = 'tb1qkzvk9hr7uvas23hspvsgqfvyc8h4nngeqjqtnj';
function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
if (pubKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid public key length');
return pubKey.slice(1);
}

View File

@@ -5,14 +5,14 @@ import { OnboardingPage } from '@tests/page-object-models/onboarding.page';
import { SendPage } from '@tests/page-object-models/send.page';
import path from 'path';
type TestFixtures = {
interface TestFixtures {
context: BrowserContext;
extensionId: string;
globalPage: GlobalPage;
homePage: HomePage;
onboardingPage: OnboardingPage;
sendPage: SendPage;
};
}
/**
* Loads the extension into the browser context. Use this test function with

View File

@@ -13,6 +13,7 @@ export default defineConfig({
environment: 'node',
setupFiles: './tests-legacy/unit-test.setup.js',
deps: { interopDefault: true },
silent: false,
},
resolve: {
alias: {

5510
yarn.lock

File diff suppressed because it is too large Load Diff