-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
closes #1051
- Loading branch information
Showing
33 changed files
with
2,118 additions
and
1,358 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { | ||
ExternalMethods, | ||
MESSAGE_SOURCE, | ||
SignatureResponseMessage, | ||
} from '@shared/message-types'; | ||
import { deleteTabForRequest, getTab, StorageKey } from '@shared/utils/storage'; | ||
import { logger } from '@shared/logger'; | ||
import { SignatureData } from '@shared/crypto/sign-message'; | ||
|
||
export const finalizeMessageSignature = (requestPayload: string, data: SignatureData | string) => { | ||
try { | ||
const tabId = getTab(StorageKey.signatureRequests, requestPayload); | ||
const responseMessage: SignatureResponseMessage = { | ||
source: MESSAGE_SOURCE, | ||
method: ExternalMethods.signatureResponse, | ||
payload: { | ||
signatureRequest: requestPayload, | ||
signatureResponse: data, | ||
}, | ||
}; | ||
chrome.tabs.sendMessage(tabId, responseMessage); | ||
deleteTabForRequest(StorageKey.signatureRequests, requestPayload); | ||
// If this is a string, then the transaction has been canceled | ||
// and the user has closed the window | ||
window.close(); | ||
} catch (error) { | ||
logger.debug('Failed to get Tab ID for message signature request:', requestPayload); | ||
throw new Error( | ||
'Your message was signed, but we lost communication with the app you started with.' | ||
); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { getAppPrivateKey, Wallet } from '@stacks/wallet-sdk'; | ||
import { SignaturePayload } from '@stacks/connect'; | ||
import { decodeToken, TokenVerifier } from 'jsontokens'; | ||
import { getPublicKeyFromPrivate } from '@stacks/encryption'; | ||
import { getAddressFromPrivateKey, TransactionVersion } from '@stacks/transactions'; | ||
|
||
function getTransactionVersionFromRequest(signature: SignaturePayload) { | ||
const { network } = signature; | ||
if (!network) return TransactionVersion.Mainnet; | ||
if (![TransactionVersion.Mainnet, TransactionVersion.Testnet].includes(network.version)) { | ||
throw new Error('Invalid network version provided'); | ||
} | ||
return network.version; | ||
} | ||
|
||
export const UNAUTHORIZED_SIGNATURE_REQUEST = | ||
'The signature request provided is not signed by this wallet.'; | ||
/** | ||
* Verify a transaction request. | ||
* A transaction request is a signed JWT that is created on an app, | ||
* via `@stacks/connect`. The private key used to sign this JWT is an | ||
* `appPrivateKey`, which an app can get from authentication. | ||
* | ||
* The payload in this JWT can include an `stxAddress`. This indicates the | ||
* 'default' STX address that should be used to sign this transaction. This allows | ||
* the wallet to use the same account to sign a transaction as it used to sign | ||
* in to the app. | ||
* | ||
* This JWT is invalidated if: | ||
* - The JWT is not signed properly | ||
* - The public key used to sign this tx request does not match an `appPrivateKey` | ||
* for any of the accounts in this wallet. | ||
* - The `stxAddress` provided in the payload does not match an STX address | ||
* for any of the accounts in this wallet. | ||
* | ||
* @returns The decoded and validated `SignaturePayload` | ||
* @throws if the transaction request is invalid | ||
*/ | ||
interface VerifySignatureRequestArgs { | ||
requestToken: string; | ||
wallet: Wallet; | ||
appDomain: string; | ||
} | ||
export async function verifySignatureRequest({ | ||
requestToken, | ||
wallet, | ||
appDomain, | ||
}: VerifySignatureRequestArgs): Promise<SignaturePayload> { | ||
const token = decodeToken(requestToken); | ||
const signature = token.payload as unknown as SignaturePayload; | ||
const { publicKey, stxAddress } = signature; | ||
const txVersion = getTransactionVersionFromRequest(signature); | ||
const verifier = new TokenVerifier('ES256k', publicKey); | ||
const isSigned = await verifier.verifyAsync(requestToken); | ||
if (!isSigned) { | ||
throw new Error('Transaction request is not signed'); | ||
} | ||
const foundAccount = wallet.accounts.find(account => { | ||
const appPrivateKey = getAppPrivateKey({ | ||
account, | ||
appDomain, | ||
}); | ||
const appPublicKey = getPublicKeyFromPrivate(appPrivateKey); | ||
if (appPublicKey !== publicKey) return false; | ||
if (!stxAddress) return true; | ||
const accountStxAddress = getAddressFromPrivateKey(account.stxPrivateKey, txVersion); | ||
if (stxAddress !== accountStxAddress) return false; | ||
return true; | ||
}); | ||
if (!foundAccount) { | ||
throw new Error(UNAUTHORIZED_SIGNATURE_REQUEST); | ||
} | ||
return signature; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
src/app/pages/signature-request/components/hash-drawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { Stack, Flex, Box, Text } from '@stacks/ui'; | ||
import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; | ||
import { useState } from 'react'; | ||
|
||
|
||
interface ShowHashButtonProps { | ||
expanded: boolean; | ||
} | ||
const ShowHashButton = (props: ShowHashButtonProps) => { | ||
const { expanded } = props; | ||
console.log('rerender ShowHashButton'); | ||
return (<Box as={expanded ? FiChevronDown : FiChevronUp} mr="tight" size="20px" />); | ||
} | ||
|
||
interface HashDrawerProps { | ||
hash: string; | ||
} | ||
|
||
export function HashDrawer(props: HashDrawerProps): JSX.Element | null { | ||
const { hash } = props; | ||
const [showHash, setShowHash] = useState(false); | ||
const [displayHash, setDisplayHash] = useState(hash); | ||
return ( | ||
<Stack | ||
py="tight" | ||
px="tight" | ||
spacing="loose" | ||
> | ||
<Flex marginBottom="0px !important"> | ||
<Text display="block" | ||
fontSize={'12px'}> | ||
{showHash ? 'Hide hash' : 'Show hash'} | ||
</Text> | ||
<Box | ||
_hover={{ cursor: 'pointer' }} | ||
style={{ marginLeft: 'auto' }} onClick={() => { setDisplayHash(showHash ? '' : hash); setShowHash(!showHash) }}> | ||
<ShowHashButton expanded={showHash} /> | ||
</Box> | ||
</Flex> | ||
<Box | ||
transition="all 0.65s cubic-bezier(0.23, 1, 0.32, 1)" | ||
py={showHash ? 'tight' : 'none'} | ||
height={showHash ? '100%' : '0'} | ||
visibility={showHash ? 'visible' : 'hidden'} | ||
> | ||
<Stack spacing="base-tight"> | ||
<Text display="block" color='#74777D' fontSize={2} fontWeight={500} lineHeight="1.6" wordBreak="break-all" fontFamily={'Fira Code'}> | ||
{displayHash} | ||
</Text> | ||
</Stack> | ||
</Box > | ||
</Stack > | ||
) | ||
} |
41 changes: 41 additions & 0 deletions
41
src/app/pages/signature-request/components/message-box.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { color, Stack } from '@stacks/ui'; | ||
import { Row } from '@app/pages/transaction-request/components/row'; | ||
import { sha256 } from 'sha.js'; | ||
import { HashDrawer } from './hash-drawer'; | ||
import { useEffect, useState } from 'react'; | ||
|
||
interface MessageBoxProps { | ||
message: string; | ||
} | ||
export function MessageBox(props: MessageBoxProps): JSX.Element | null { | ||
const { message } = props; | ||
if (!message) return null; | ||
const [hash, setHash] = useState<string | undefined>(); | ||
|
||
useEffect(() => { | ||
setHash(new sha256().update(message).digest('hex')); | ||
}, [message]) | ||
|
||
return ( | ||
<> | ||
<Stack minHeight={'260px'}> | ||
<Stack | ||
border="4px solid" | ||
borderColor={color('border')} | ||
borderRadius="12px" | ||
backgroundColor={color('border')} | ||
> | ||
<Stack | ||
py="base-tight" | ||
px="base-loose" | ||
spacing="loose" | ||
borderRadius="12px" | ||
backgroundColor={'white'}> | ||
<Row value={message} /> | ||
</Stack> | ||
{hash ? <HashDrawer hash={hash}/> : null} | ||
</Stack> | ||
</Stack> | ||
</> | ||
); | ||
} |
28 changes: 28 additions & 0 deletions
28
src/app/pages/signature-request/components/network-row.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { useCurrentNetwork } from "@app/common/hooks/use-current-network"; | ||
import { SpaceBetween } from "@app/components/space-between"; | ||
import { Caption } from "@app/components/typography"; | ||
import { StacksNetwork } from "@stacks/network"; | ||
import { ChainID } from "@stacks/transactions"; | ||
import { Stack } from "@stacks/ui"; | ||
import { useMemo } from "react"; | ||
|
||
interface NetworkRowProps { | ||
network: StacksNetwork; | ||
} | ||
export function NetworkRow(props: NetworkRowProps): JSX.Element | null { | ||
const { network } = props; | ||
const isTestnetChain = useMemo(() => network.chainId === ChainID.Testnet, [network.chainId]); | ||
|
||
return ( | ||
<Stack spacing="base"> | ||
<SpaceBetween position="relative"> | ||
<Stack alignItems="center" isInline> | ||
<Caption>No fees will be incured</Caption> | ||
</Stack> | ||
<Caption> | ||
<span>{isTestnetChain ? 'Testnet' : 'Mainnet'}</span> | ||
</Caption> | ||
</SpaceBetween> | ||
</Stack> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { memo } from 'react'; | ||
import { Stack } from '@stacks/ui'; | ||
|
||
import { useCurrentNetwork } from '@app/common/hooks/use-current-network'; | ||
import { addPortSuffix, getUrlHostname } from '@app/common/utils'; | ||
import { Caption, Title } from '@app/components/typography'; | ||
import { useOrigin, useSignatureRequestState } from '@app/store/signatures/requests.hooks'; | ||
|
||
function PageTopBase(): JSX.Element | null { | ||
const signatureRequest = useSignatureRequestState(); | ||
const origin = useOrigin(); | ||
const network = useCurrentNetwork(); | ||
if (!signatureRequest) return null; | ||
|
||
const appName = signatureRequest?.appDetails?.name; | ||
const originAddition = origin ? ` (${getUrlHostname(origin)})` : ''; | ||
const testnetAddition = network.isTestnet | ||
? ` using ${getUrlHostname(network.url)}${addPortSuffix(network.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); |
70 changes: 70 additions & 0 deletions
70
src/app/pages/signature-request/components/sign-action.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { finalizeMessageSignature } from '@app/common/actions/finalize-message-signature'; | ||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; | ||
import { useCurrentAccount } from '@app/store/accounts/account.hooks'; | ||
import { useRequestTokenState } from '@app/store/signatures/requests.hooks'; | ||
import { signMessage } from '@shared/crypto/sign-message'; | ||
import { logger } from '@shared/logger'; | ||
import { createStacksPrivateKey } from '@stacks/transactions'; | ||
import { Button, Stack } from '@stacks/ui'; | ||
import { useCallback, useState } from 'react'; | ||
|
||
export function useSignMessageSoftwareWallet() { | ||
const account = useCurrentAccount(); | ||
return useCallback( | ||
(message: string) => { | ||
if (!account) return null; | ||
const privateKey = createStacksPrivateKey(account.stxPrivateKey) | ||
return signMessage(message, privateKey); | ||
}, | ||
[account] | ||
); | ||
} | ||
|
||
interface SignActionProps { | ||
message: string; | ||
} | ||
export function SignAction(props: SignActionProps): JSX.Element { | ||
const { message } = props; | ||
const signSoftwareWalletMessage = useSignMessageSoftwareWallet(); | ||
const requestToken = useRequestTokenState(); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const analytics = useAnalytics(); | ||
|
||
const sign = () => { | ||
setIsLoading(true); | ||
void analytics.track('request_signature_sign'); | ||
const messageSignature = signSoftwareWalletMessage(message); | ||
console.log('signed Message', messageSignature); | ||
if (!messageSignature) { | ||
logger.error('Cannot sign message, no account in state'); | ||
void analytics.track('request_signature_cannot_sign_message_no_account'); | ||
return; | ||
} | ||
setTimeout(() => { // Since the signature is really fast, we add a delay to improve the UX | ||
setIsLoading(false); | ||
finalizeMessageSignature(requestToken, messageSignature); | ||
}, 1000); | ||
} | ||
|
||
const cancel = () => { | ||
void analytics.track('request_signature_cancel'); | ||
finalizeMessageSignature(requestToken, 'cancel'); | ||
} | ||
|
||
return ( | ||
<Stack isInline> | ||
<Button onClick={cancel} flexGrow={1} borderRadius="10px" mode="tertiary"> | ||
Cancel | ||
</Button> | ||
<Button | ||
type="submit" | ||
flexGrow={1} | ||
borderRadius="10px" | ||
onClick={sign} | ||
isLoading={isLoading} | ||
> | ||
Sign | ||
</Button> | ||
</Stack> | ||
); | ||
} |
Oops, something went wrong.