From e75aaca44830a4c2bba2d578d1c5448356287236 Mon Sep 17 00:00:00 2001 From: Ivan-Mahda Date: Fri, 5 Apr 2024 01:35:01 +0300 Subject: [PATCH 01/11] [BRO-13] Payload decoding in the browser wallet of the CIS3 standard Proposal on how to decode the payload for the CIS3 contract --- .../popup/pages/SignMessage/SignMessage.tsx | 81 +++++++++++++++---- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx index b48f6992..ffcfb2b8 100644 --- a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx +++ b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx @@ -1,8 +1,8 @@ -import React, { useContext, useCallback, useState, useMemo } from 'react'; +import React, { useContext, useCallback, useState, useMemo, useEffect } from 'react'; import { Buffer } from 'buffer/'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { useTranslation } from 'react-i18next'; -import { useSetAtom } from 'jotai'; +import { TFunction, useTranslation } from 'react-i18next'; +import { useAtomValue, useSetAtom } from 'jotai'; import { useLocation } from 'react-router-dom'; import { signMessage, @@ -10,6 +10,12 @@ import { AccountTransactionSignature, AccountAddress, deserializeTypeValue, + ContractAddress, + ModuleReference, + getUpdateContractParameterSchema, + ContractName, + EntrypointName, + ConcordiumGRPCClient, } from '@concordium/web-sdk'; import { usePrivateKey } from '@popup/shared/utils/account-helpers'; import { displayUrl } from '@popup/shared/utils/string-helpers'; @@ -21,6 +27,7 @@ import ExternalRequestLayout from '@popup/page-layouts/ExternalRequestLayout'; import TabBar from '@popup/shared/TabBar'; import clsx from 'clsx'; import { stringify } from 'json-bigint'; +import { grpcClientAtom } from '@popup/store/settings'; type Props = { onSubmit(signature: AccountTransactionSignature): void; @@ -42,23 +49,69 @@ type MessageObject = { data: string; }; +async function parseMessage( + message: MessageObject, + client: ConcordiumGRPCClient, + t: TFunction, + setParsedMessage: React.Dispatch> +) { + try { + const deserializedMessage = deserializeTypeValue( + Buffer.from(message.data, 'hex'), + Buffer.from(message.schema, 'base64') + ); + + const instanceInfo = await client.getInstanceInfo( + ContractAddress.create( + deserializedMessage.contract_address.index, + deserializedMessage.contract_address.subindex + ) + ); + + // Need better way to define is contract CIS3. Something like function confirmCIS2Contract ? + const isCIS3 = instanceInfo.name.value.includes('cis3'); + + // Contract name does not match value stored in instanceInfo.name.value -> init_cis3_nft + // Used contract 6372 + const CONTRACT_NAME = 'cis3_nft'; + + if (isCIS3) { + const schema = await client.getEmbeddedSchema( + ModuleReference.fromHexString(instanceInfo.sourceModule.moduleRef) + ); + + const updateContractParameterSchema = getUpdateContractParameterSchema( + schema, + ContractName.fromString(CONTRACT_NAME), + EntrypointName.fromString(deserializedMessage.entry_point), + instanceInfo.version + ); + + deserializedMessage.payload = deserializeTypeValue( + BigInt64Array.from(deserializedMessage.payload).buffer, + updateContractParameterSchema.buffer + ); + } + setParsedMessage(stringify(deserializedMessage, undefined, 2)); + } catch (e) { + setParsedMessage(`${t('unableToDeserialize')}`); + } +} + function BinaryDisplay({ message, url }: { message: MessageObject; url: string }) { const { t } = useTranslation('signMessage'); + const client = useAtomValue(grpcClientAtom); const [displayDeserialized, setDisplayDeserialized] = useState(true); + const [parsedMessage, setParsedMessage] = useState(''); - const parsedMessage = useMemo(() => { - try { - return stringify( - deserializeTypeValue(Buffer.from(message.data, 'hex'), Buffer.from(message.schema, 'base64')), - undefined, - 2 - ); - } catch (e) { - return t('unableToDeserialize'); - } + useEffect(() => { + parseMessage(message, client, t, setParsedMessage); }, []); - const display = useMemo(() => (displayDeserialized ? parsedMessage : message.data), [displayDeserialized]); + const display = useMemo( + () => (displayDeserialized ? parsedMessage : message.data), + [displayDeserialized, parsedMessage] + ); return ( <> From 768ffefe0d0c6ffd870334a352fdd586f92dcdf3 Mon Sep 17 00:00:00 2001 From: Ivan-Mahda Date: Fri, 5 Apr 2024 02:19:50 +0300 Subject: [PATCH 02/11] [BRO-13] Payload decoding in the browser wallet of the CIS3 standard Fix typescript types errors --- .../popup/pages/SignMessage/SignMessage.tsx | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx index ffcfb2b8..0d5354ad 100644 --- a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx +++ b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx @@ -1,7 +1,7 @@ import React, { useContext, useCallback, useState, useMemo, useEffect } from 'react'; import { Buffer } from 'buffer/'; import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout'; -import { TFunction, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { useAtomValue, useSetAtom } from 'jotai'; import { useLocation } from 'react-router-dom'; import { @@ -49,53 +49,57 @@ type MessageObject = { data: string; }; +type DeserializedMessageObject = { + contract_address: { + index: number; + subindex: number; + }; + entry_point: string; + payload: bigint[] | []; +}; + async function parseMessage( message: MessageObject, client: ConcordiumGRPCClient, - t: TFunction, setParsedMessage: React.Dispatch> ) { - try { - const deserializedMessage = deserializeTypeValue( - Buffer.from(message.data, 'hex'), - Buffer.from(message.schema, 'base64') + const deserializedMessage = deserializeTypeValue( + Buffer.from(message.data, 'hex'), + Buffer.from(message.schema, 'base64') + ) as DeserializedMessageObject; + + const instanceInfo = await client.getInstanceInfo( + ContractAddress.create( + deserializedMessage.contract_address.index, + deserializedMessage.contract_address.subindex + ) + ); + + // Need better way to define is contract CIS3. Something like function confirmCIS2Contract ? + const isCIS3 = instanceInfo.name.value.includes('cis3'); + + // Contract name does not match value stored in instanceInfo.name.value -> init_cis3_nft + // Used contract 6372 + const CONTRACT_NAME = 'cis3_nft'; + + if (isCIS3) { + const schema = await client.getEmbeddedSchema( + ModuleReference.fromHexString(instanceInfo.sourceModule.moduleRef) ); - const instanceInfo = await client.getInstanceInfo( - ContractAddress.create( - deserializedMessage.contract_address.index, - deserializedMessage.contract_address.subindex - ) + const updateContractParameterSchema = getUpdateContractParameterSchema( + schema, + ContractName.fromString(CONTRACT_NAME), + EntrypointName.fromString(deserializedMessage.entry_point), + instanceInfo.version ); - // Need better way to define is contract CIS3. Something like function confirmCIS2Contract ? - const isCIS3 = instanceInfo.name.value.includes('cis3'); - - // Contract name does not match value stored in instanceInfo.name.value -> init_cis3_nft - // Used contract 6372 - const CONTRACT_NAME = 'cis3_nft'; - - if (isCIS3) { - const schema = await client.getEmbeddedSchema( - ModuleReference.fromHexString(instanceInfo.sourceModule.moduleRef) - ); - - const updateContractParameterSchema = getUpdateContractParameterSchema( - schema, - ContractName.fromString(CONTRACT_NAME), - EntrypointName.fromString(deserializedMessage.entry_point), - instanceInfo.version - ); - - deserializedMessage.payload = deserializeTypeValue( - BigInt64Array.from(deserializedMessage.payload).buffer, - updateContractParameterSchema.buffer - ); - } - setParsedMessage(stringify(deserializedMessage, undefined, 2)); - } catch (e) { - setParsedMessage(`${t('unableToDeserialize')}`); + deserializedMessage.payload = deserializeTypeValue( + BigInt64Array.from(deserializedMessage.payload).buffer, + updateContractParameterSchema.buffer + ) as []; } + setParsedMessage(stringify(deserializedMessage, undefined, 2)); } function BinaryDisplay({ message, url }: { message: MessageObject; url: string }) { @@ -105,7 +109,11 @@ function BinaryDisplay({ message, url }: { message: MessageObject; url: string } const [parsedMessage, setParsedMessage] = useState(''); useEffect(() => { - parseMessage(message, client, t, setParsedMessage); + try { + parseMessage(message, client, setParsedMessage); + } catch (e) { + setParsedMessage(t('unableToDeserialize')); + } }, []); const display = useMemo( From be17e83a349b6de8b973e0be23654eca6be55672 Mon Sep 17 00:00:00 2001 From: Ivan-Mahda Date: Tue, 9 Apr 2024 20:17:30 +0300 Subject: [PATCH 03/11] [BRO-13] Payload decoding in the browser wallet of the CIS3 standard Created signCIS3Message method for explicit CIS3 sign --- .../src/wallet-api-types.ts | 19 +++++++ packages/browser-wallet-api/src/wallet-api.ts | 30 +++++++++++ .../popup/pages/SignMessage/SignMessage.tsx | 52 ++++++++++--------- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 88936b1d..ea66199b 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -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'; @@ -250,6 +252,23 @@ interface MainWalletApi { message: string | SignMessageObject ): Promise; + /** + * 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. + * Note that if the user rejects signing the message, this will throw an error. + * @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 accountAddress the address of the account that should sign the message + * @param message message to be signed. Note that the wallet will prepend some bytes to ensure the message cannot be a transaction. The message should either be a utf8 string or { @link SignMessageObject }. + */ + signCIS3Message( + contractAddress: ContractAddress.Type, + contractName: ContractName.Type, + entrypointName: EntrypointName.Type, + accountAddress: AccountAddressSource, + message: string | SignMessageObject + ): Promise; + /** * 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. diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 18755d6d..2ac91b41 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -12,6 +12,8 @@ import { SchemaVersion, ContractAddress, VerifiablePresentation, + ContractName, + EntrypointName, } from '@concordium/web-sdk/types'; import { CredentialStatements } from '@concordium/web-sdk/web3-id'; import { @@ -79,6 +81,34 @@ class WalletApi extends EventEmitter implements IWalletApi { return response.result; } + public async signCIS3Message( + contractAddress: ContractAddress.Type, + contractName: ContractName.Type, + entrypointName: EntrypointName.Type, + accountAddress: AccountAddressSource, + message: string | SignMessageObject + ): Promise { + const input = sanitizeSignMessageInput(accountAddress, message); + const response = await this.messageHandler.sendMessage>( + MessageType.SignMessage, + { + message: input.message, + accountAddress: AccountAddress.toBase58(input.accountAddress), + cis3ContractDetails: { + contractAddress, + contractName, + entrypointName, + }, + } + ); + + 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. */ diff --git a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx index 0d5354ad..496c3a78 100644 --- a/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx +++ b/packages/browser-wallet/src/popup/pages/SignMessage/SignMessage.tsx @@ -40,6 +40,7 @@ interface Location { accountAddress: string; message: string | MessageObject; url: string; + cis3ContractDetails: Cis3ContractDetailsObject | undefined; }; }; } @@ -49,18 +50,20 @@ type MessageObject = { data: string; }; +type Cis3ContractDetailsObject = { + contractAddress: ContractAddress.Type; + contractName: ContractName.Type; + entrypointName: EntrypointName.Type; +}; + type DeserializedMessageObject = { - contract_address: { - index: number; - subindex: number; - }; - entry_point: string; payload: bigint[] | []; }; async function parseMessage( message: MessageObject, client: ConcordiumGRPCClient, + cis3ContractDetails: Cis3ContractDetailsObject | undefined, setParsedMessage: React.Dispatch> ) { const deserializedMessage = deserializeTypeValue( @@ -68,29 +71,18 @@ async function parseMessage( Buffer.from(message.schema, 'base64') ) as DeserializedMessageObject; - const instanceInfo = await client.getInstanceInfo( - ContractAddress.create( - deserializedMessage.contract_address.index, - deserializedMessage.contract_address.subindex - ) - ); - - // Need better way to define is contract CIS3. Something like function confirmCIS2Contract ? - const isCIS3 = instanceInfo.name.value.includes('cis3'); - - // Contract name does not match value stored in instanceInfo.name.value -> init_cis3_nft - // Used contract 6372 - const CONTRACT_NAME = 'cis3_nft'; + if (cis3ContractDetails) { + const { contractAddress, contractName, entrypointName } = cis3ContractDetails; + const instanceInfo = await client.getInstanceInfo(contractAddress); - if (isCIS3) { const schema = await client.getEmbeddedSchema( ModuleReference.fromHexString(instanceInfo.sourceModule.moduleRef) ); const updateContractParameterSchema = getUpdateContractParameterSchema( schema, - ContractName.fromString(CONTRACT_NAME), - EntrypointName.fromString(deserializedMessage.entry_point), + contractName, + entrypointName, instanceInfo.version ); @@ -102,7 +94,15 @@ async function parseMessage( setParsedMessage(stringify(deserializedMessage, undefined, 2)); } -function BinaryDisplay({ message, url }: { message: MessageObject; url: string }) { +function BinaryDisplay({ + message, + url, + cis3ContractDetails, +}: { + message: MessageObject; + url: string; + cis3ContractDetails: Cis3ContractDetailsObject | undefined; +}) { const { t } = useTranslation('signMessage'); const client = useAtomValue(grpcClientAtom); const [displayDeserialized, setDisplayDeserialized] = useState(true); @@ -110,7 +110,7 @@ function BinaryDisplay({ message, url }: { message: MessageObject; url: string } useEffect(() => { try { - parseMessage(message, client, setParsedMessage); + parseMessage(message, client, cis3ContractDetails, setParsedMessage); } catch (e) { setParsedMessage(t('unableToDeserialize')); } @@ -158,7 +158,7 @@ export default function SignMessage({ onSubmit, onReject }: Props) { const { accountAddress, url } = state.payload; const key = usePrivateKey(accountAddress); const addToast = useSetAtom(addToastAtom); - const { message } = state.payload; + const { message, cis3ContractDetails } = state.payload; const messageIsAString = typeof message === 'string'; const onClick = useCallback(async () => { @@ -179,7 +179,9 @@ export default function SignMessage({ onSubmit, onReject }: Props) {

{t('description', { dApp: displayUrl(url) })}

{messageIsAString &&