-
Notifications
You must be signed in to change notification settings - Fork 144
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add rpc method for signing stacks messages
- Loading branch information
1 parent
6b7ce6a
commit e77a8d8
Showing
26 changed files
with
704 additions
and
205 deletions.
There are no files selected for viewing
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
...components/message-signing-disclaimer.tsx → ...components/message-signing-disclaimer.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
File renamed without changes.
41 changes: 41 additions & 0 deletions
41
src/app/features/stacks-message-signer/components/stacks-signature-message-content.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 { ChainID, bytesToHex } from '@stacks/common'; | ||
import { hashMessage } from '@stacks/encryption'; | ||
|
||
import { UnsignedMessage } from '@shared/signature/signature-types'; | ||
|
||
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 { Utf8Payload } from '../stacks-message-signing'; | ||
import { StacksMessageSigningDisclaimer } from './message-signing-disclaimer'; | ||
|
||
interface SignatureRequestMessageContentProps { | ||
isLoading: boolean; | ||
onSignMessage(unsignedMessage: UnsignedMessage): Promise<void>; | ||
onCancelMessageSigning(): void; | ||
payload: Utf8Payload; | ||
} | ||
export function StacksSignatureRequestMessageContent({ | ||
isLoading, | ||
onSignMessage, | ||
onCancelMessageSigning, | ||
payload, | ||
}: SignatureRequestMessageContentProps) { | ||
return ( | ||
<> | ||
<MessagePreviewBox | ||
message={payload.message} | ||
hash={bytesToHex(hashMessage(payload.message))} | ||
/> | ||
<NoFeesWarningRow chainId={payload.network?.chainId ?? ChainID.Testnet} /> | ||
<SignMessageActions | ||
isLoading={isLoading} | ||
onSignMessageCancel={onCancelMessageSigning} | ||
onSignMessage={() => onSignMessage({ messageType: 'utf8', message: payload.message })} | ||
/> | ||
<hr /> | ||
<StacksMessageSigningDisclaimer appName={payload.appName} /> | ||
</> | ||
); | ||
} |
File renamed without changes.
43 changes: 43 additions & 0 deletions
43
src/app/features/stacks-message-signer/components/structured-data-content.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,43 @@ | ||
import { ChainID } from '@stacks/common'; | ||
|
||
import { UnsignedMessage } from '@shared/signature/signature-types'; | ||
|
||
import { NoFeesWarningRow } from '@app/components/no-fees-warning-row'; | ||
import { SignMessageActions } from '@app/features/message-signer/stacks-sign-message-action'; | ||
|
||
import { StructuredPayload } from '../stacks-message-signing'; | ||
import { StacksMessageSigningDisclaimer } from './message-signing-disclaimer'; | ||
import { StructuredDataBox } from './structured-data-box'; | ||
|
||
interface SignatureRequestStructuredDataContentProps { | ||
isLoading: boolean; | ||
onSignMessage(unsignedMessage: UnsignedMessage): Promise<void>; | ||
onCancelMessageSigning(): void; | ||
payload: StructuredPayload; | ||
} | ||
export function SignatureRequestStructuredDataContent({ | ||
isLoading, | ||
onSignMessage, | ||
onCancelMessageSigning, | ||
payload, | ||
}: SignatureRequestStructuredDataContentProps) { | ||
return ( | ||
<> | ||
<StructuredDataBox message={payload.message} domain={payload.domain} /> | ||
<NoFeesWarningRow chainId={payload.network?.chainId ?? ChainID.Testnet} /> | ||
<SignMessageActions | ||
isLoading={isLoading} | ||
onSignMessageCancel={onCancelMessageSigning} | ||
onSignMessage={() => | ||
onSignMessage({ | ||
messageType: 'structured', | ||
message: payload.message, | ||
domain: payload.domain, | ||
}) | ||
} | ||
/> | ||
<hr /> | ||
<StacksMessageSigningDisclaimer appName={payload.appName} /> | ||
</> | ||
); | ||
} |
89 changes: 89 additions & 0 deletions
89
src/app/features/stacks-message-signer/stacks-message-signing.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,89 @@ | ||
import { Outlet } from 'react-router-dom'; | ||
|
||
import { StacksNetwork } from '@stacks/network'; | ||
import { ClarityValue } from '@stacks/transactions/dist/esm/clarity'; | ||
|
||
import { | ||
SignedMessageType, | ||
StructuredMessageDataDomain, | ||
UnsignedMessage, | ||
isSignableMessageType, | ||
isStructuredMessageType, | ||
isUtf8MessageType, | ||
} from '@shared/signature/signature-types'; | ||
import { closeWindow } from '@shared/utils'; | ||
|
||
import { useRouteHeader } from '@app/common/hooks/use-route-header'; | ||
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 { MessageSigningRequestLayout } from '../message-signer/message-signing-request.layout'; | ||
import { StacksSignatureRequestMessageContent } from './components/stacks-signature-message-content'; | ||
import { SignatureRequestStructuredDataContent } from './components/structured-data-content'; | ||
|
||
export interface Utf8Payload { | ||
messageType: 'utf8'; | ||
message: string; | ||
network: StacksNetwork | undefined; | ||
appName: string | undefined | null; | ||
} | ||
|
||
export interface StructuredPayload { | ||
messageType: 'structured'; | ||
message: ClarityValue; | ||
network: StacksNetwork | undefined; | ||
appName: string | undefined | null; | ||
domain: StructuredMessageDataDomain; | ||
} | ||
|
||
interface StacksMessageSigningProps { | ||
messageType: SignedMessageType; | ||
tabId: number | null; | ||
origin: string | null; | ||
isLoading: boolean; | ||
onSignMessage(unsignedMessage: UnsignedMessage): Promise<void>; | ||
onCancelMessageSigning(): void; | ||
payload: Utf8Payload | StructuredPayload; | ||
} | ||
|
||
export function StacksMessageSigning({ | ||
messageType, | ||
tabId, | ||
origin, | ||
isLoading, | ||
onSignMessage, | ||
onCancelMessageSigning, | ||
payload, | ||
}: StacksMessageSigningProps) { | ||
useRouteHeader(<PopupHeader />); | ||
useOnOriginTabClose(() => closeWindow()); | ||
|
||
if (!tabId) return null; | ||
if (!isSignableMessageType(messageType)) return null; | ||
if (!origin) return null; | ||
|
||
return ( | ||
<MessageSigningRequestLayout> | ||
<MessageSigningHeader name={origin} origin={origin} /> | ||
|
||
{isUtf8MessageType(messageType) && payload.messageType === 'utf8' && ( | ||
<StacksSignatureRequestMessageContent | ||
isLoading={isLoading} | ||
onSignMessage={onSignMessage} | ||
onCancelMessageSigning={onCancelMessageSigning} | ||
payload={payload} | ||
/> | ||
)} | ||
{isStructuredMessageType(messageType) && payload.messageType === 'structured' && ( | ||
<SignatureRequestStructuredDataContent | ||
isLoading={isLoading} | ||
onSignMessage={onSignMessage} | ||
onCancelMessageSigning={onCancelMessageSigning} | ||
payload={payload} | ||
/> | ||
)} | ||
<Outlet /> | ||
</MessageSigningRequestLayout> | ||
); | ||
} |
32 changes: 32 additions & 0 deletions
32
src/app/features/stacks-message-signer/stacks-message-signing.utils.ts
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 { useCallback } from 'react'; | ||
|
||
import { ClarityValue, TupleCV, createStacksPrivateKey } from '@stacks/transactions'; | ||
|
||
import { signMessage, signStructuredDataMessage } from '@shared/crypto/sign-message'; | ||
import { isString } from '@shared/utils'; | ||
|
||
import { createDelay } from '@app/common/utils'; | ||
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; | ||
|
||
export const improveUxWithShortDelayAsStacksSigningIsSoFast = createDelay(1000); | ||
|
||
export 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] | ||
); | ||
} |
61 changes: 61 additions & 0 deletions
61
src/app/features/stacks-message-signer/use-sign-stacks-message.ts
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,61 @@ | ||
import { useState } from 'react'; | ||
|
||
import { SignatureData } from '@stacks/connect'; | ||
|
||
import { logger } from '@shared/logger'; | ||
import { UnsignedMessage, whenSignableMessageOfType } from '@shared/signature/signature-types'; | ||
|
||
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; | ||
import { useWalletType } from '@app/common/use-wallet-type'; | ||
import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate'; | ||
import { | ||
improveUxWithShortDelayAsStacksSigningIsSoFast, | ||
useMessageSignerStacksSoftwareWallet, | ||
} from '@app/features/stacks-message-signer/stacks-message-signing.utils'; | ||
|
||
interface SignStacksMessageProps { | ||
onSignMessageCompleted(messageSignature: SignatureData): void; | ||
} | ||
|
||
export function useSignStacksMessage({ onSignMessageCompleted }: SignStacksMessageProps) { | ||
const analytics = useAnalytics(); | ||
const signSoftwareWalletMessage = useMessageSignerStacksSoftwareWallet(); | ||
|
||
const { whenWallet } = useWalletType(); | ||
const ledgerNavigate = useLedgerNavigate(); | ||
|
||
const [isLoading, setIsLoading] = useState(false); | ||
|
||
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 improveUxWithShortDelayAsStacksSigningIsSoFast(); | ||
setIsLoading(false); | ||
|
||
onSignMessageCompleted(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); | ||
}, | ||
}); | ||
}, | ||
}); | ||
|
||
return { isLoading, signMessage }; | ||
} |
32 changes: 32 additions & 0 deletions
32
src/app/pages/rpc-sign-stacks-message/rpc-sign-stacks-message.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,32 @@ | ||
import { isSignableMessageType } from '@shared/signature/signature-types'; | ||
|
||
import { StacksMessageSigning } from '@app/features/stacks-message-signer/stacks-message-signing'; | ||
|
||
import { | ||
useRpcSignStacksMessage, | ||
useRpcSignStacksMessageParams, | ||
useRpcStacksMessagePayload, | ||
} from './use-rpc-sign-stacks-message'; | ||
|
||
export function RpcStacksMessageSigning() { | ||
const { requestId, messageType, tabId, origin } = useRpcSignStacksMessageParams(); | ||
const { isLoading, signMessage, onCancelMessageSigning } = useRpcSignStacksMessage(); | ||
const payload = useRpcStacksMessagePayload(); | ||
|
||
if (!requestId || !tabId) return null; | ||
if (!isSignableMessageType(messageType)) return null; | ||
if (!origin) return null; | ||
if (!payload) return null; | ||
|
||
return ( | ||
<StacksMessageSigning | ||
payload={payload} | ||
isLoading={isLoading} | ||
onSignMessage={signMessage} | ||
onCancelMessageSigning={onCancelMessageSigning} | ||
messageType={messageType} | ||
tabId={tabId} | ||
origin={origin} | ||
/> | ||
); | ||
} |
Oops, something went wrong.