Skip to content

Commit

Permalink
Merge pull request #460 from Concordium/BRO-13-payload-decoding-in-th…
Browse files Browse the repository at this point in the history
…e-browser-wallet-of-the-cis3-standard

[BRO-13] Payload decoding in the browser wallet of the CIS3 standard
  • Loading branch information
Ivan-Mahda authored Apr 23, 2024
2 parents b1adbad + 5abf799 commit f30327a
Show file tree
Hide file tree
Showing 14 changed files with 322 additions and 0 deletions.
25 changes: 25 additions & 0 deletions packages/browser-wallet-api-helpers/src/wallet-api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type {
DeployModulePayload,
ConfigureBakerPayload,
ConfigureDelegationPayload,
ContractName,
EntrypointName,
} from '@concordium/web-sdk';
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
import { LaxNumberEnumValue, LaxStringEnumValue } from './util';
Expand Down Expand Up @@ -250,6 +252,29 @@ interface MainWalletApi {
message: string | SignMessageObject
): Promise<AccountTransactionSignature>;

/**
* Sends a message of the CIS3 contract standard, to the Concordium Wallet and awaits the users action. If the user signs the message, this will resolve to the signature.
*
* @param contractAddress the {@link ContractAddress} of the contract
* @param contractName the {@link ContractName} of the contract
* @param entrypointName the {@link EntrypointName} of the contract
* @param nonce the nonce (CIS3 standard) that was part of the message that was signed
* @param expiryTimeSignature RFC 3339 format (e.g. 2030-08-08T05:15:00Z)
* @param accountAddress the address of the account that should sign the message
* @param payloadMessage payload message to be signed, complete CIS3 message will be created from provided parameters. Note that the wallet will prepend some bytes to ensure the message cannot be a transaction. The message should be { @link SignMessageObject }.
*
* @throws if the user rejects signing the message.
*/
signCIS3Message(
contractAddress: ContractAddress.Type,
contractName: ContractName.Type,
entrypointName: EntrypointName.Type,
nonce: bigint | number,
expiryTimeSignature: string,
accountAddress: AccountAddressSource,
payloadMessage: SignMessageObject
): Promise<AccountTransactionSignature>;

