Skip to content

Commit

Permalink
Merge pull request #28 from CoongCrafts/coong/feature/sign-raw-message
Browse files Browse the repository at this point in the history
Sign raw messages
  • Loading branch information
sinzii authored Apr 8, 2023
2 parents efb8f5b + 3e7e0be commit 8a814c8
Show file tree
Hide file tree
Showing 17 changed files with 678 additions and 362 deletions.
1 change: 1 addition & 0 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@coong/utils": "^0.0.20",
"@polkadot/networks": "^10.4.1",
"@polkadot/types": "^9.14.1",
"@polkadot/util": "^10.4.1",
"@polkadot/util-crypto": "^10.4.1",
"rxjs": "^7.8.0"
},
Expand Down
29 changes: 28 additions & 1 deletion packages/base/src/requests/WalletState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { TypeRegistry } from '@polkadot/types';
import { SignerPayloadJSON } from '@polkadot/types/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { u8aToHex, u8aWrapBytes } from '@polkadot/util';
import { encodeAddress } from '@polkadot/util-crypto';
import { KeypairType } from '@polkadot/util-crypto/types';
import Keyring from '@coong/keyring';
Expand Down Expand Up @@ -159,6 +160,8 @@ export default class WalletState {

const registry = new TypeRegistry();
registry.setSignedExtensions(payloadJSON.signedExtensions);

// https://github.com/polkadot-js/extension/blob/master/packages/extension-base/src/background/RequestExtrinsicSign.ts#L18-L22
const payload = registry.createType('ExtrinsicPayload', payloadJSON, { version: payloadJSON.version });
const result = payload.sign(pair);

Expand All @@ -170,7 +173,31 @@ export default class WalletState {

cancelSignExtrinsic() {
const currentMessage = this.getCurrentRequestMessage('tab/signExtrinsic');
currentMessage.reject(new StandardCoongError('Cancelled'));
}

async signRawMessage(password: string) {
await this.#keyring.verifyPassword(password);

const currentMessage = this.getCurrentRequestMessage('tab/signRaw');

const { id, request, resolve } = currentMessage;
const payloadJSON = request.body as SignerPayloadRaw;

const pair = this.#keyring.getSigningPair(payloadJSON.address);
pair.unlock(password);

// https://github.com/polkadot-js/extension/blob/master/packages/extension-base/src/background/RequestBytesSign.ts#L20-L27
const signature = u8aToHex(pair.sign(u8aWrapBytes(payloadJSON.data)));

resolve({
id,
signature,
});
}

cancelSignRawMessage() {
const currentMessage = this.getCurrentRequestMessage('tab/signRaw');
currentMessage.reject(new StandardCoongError('Cancelled'));
}

Expand Down
18 changes: 16 additions & 2 deletions packages/ui/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"Address copied!": "",
"Address format": "",
"An application, self-identifying as request app name is requesting access your wallet from origin.": "An application, self-identifying as <strong>{{appName}}</strong> is requesting access your wallet from <strong>{{origin}}</strong>.",
"Approve Transaction": "",
"Auto-lock wallet after": "",
"Back": "",
"Backup secret recovery phrase": "",
Expand Down Expand Up @@ -69,10 +70,14 @@
"Set up your Coong wallet now": "",
"Settings": "",
"Setup your Coong wallet now to connect": "",
"Sign": "",
"Sign Message": "",
"Sign Message Request": "",
"System": "",
"Theme Mode": "",
"This page should be loaded inside an iframe!": "",
"This page should not be open directly!": "",
"Transaction Approval Request": "",
"Type again your chosen password to ensure you remember it.": "",
"UnknownRequest": "Unknown request",
"UnknownRequestOrigin": "Unknown request origin",
Expand All @@ -87,7 +92,16 @@
"Welcome to Coong Wallet!": "",
"Write down the below 12 words and keep it in a safe place.": "",
"You are about to reveal the secret recovery phrase which give access to your accounts and funds.": "",
"You are approving a transaction with account": "",
"You are signing a message with account": "",
"Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password": "Your password will be used to <strong>encrypt accounts as well as unlock the wallet</strong>, make sure to pick a <strong>strong & easy-to-remember</strong> password.",
"Your wallet password": "",
"account(s) selected": ""
}
"account(s) selected": "",
"bytes": "",
"from": "",
"genesis": "",
"life time": "",
"method data": "",
"nonce": "",
"version": ""
}
5 changes: 4 additions & 1 deletion packages/ui/src/components/pages/Request/RequestContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, { FC } from 'react';
import { CoongError, ErrorCode } from '@coong/utils';
import { CircularProgress } from '@mui/material';
import RequestAccess from 'components/pages/Request/RequestAccess';
import RequestTransactionApproval from 'components/pages/Request/RequestTransactionApproval';
import RequestSignRawMessage from 'components/pages/Request/RequestSigning/RequestSignRawMessage';
import RequestTransactionApproval from 'components/pages/Request/RequestSigning/RequestTransactionApproval';
import useCurrentRequestMessage from 'hooks/messages/useCurrentRequestMessage';
import { Props } from 'types';

Expand All @@ -23,6 +24,8 @@ const RequestContent: FC<Props> = () => {
return <RequestAccess message={message!} />;
} else if (requestName === 'tab/signExtrinsic') {
return <RequestTransactionApproval message={message!} />;
} else if (requestName === 'tab/signRaw') {
return <RequestSignRawMessage message={message!} />;
}

throw new CoongError(ErrorCode.UnknownRequest);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import { Props } from 'types';

export enum ValueStyle {
TEXT_BOLD,
BOX,
}

export interface DetailRowProps extends Props {
name: string;
value: any;
breakWord?: boolean;
style?: ValueStyle;
}

