Skip to content

Commit

Permalink
feat: add rpc method for signing stacks messages
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarkhanzadian committed Dec 6, 2023
1 parent 6b7ce6a commit e77a8d8
Show file tree
Hide file tree
Showing 26 changed files with 704 additions and 205 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Disclaimer } from '@app/components/disclaimer';

interface DisclaimerProps {
appName?: string;
appName?: string | null;
}
export function StacksMessageSigningDisclaimer({ appName }: DisclaimerProps) {
return (
Expand Down
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} />
</>
);
}
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 src/app/features/stacks-message-signer/stacks-message-signing.tsx
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>
);
}
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 src/app/features/stacks-message-signer/use-sign-stacks-message.ts
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 src/app/pages/rpc-sign-stacks-message/rpc-sign-stacks-message.tsx
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}
/>
);
}
Loading

0 comments on commit e77a8d8

Please sign in to comment.