/**
* Requests a connection to the Concordium wallet, prompting the user to either accept or reject the request.
* If a connection has already been accepted for the url once the returned promise will resolve without prompting the user.
Expand Down
34 changes: 34 additions & 0 deletions packages/browser-wallet-api/src/wallet-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
SchemaVersion,
ContractAddress,
VerifiablePresentation,
ContractName,
EntrypointName,
} from '@concordium/web-sdk/types';
import { CredentialStatements } from '@concordium/web-sdk/web3-id';
import {
Expand Down Expand Up @@ -79,6 +81,38 @@ class WalletApi extends EventEmitter implements IWalletApi {
return response.result;
}

public async signCIS3Message(
contractAddress: ContractAddress.Type,
contractName: ContractName.Type,
entrypointName: EntrypointName.Type,
nonce: bigint | number,
expiryTimeSignature: string,
accountAddress: AccountAddressSource,
payloadMessage: SignMessageObject
): Promise<AccountTransactionSignature> {
const input = sanitizeSignMessageInput(accountAddress, payloadMessage);
const response = await this.messageHandler.sendMessage<MessageStatusWrapper<AccountTransactionSignature>>(
MessageType.SignCIS3Message,
{
message: input.message,
accountAddress: AccountAddress.toBase58(input.accountAddress),
cis3ContractDetails: {
contractAddress,
contractName,
entrypointName,
nonce,
expiryTimeSignature,
},
}
);

if (!response.success) {
throw new Error(response.message);
}

return response.result;
}

/**
* Requests connection to wallet. Resolves with account address or rejects if rejected in wallet.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/browser-wallet-message-hub/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum MessageType {
ConnectAccounts = 'M_ConnectAccounts',
AddWeb3IdCredential = 'M_AddWeb3IdCredential',
AddWeb3IdCredentialFinish = 'M_AddWeb3IdCredentialFinish',
SignCIS3Message = 'M_SignCIS3Message',
}

/**
Expand Down Expand Up @@ -49,6 +50,7 @@ export enum InternalMessageType {
ImportWeb3IdBackup = 'I_ImportWeb3IdBackup',
AbortRecovery = 'I_AbortRecovery',
OpenFullscreen = 'I_OpenFullscreen',
SignCIS3Message = 'I_SignCIS3Message',
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
8 changes: 8 additions & 0 deletions packages/browser-wallet/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,14 @@ forwardToPopup(
undefined,
withPromptEnd
);
forwardToPopup(
MessageType.SignCIS3Message,
InternalMessageType.SignCIS3Message,
runConditionComposer(runIfAccountIsAllowlisted, ensureMessageWithSchemaParse, withPromptStart()),
appendUrlToPayload,
undefined,
withPromptEnd
);
forwardToPopup(
MessageType.AddTokens,
InternalMessageType.AddTokens,
Expand Down
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/popup/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export const relativeRoutes = {
signMessage: {
path: 'sign-message',
},
signCIS3Message: {
path: 'sign-cis3-message',
},
sendTransaction: {
path: 'send-transaction',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
$message-details-horizontal-padding: rem(20px);

.sign-cis3-message {
&__details {
display: flex;
flex-direction: column;
align-items: center;
background-color: $color-bg;
height: 100%;
border: rem(1px) solid $color-grey;
border-radius: rem(10px);
padding: rem(10px) $message-details-horizontal-padding;

:where(&) h5 {
margin-top: rem(10px);
margin-bottom: 0;
}
}

&__details-text-area textarea {
min-height: 10rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
font-family: $font-family-mono;
padding: rem(10px);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import clsx from 'clsx';
import { stringify } from 'json-bigint';
import { useTranslation } from 'react-i18next';
import { useSetAtom } from 'jotai';
import { Buffer } from 'buffer/';
import {
AccountAddress,
AccountTransactionSignature,
buildBasicAccountSigner,
ContractAddress,
ContractName,
deserializeTypeValue,
EntrypointName,
serializeTypeValue,
signMessage,
} from '@concordium/web-sdk';
import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout';
import { usePrivateKey } from '@popup/shared/utils/account-helpers';
import { displayUrl } from '@popup/shared/utils/string-helpers';
import { TextArea } from '@popup/shared/Form/TextArea';
import ConnectedBox from '@popup/pages/Account/ConnectedBox';
import Button from '@popup/shared/Button';
import { addToastAtom } from '@popup/state';
import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout';
import { SignMessageObject } from '@concordium/browser-wallet-api-helpers';

const SERIALIZATION_HELPER_SCHEMA =
'FAAFAAAAEAAAAGNvbnRyYWN0X2FkZHJlc3MMBQAAAG5vbmNlBQkAAAB0aW1lc3RhbXANCwAAAGVudHJ5X3BvaW50FgEHAAAAcGF5bG9hZBABAg==';

type Props = {
onSubmit(signature: AccountTransactionSignature): void;
onReject(): void;
};

interface Location {
state: {
payload: {
accountAddress: string;
message: SignMessageObject;
url: string;
cis3ContractDetails: Cis3ContractDetailsObject;
};
};
}

type Cis3ContractDetailsObject = {
contractAddress: ContractAddress.Type;
contractName: ContractName.Type;
entrypointName: EntrypointName.Type;
nonce: bigint | number;
expiryTimeSignature: string;
};

async function parseMessage(message: SignMessageObject) {
return stringify(
deserializeTypeValue(Buffer.from(message.data, 'hex'), Buffer.from(message.schema, 'base64')),
undefined,
2
);
}

function serializeMessage(payloadMessage: SignMessageObject, cis3ContractDetails: Cis3ContractDetailsObject) {
const { contractAddress, entrypointName, nonce, expiryTimeSignature } = cis3ContractDetails;
const message = {
contract_address: {
index: Number(contractAddress.index),
subindex: Number(contractAddress.subindex),
},
nonce: Number(nonce),
timestamp: expiryTimeSignature,
entry_point: EntrypointName.toString(entrypointName),
payload: Array.from(Buffer.from(payloadMessage.data, 'hex')),
};

return serializeTypeValue(message, Buffer.from(SERIALIZATION_HELPER_SCHEMA, 'base64'));
}

function MessageDetailsDisplay({
payloadMessage,
cis3ContractDetails,
}: {
payloadMessage: SignMessageObject;
cis3ContractDetails: Cis3ContractDetailsObject;
}) {
const { t } = useTranslation('signCIS3Message');
const { contractAddress, contractName, entrypointName, nonce, expiryTimeSignature } = cis3ContractDetails;
const [parsedMessage, setParsedMessage] = useState<string>('');
const expiry = new Date(expiryTimeSignature).toString();

useEffect(() => {
parseMessage(payloadMessage)
.then((m) => setParsedMessage(m))
.catch(() => setParsedMessage(t('unableToDeserialize')));
}, []);

return (
<div className="m-10 sign-cis3-message__details">
<h5>{t('contractIndex')}:</h5>
<div>
{contractAddress.index.toString()} ({contractAddress.subindex.toString()})
</div>
<h5>{t('receiveName')}:</h5>
<div>
{contractName.value.toString()}.{entrypointName.value.toString()}
</div>
<h5>{t('nonce')}:</h5>
<div>{nonce.toString()}</div>
<h5>{t('expiry')}:</h5>
<div>{expiry}</div>
<h5>{t('parameter')}:</h5>
<TextArea
readOnly
className={clsx('m-b-10 w-full flex-child-fill sign-cis3-message__details-text-area')}
value={parsedMessage}
/>
</div>
);
}

export default function SignCIS3Message({ onSubmit, onReject }: Props) {
const { state } = useLocation() as Location;
const { t } = useTranslation('signCIS3Message');
const { withClose } = useContext(fullscreenPromptContext);
const { accountAddress, url, message, cis3ContractDetails } = state.payload;
const key = usePrivateKey(accountAddress);
const addToast = useSetAtom(addToastAtom);
const onClick = useCallback(async () => {
if (!key) {
throw new Error('Missing key for the chosen address');
}

return signMessage(
AccountAddress.fromBase58(accountAddress),
serializeMessage(message, cis3ContractDetails).buffer,
buildBasicAccountSigner(key)
);
}, [state.payload.message, state.payload.accountAddress, key]);

return (
<ExternalRequestLayout className="p-10">
<ConnectedBox accountAddress={accountAddress} url={new URL(url).origin} />
<div className="h-full flex-column align-center">
<h3 className="m-t-0 text-center">{t('description', { dApp: displayUrl(url) })}</h3>
<p className="m-t-0 text-center">{t('descriptionWithSchema', { dApp: displayUrl(url) })}</p>
<MessageDetailsDisplay payloadMessage={message} cis3ContractDetails={cis3ContractDetails} />
<br />
<div className="flex p-b-10 p-t-10 m-t-auto">
<Button width="narrow" className="m-r-10" onClick={withClose(onReject)}>
{t('reject')}
</Button>
<Button
width="narrow"
onClick={() =>
onClick()
.then(withClose(onSubmit))
.catch((e) => addToast(e.message))
}
>
{t('sign')}
</Button>
</div>
</div>
</ExternalRequestLayout>
);
}
17 changes: 17 additions & 0 deletions packages/browser-wallet/src/popup/pages/SignCIS3Message/i18n/da.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type en from './en';

const t: typeof en = {
description: '{{ dApp }} anmoder om en signatur på følgende besked',
descriptionWithSchema:
'{{ dApp }} har sendt en rå besked og et schema til at oversætte den. Vi har oversat beskeden, men du burde kun underskrive hvis du stoler på {{ dApp }}',
unableToDeserialize: 'Det var ikke muligt at oversætte beskeden',
contractIndex: 'Kontrakt indeks (under indeks)',
receiveName: 'Kontrakt og funktions navn',
parameter: 'Parameter',
nonce: 'Nonce',
expiry: 'Udløber',
sign: 'Signér',
reject: 'Afvis',
};

export default t;
15 changes: 15 additions & 0 deletions packages/browser-wallet/src/popup/pages/SignCIS3Message/i18n/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const t = {
description: '{{ dApp }} requests a signature on a message',
descriptionWithSchema:
"{{ dApp }} has provided the raw message and a schema to render it. We've rendered the message but you should only sign it if you trust {{ dApp }}.",
unableToDeserialize: 'Unable to render message',
contractIndex: 'Contract index (subindex)',
receiveName: 'Contract and function name',
parameter: 'Parameter',
nonce: 'Nonce',
expiry: 'Expiry time',
sign: 'Sign',
reject: 'Reject',
};

export default t;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SignCIS3Message';
18 changes: 18 additions & 0 deletions packages/browser-wallet/src/popup/shell/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import FullscreenPromptLayout from '@popup/page-layouts/FullscreenPromptLayout';
import Account from '@popup/pages/Account';
import Identity from '@popup/pages/Identity';
import SignMessage from '@popup/pages/SignMessage';
import SignCIS3Message from '@popup/pages/SignCIS3Message';
import SendTransaction from '@popup/pages/SendTransaction';
import Setup from '@popup/pages/Setup';
import ConnectionRequest from '@popup/pages/ConnectionRequest';
Expand Down Expand Up @@ -106,6 +107,10 @@ export default function Routes() {
InternalMessageType.SignMessage,
'signMessage'
);
const handleSignCIS3MessageResponse = useMessagePrompt<MessageStatusWrapper<AccountTransactionSignature>>(
InternalMessageType.SignCIS3Message,
'signCIS3Message'
);
const handleAddTokensResponse = useMessagePrompt<MessageStatusWrapper<string[]>>(
InternalMessageType.AddTokens,
'addTokens'
Expand Down Expand Up @@ -155,6 +160,19 @@ export default function Routes() {
/>
}
/>
<Route
path={relativeRoutes.prompt.signCIS3Message.path}
element={
<SignCIS3Message
onSubmit={(signature) =>
handleSignCIS3MessageResponse({ success: true, result: signature })
}
onReject={() =>
handleSignCIS3MessageResponse({ success: false, message: 'Signing was rejected' })
}
/>
}
/>
<Route
path={relativeRoutes.prompt.sendTransaction.path}
element={
Expand Down
Loading

0 comments on commit f30327a

Please sign in to comment.