mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-01-12 09:34:37 +08:00
feat: bip322, closes #3386
This commit is contained in:
@@ -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],
|
||||
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })));
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
33
src/app/features/message-signer/message-signing-header.tsx
Normal file
33
src/app/features/message-signer/message-signing-header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
160
src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts
Normal file
160
src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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} />
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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} />} />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
18
src/shared/crypto/bitcoin/bip322/bip322-utils.spec.ts
Normal file
18
src/shared/crypto/bitcoin/bip322/bip322-utils.spec.ts
Normal 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')
|
||||
)
|
||||
);
|
||||
});
|
||||
89
src/shared/crypto/bitcoin/bip322/bip322-utils.ts
Normal file
89
src/shared/crypto/bitcoin/bip322/bip322-utils.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
166
src/shared/crypto/bitcoin/bip322/sign-message-bip322.spec.ts
Normal file
166
src/shared/crypto/bitcoin/bip322/sign-message-bip322.spec.ts
Normal 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
54
src/shared/crypto/bitcoin/bip322/sign-message-bip322.ts
Normal file
54
src/shared/crypto/bitcoin/bip322/sign-message-bip322.ts
Normal 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 };
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -85,4 +85,6 @@ export enum RouteUrls {
|
||||
RpcSendTransferChooseFee = '/send-transfer/choose-fee',
|
||||
RpcSendTransferConfirmation = '/send-transfer/confirm',
|
||||
RpcSendTransferSummary = '/send-transfer/summary',
|
||||
|
||||
RpcSignBip322Message = '/sign-bip322-message',
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
4
tests/fixtures/fixtures.ts
vendored
4
tests/fixtures/fixtures.ts
vendored
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
environment: 'node',
|
||||
setupFiles: './tests-legacy/unit-test.setup.js',
|
||||
deps: { interopDefault: true },
|
||||
silent: false,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user