const DetailRow: FC<DetailRowProps> = ({ name, value, breakWord = false, style = ValueStyle.TEXT_BOLD }) => {
const { t } = useTranslation();
const renderValue = () => {
switch (style) {
case ValueStyle.BOX:
return (
<div className='py-2 px-4 bg-black/10 dark:bg-white/15 border border-black/10 dark:border-white/15 w-full'>
{value}
</div>
);
case ValueStyle.TEXT_BOLD:
default:
return <strong className={clsx({ 'break-all': breakWord })}>{value}</strong>;
}
};

return (
<div className='flex items-start mb-2 gap-2' data-testid={`row-${name.replace(' ', '-')}`}>
<div className='text-gray-500 dark:text-gray-200 min-w-[80px] text-right'>{t<string>(name)}: </div>
{renderValue()}
</div>
);
};
export default DetailRow;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FC } from 'react';
import DetailRow, { DetailRowProps } from 'components/pages/Request/RequestSigning/DetailRow';
import { Props } from 'types';

interface RequestDetails extends Props {
rows: DetailRowProps[];
}

const RequestDetails: FC<RequestDetails> = ({ className = '', rows }) => {
return (
<div className={`${className}`}>
{rows.map((row) => (
<DetailRow key={row.name} {...row} />
))}
</div>
);
};

export default RequestDetails;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import AccountCard from 'components/pages/Accounts/AccountCard';
import RequestDetails from 'components/pages/Request/RequestSigning/RequestDetails';
import SignArea from 'components/pages/Request/RequestSigning/SignArea';
import { useRawMessageDetails } from 'components/pages/Request/RequestSigning/hooks/useRawMessageDetails';
import useTargetAccount from 'components/pages/Request/RequestSigning/hooks/useTargetAccount';
import { RequestProps } from 'components/pages/Request/types';
import { useWalletState } from 'providers/WalletStateProvider';

const RequestSignRawMessage: FC<RequestProps> = ({ className = '', message }) => {
const { t } = useTranslation();
const { walletState } = useWalletState();
const targetAccount = useTargetAccount(message);
const detailRows = useRawMessageDetails(message);

const doSignMessage = async (password: string) => {
await walletState.signRawMessage(password);
};

const cancelRequest = () => {
walletState.cancelSignRawMessage();
};

return (
<div className={className}>
<h2 className='text-center'>{t<string>('Sign Message Request')}</h2>
<p className='mb-2'>{t<string>('You are signing a message with account')}</p>
{targetAccount && <AccountCard account={targetAccount} />}
<RequestDetails rows={detailRows} />
<SignArea onSign={doSignMessage} onCancel={cancelRequest} signButtonLabel={'Sign Message'} />
</div>
);
};

export default RequestSignRawMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import AccountCard from 'components/pages/Accounts/AccountCard';
import RequestDetails from 'components/pages/Request/RequestSigning/RequestDetails';
import SignArea from 'components/pages/Request/RequestSigning/SignArea';
import useTargetAccount from 'components/pages/Request/RequestSigning/hooks/useTargetAccount';
import useTransactionDetails from 'components/pages/Request/RequestSigning/hooks/useTransactionDetails';
import { RequestProps } from 'components/pages/Request/types';
import { useWalletState } from 'providers/WalletStateProvider';

const RequestTransactionApproval: FC<RequestProps> = ({ className, message }) => {
const { t } = useTranslation();
const { walletState } = useWalletState();
const targetAccount = useTargetAccount(message);
const detailRows = useTransactionDetails(message);

const approveTransaction = async (password: string) => {
await walletState.approveSignExtrinsic(password);
};

const cancelRequest = () => {
walletState.cancelSignExtrinsic();
};

return (
<div className={className}>
<h2 className='text-center'>{t<string>('Transaction Approval Request')}</h2>
<p className='mb-2'>{t<string>('You are approving a transaction with account')}</p>
{targetAccount && <AccountCard account={targetAccount} />}
<RequestDetails className='my-4' rows={detailRows} />
<SignArea onSign={approveTransaction} onCancel={cancelRequest} signButtonLabel={'Approve Transaction'} />
</div>
);
};

export default RequestTransactionApproval;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FC, FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Form } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useToggle } from 'react-use';
import { Button, TextField } from '@mui/material';
import { Props } from 'types';

interface SignAreaProps extends Props {
onSign: (password: string) => void;
onCancel: () => void;
cancelButtonLabel?: string;
signButtonLabel?: string;
}

const SignArea: FC<SignAreaProps> = ({ onSign, onCancel, cancelButtonLabel = 'Cancel', signButtonLabel = 'Sign' }) => {
const { t } = useTranslation();
const [password, setPassword] = useState<string>('');
const [loading, toggleLoading] = useToggle(false);

const doSign = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
toggleLoading(true);

// signing & accounts decryption are synchronous operations
// and might take some time to do
// so we delay it a short amount of time to make sure the UI could be updated (disable button, ...)
// before the signing process begin
// TODO: Moving CPU-intensive operations to worker
setTimeout(async () => {
try {
await onSign(password);
} catch (e: any) {
toggleLoading(false);
toast.error(t<string>(e.message));
}
}, 200);
};

return (
<Form className='mt-8' onSubmit={doSign}>
<TextField
label={t<string>('Wallet password')}
size='medium'
type='password'
fullWidth
autoFocus
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
<div className='flex mt-4 gap-4'>
<Button size='large' variant='text' className='xs:w-2/5' color='warning' onClick={onCancel}>
{t<string>(cancelButtonLabel)}
</Button>
<Button size='large' className='w-full xs:w-3/5' disabled={!password || loading} type='submit'>
{t<string>(signButtonLabel)}
</Button>
</div>
</Form>
);
};

export default SignArea;
Loading

0 comments on commit 8a814c8

Please sign in to comment.