diff --git a/front-end-tools/CHANGELOG.md b/front-end-tools/CHANGELOG.md index 0927b048..4a00204e 100644 --- a/front-end-tools/CHANGELOG.md +++ b/front-end-tools/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased changes +## 3.0.0 + +- Add reading from smart contract and writing to smart contract feature + ## 2.2.0 - Add warning box if module references in step 1 and step 2 are not the same. Add warning box if embedded schema indicates an input parameter but no input parameter is provided. diff --git a/front-end-tools/package.json b/front-end-tools/package.json index 84d513fb..4d81c71e 100644 --- a/front-end-tools/package.json +++ b/front-end-tools/package.json @@ -1,7 +1,7 @@ { "name": "front-end-tools", "packageManager": "yarn@3.2.0", - "version": "2.2.0", + "version": "3.0.0", "license": "Apache-2.0", "engines": { "node": ">=16.x" @@ -15,6 +15,8 @@ "react": "^18.1.0", "react-bootstrap": "^2.7.4", "react-dom": "^18.1.0", + "react-hook-form": "^7.47.0", + "react-select": "^5.7.7", "react-switch": "^7.0.0" }, "devDependencies": { diff --git a/front-end-tools/src/Main.tsx b/front-end-tools/src/Main.tsx index 894643fd..3d7f88fa 100644 --- a/front-end-tools/src/Main.tsx +++ b/front-end-tools/src/Main.tsx @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -import React, { useEffect, useState, ChangeEvent, PropsWithChildren, useCallback, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; + import { WalletConnectionProps, useConnection, @@ -7,436 +7,94 @@ import { useGrpcClient, TESTNET, MAINNET, + useWalletConnectorSelector, } from '@concordium/react-components'; -import { Buffer } from 'buffer'; -import { - AccountAddress, - ModuleReference, - TransactionKindString, - TransactionSummaryType, - displayTypeSchemaTemplate, - sha256, - toBuffer, - getInitContractParameterSchema, -} from '@concordium/web-sdk'; -import { WalletConnectionTypeButton } from './WalletConnectorTypeButton'; - -import { initialize, deploy } from './writing_to_blockchain'; - -import { BROWSER_WALLET, REFRESH_INTERVAL, EXAMPLE_ARRAYS, EXAMPLE_JSON_OBJECT } from './constants'; -type TestBoxProps = PropsWithChildren<{ - header: string; -}>; +import { Alert } from 'react-bootstrap'; +import DeployComponent from './components/DeployComponent'; +import ReadComponent from './components/ReadComponent'; +import UpdateComponent from './components/UpdateComponent'; +import InitComponent from './components/InitComponent'; +import { AccountLink } from './components/CCDScanLinks'; +import { getAccountInfo } from './reading_from_blockchain'; -function TestBox({ header, children }: TestBoxProps) { - return ( -
- {header} -
{children}
-
-
- ); -} +import { BROWSER_WALLET, REFRESH_INTERVAL } from './constants'; interface ConnectionProps { walletConnectionProps: WalletConnectionProps; isTestnet: boolean; } +/** The main component manages the connection to the browser wallet and + * combines the rest of the components (DeployComponent, InitComponent, ReadComponent, and UpdateComponent) to form the page. + * The connected account address, and its balance are displayed at the top. Links for further reading are displayed at the bottom. + */ export default function Main(props: ConnectionProps) { + // Network state const { walletConnectionProps, isTestnet } = props; - const { activeConnectorType, activeConnector, activeConnectorError, connectedAccounts, genesisHashes } = walletConnectionProps; - const { connection, setConnection, account } = useConnection(connectedAccounts, genesisHashes); - const { connect, isConnecting, connectError } = useConnect(activeConnector, setConnection); - const client = useGrpcClient(isTestnet ? TESTNET : MAINNET); + const { isConnected, select } = useWalletConnectorSelector(BROWSER_WALLET, connection, { + ...walletConnectionProps, + }); + const { connect, connectError } = useConnect(activeConnector, setConnection); - const [viewErrorAccountInfo, setViewErrorAccountInfo] = useState(''); - const [viewErrorModuleReference, setViewErrorModuleReference] = useState(''); - const [transactionErrorDeploy, setTransactionErrorDeploy] = useState(''); - const [transactionErrorInit, setTransactionErrorInit] = useState(''); - const [uploadError, setUploadError] = useState(''); - const [uploadError2, setUploadError2] = useState(''); - const [parsingError, setParsingError] = useState(''); - const [smartContractIndexError, setSmartContractIndexError] = useState(''); - const [moduleReferenceError, setModuleReferenceError] = useState(''); - const [moduleReferenceLengthError, setModuleReferenceLengthError] = useState(''); - const [schemaError, setSchemaError] = useState(''); + const client = useGrpcClient(isTestnet ? TESTNET : MAINNET); + // Account state + const [viewErrorAccountInfo, setViewErrorAccountInfo] = useState(undefined); const [accountExistsOnNetwork, setAccountExistsOnNetwork] = useState(true); - const [moduleReferenceCalculated, setModuleReferenceCalculated] = useState(''); - const [isModuleReferenceAlreadyDeployedStep1, setIsModuleReferenceAlreadyDeployedStep1] = useState(false); - const [isModuleReferenceAlreadyDeployedStep2, setIsModuleReferenceAlreadyDeployedStep2] = useState(false); - const [moduleReferenceDeployed, setModuleReferenceDeployed] = useState(''); - - const [shouldWarnDifferenceModuleReferences, setShouldWarnDifferenceModuleReferences] = useState(false); - const [shouldWarnInputParameterInSchemaIgnored, setShouldWarnInputParameterInSchemaIgnored] = useState(false); + const [accountBalance, setAccountBalance] = useState(undefined); - const [txHashDeploy, setTxHashDeploy] = useState(''); - const [txHashInit, setTxHashInit] = useState(''); - - const [accountBalance, setAccountBalance] = useState(''); - const [inputParameter, setInputParameter] = useState(''); - const [contractName, setContractName] = useState(''); - const [moduleReference, setModuleReference] = useState(''); - const [cCDAmount, setCCDAmount] = useState(''); - const [base64Module, setBase64Module] = useState(''); - const [uploadedModuleSchemaBase64, setUploadedModuleSchemaBase64] = useState(''); - const [dropDown, setDropDown] = useState('number'); - const [smartContractIndex, setSmartContractIndex] = useState(''); - const [maxContractExecutionEnergy, setMaxContractExecutionEnergy] = useState(''); - const [useModuleFromStep1, setUseModuleFromStep1] = useState(false); + // Shared state between deploy step and init step + const [moduleReferenceCalculated, setModuleReferenceCalculated] = useState(undefined); + const [moduleReferenceDeployed, setModuleReferenceDeployed] = useState(undefined); const [contracts, setContracts] = useState([]); - const [displayContracts, setDisplayContracts] = useState([]); - const [inputParameterTemplate, setInputParameterTemplate] = useState(''); - const [embeddedModuleSchemaBase64, setEmbeddedModuleSchemaBase64] = useState(''); - - const [isWaitingForTransaction, setWaitingForUser] = useState(false); - const [hasInputParameter, setHasInputParameter] = useState(false); - const [isPayable, setIsPayable] = useState(false); - - const moduleFileRef = useRef(null); - const inputParameterDropDownRef = useRef(null); - const contractNameDropDownRef = useRef(null); - const schemaFileRef = useRef(null); - const inputParameterTextAreaRef = useRef(null); - const useModuleReferenceFromStep1Ref = useRef(null); - const moduleReferenceRef = useRef(null); - const inputParameterFieldRef = useRef(null); - - function arraysEqual(a: Uint8Array, b: Uint8Array) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length !== b.length) return false; - - for (let i = 0; i < a.length; i += 1) { - if (a[i] !== b[i]) return false; - } - return true; - } - - function getObjectExample(template: string) { - return template !== '' - ? JSON.stringify(JSON.parse(template), undefined, 2) - : JSON.stringify(EXAMPLE_JSON_OBJECT, undefined, 2); - } - - function getArrayExample(template: string) { - return template !== '' ? JSON.stringify(JSON.parse(template), undefined, 2) : EXAMPLE_ARRAYS; - } - - const changeModuleReferenceHandler = useCallback((event: ChangeEvent) => { - setTransactionErrorInit(''); - setModuleReferenceLengthError(''); - setModuleReference(''); - - const target = event.target as HTMLTextAreaElement; - - const moduleReferenceInput = target.value; - - if (moduleReferenceInput.length !== 64) { - setModuleReferenceLengthError('Module reference has to be of length 64'); - } else { - setModuleReference(target.value); - } - }, []); - - const changeInputParameterDropDownHandler = useCallback(() => { - setParsingError(''); - setInputParameter(''); - setTransactionErrorInit(''); - const e = inputParameterDropDownRef.current as unknown as HTMLSelectElement; - const sel = e.selectedIndex; - const { value } = e.options[sel]; - setDropDown(value); - }, []); - - const changeSmarContractDropDownHandler = useCallback(() => { - setTransactionErrorInit(''); - const e = contractNameDropDownRef.current as unknown as HTMLSelectElement; - const sel = e.selectedIndex; - const { value } = e.options[sel]; - setContractName(value); - }, []); - - const changeCCDAmountHandler = useCallback((event: ChangeEvent) => { - const target = event.target as HTMLTextAreaElement; - setCCDAmount(target.value); - }, []); - - const changeMaxExecutionEnergyHandler = useCallback((event: ChangeEvent) => { - const target = event.target as HTMLTextAreaElement; - setMaxContractExecutionEnergy(target.value); - }, []); - - const changeContractNameHandler = useCallback((event: ChangeEvent) => { - setTransactionErrorInit(''); - const target = event.target as HTMLTextAreaElement; - setContractName(target.value); - }, []); - - const changeInputParameterFieldHandler = useCallback((event: ChangeEvent) => { - setParsingError(''); - setTransactionErrorInit(''); - const target = event.target as HTMLTextAreaElement; - setInputParameter(target.value); - }, []); - - const changeInputParameterTextAreaHandler = useCallback((event: ChangeEvent) => { - setParsingError(''); - setTransactionErrorInit(''); - const inputTextArea = inputParameterTextAreaRef.current as unknown as HTMLTextAreaElement; - inputTextArea?.setAttribute('style', `height:${inputTextArea.scrollHeight}px;overflow-y:hidden;`); - const target = event.target as HTMLTextAreaElement; - - try { - JSON.parse(target.value); - } catch (e) { - setParsingError((e as Error).message); - return; - } - - setInputParameter(JSON.stringify(JSON.parse(target.value))); - }, []); + const [embeddedModuleSchemaBase64Init, setEmbeddedModuleSchemaBase64Init] = useState(undefined); // Refresh accountInfo periodically. // eslint-disable-next-line consistent-return useEffect(() => { if (connection && client && account) { const interval = setInterval(() => { - console.log('refreshing_accountInfo'); - client - .getAccountInfo(new AccountAddress(account)) - .then((value) => { - if (value !== undefined) { - setAccountBalance(value.accountAmount.toString()); - setAccountExistsOnNetwork(true); - } else { - setAccountExistsOnNetwork(false); - } - setViewErrorAccountInfo(''); - }) - .catch((e) => { - setAccountBalance(''); - setViewErrorAccountInfo((e as Error).message.replaceAll('%20', ' ')); - setAccountExistsOnNetwork(false); - }); + getAccountInfo(client, account, setAccountBalance, setAccountExistsOnNetwork, setViewErrorAccountInfo); }, REFRESH_INTERVAL.asMilliseconds()); return () => clearInterval(interval); } }, [connection, account, client]); - // Refresh moduleReference periodically. - // eslint-disable-next-line consistent-return - useEffect(() => { - if (connection && client && account && txHashDeploy !== '') { - const interval = setInterval(() => { - console.log('refreshing_moduleReference'); - client - .getBlockItemStatus(txHashDeploy) - .then((report) => { - if (report !== undefined) { - setViewErrorModuleReference(''); - if ( - report.status === 'finalized' && - report.outcome.summary.type === TransactionSummaryType.AccountTransaction && - report.outcome.summary.transactionType === TransactionKindString.DeployModule - ) { - setModuleReferenceDeployed(report.outcome.summary.moduleDeployed.contents); - } - } - }) - .catch((e) => { - setModuleReferenceDeployed(''); - setViewErrorModuleReference((e as Error).message); - }); - }, REFRESH_INTERVAL.asMilliseconds()); - return () => clearInterval(interval); - } - }, [connection, account, client, txHashDeploy]); - - // Refresh smartContractIndex periodically. - // eslint-disable-next-line consistent-return - useEffect(() => { - if (connection && client && account && txHashInit !== '') { - const interval = setInterval(() => { - console.log('refreshing_smartContractIndex'); - client - .getBlockItemStatus(txHashInit) - .then((report) => { - if (report !== undefined) { - setViewErrorModuleReference(''); - if (report.status === 'finalized') { - if ( - report.outcome.summary.type === TransactionSummaryType.AccountTransaction && - report.outcome.summary.transactionType === TransactionKindString.InitContract - ) { - setSmartContractIndex( - report.outcome.summary.contractInitialized.address.index.toString() - ); - } else { - setSmartContractIndexError('Contract initialization failed'); - } - } - } - }) - .catch((e) => { - setModuleReferenceDeployed(''); - setViewErrorModuleReference((e as Error).message); - }); - }, REFRESH_INTERVAL.asMilliseconds()); - return () => clearInterval(interval); - } - }, [connection, account, client, txHashInit]); - - useEffect(() => { - if (connection && client && account && moduleReferenceCalculated) { - client - .getModuleSource(new ModuleReference(moduleReferenceCalculated)) - .then((value) => { - if (value === undefined) { - setIsModuleReferenceAlreadyDeployedStep1(false); - } else { - setIsModuleReferenceAlreadyDeployedStep1(true); - } - }) - .catch(() => { - setIsModuleReferenceAlreadyDeployedStep1(false); - }); - } - }, [connection, account, client, moduleReferenceCalculated]); - - useEffect(() => { - if (connection && client && account && moduleReference) { - client - .getModuleSource(new ModuleReference(moduleReference)) - .then((value) => { - if (value === undefined) { - setIsModuleReferenceAlreadyDeployedStep2(false); - } else { - setIsModuleReferenceAlreadyDeployedStep2(true); - } - }) - .catch(() => { - setIsModuleReferenceAlreadyDeployedStep2(false); - }); - } - }, [connection, account, client, moduleReference]); - - useEffect(() => { - if ( - moduleReference !== '' && - moduleReferenceCalculated !== '' && - moduleReferenceCalculated !== moduleReference - ) { - setShouldWarnDifferenceModuleReferences(true); - } else { - setShouldWarnDifferenceModuleReferences(false); - } - }, [moduleReference, moduleReferenceCalculated]); - - useEffect(() => { - if (inputParameterTemplate !== '' && hasInputParameter === false) { - setShouldWarnInputParameterInSchemaIgnored(true); - } else { - setShouldWarnInputParameterInSchemaIgnored(false); - } - }, [inputParameterTemplate, hasInputParameter]); - - useEffect(() => { - setSchemaError(''); - setInputParameterTemplate(''); - - let template = ''; - - if (contractName !== '' && (useModuleFromStep1 || uploadedModuleSchemaBase64 !== '')) { - try { - const inputParamterTypeSchemaBuffer = getInitContractParameterSchema( - toBuffer(useModuleFromStep1 ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64, 'base64'), - contractName, - 2 - ); - - template = displayTypeSchemaTemplate(inputParamterTypeSchemaBuffer); - - setInputParameterTemplate(template); - } catch (e) { - if (useModuleFromStep1) { - setSchemaError( - `Could not get embedded input parameter schema from the uploaded module. \nUncheck "Use Module from Step 1" checkbox to manually upload a schema. Original error: ${e}` - ); - } else { - setSchemaError(`Could not get input parameter schema from uploaded schema. Original error: ${e}`); - } - } - } - - if (dropDown === 'array') { - const element = inputParameterTextAreaRef.current as unknown as HTMLSelectElement; - element.value = getArrayExample(template); - } else if (dropDown === 'object') { - const element = inputParameterTextAreaRef.current as unknown as HTMLSelectElement; - element.value = getObjectExample(template); - } - }, [hasInputParameter, useModuleFromStep1, contractName, uploadedModuleSchemaBase64, dropDown]); - useEffect(() => { if (connection && client && account) { - client - .getAccountInfo(new AccountAddress(account)) - .then((value) => { - if (value !== undefined) { - setAccountBalance(value.accountAmount.toString()); - setAccountExistsOnNetwork(true); - } else { - setAccountExistsOnNetwork(false); - } - setViewErrorAccountInfo(''); - }) - .catch((e) => { - setViewErrorAccountInfo((e as Error).message.replaceAll('%20', ' ')); - setAccountBalance(''); - setAccountExistsOnNetwork(false); - }); + getAccountInfo(client, account, setAccountBalance, setAccountExistsOnNetwork, setViewErrorAccountInfo); } }, [connection, account, client]); + useEffect(() => { + select(); + }, []); + return (

- - {activeConnectorError && ( -

- Connector Error: {activeConnectorError}. -

- )} - {!activeConnectorError && !isWaitingForTransaction && activeConnectorType && !activeConnector && ( + {activeConnectorError && Connector Error: {activeConnectorError}.} + {!activeConnectorError && activeConnectorType && !activeConnector && (

Loading connector...

)} - {connectError && ( -

- Connect Error: {connectError}. -

- )} - {!connection && !isWaitingForTransaction && activeConnectorType && activeConnector && ( -

- -

+ {connectError && Connect Error: {connectError}.} + {!isConnected && ( + )} {connection && !accountExistsOnNetwork && ( <> @@ -461,639 +119,48 @@ export default function Main(props: ConnectionProps) { Error: {viewErrorAccountInfo}.
)} - {viewErrorModuleReference && ( -
- Error: {viewErrorModuleReference}. -
- )}
Connected account:
- +
-
Your account balance:
-
{accountBalance.replace(/(\d)(?=(\d\d\d\d\d\d)+(?!\d))/g, '$1.')} CCD
- - - {uploadError !== '' && ( -
- Error: {uploadError}. -
- )} -
- {base64Module && moduleReferenceCalculated && ( - <> -
- Calculated module reference: -
{moduleReferenceCalculated}
-
-
- Module in base64: -
{base64Module.toString().slice(0, 30)} ...
-
- {isModuleReferenceAlreadyDeployedStep1 && ( -
- Module reference already deployed. -
- )} -
- {!isModuleReferenceAlreadyDeployedStep1 && ( - - )} -
-
- - )} - {!txHashDeploy && transactionErrorDeploy && ( -
- Error: {transactionErrorDeploy}. -
- )} - {txHashDeploy && ( - <> -
- Transaction hash:{' '} - - {txHashDeploy} - -
-
-
- CCDScan will take a moment to pick up the above transaction, hence the above - link will work in a bit. -
-
- Deployed module reference will appear below once the transaction is - finalized. -
- - )} - {moduleReferenceDeployed && ( - <> -
-
-
- Module Reference deployed: -
{moduleReferenceDeployed}
-
- - )} -
- -
-
-
- -
- {useModuleFromStep1 && ( - <> -
-
-
- This checkbox autofilled the module reference, the{' '} - smart contract name, and the{' '} - input parameter schema from the module in step1. -
-
-
- Uncheck this box, if you want to manually fill in a{' '} - module reference, the smart contract name, or - an input parameter schema. -
-
-
- Uncheck and check this box again, if you want to load a - new module from step 1. -
-
-
- - )} - {moduleReferenceError && ( -
- Error: {moduleReferenceError}. -
- )} - - - {useModuleFromStep1 && - displayContracts.length > 0 && - (moduleReferenceDeployed !== '' || moduleReferenceCalculated !== '') ? ( - - ) : ( - - )} - {moduleReferenceLengthError && ( -
- Error: {moduleReferenceLengthError}. -
- )} -
-
-
- -
- {isPayable && ( -
- -
- )} -
-
- -
- {hasInputParameter && ( -
- {!useModuleFromStep1 && ( - <> - -
- {uploadedModuleSchemaBase64 && ( -
- Schema in base64: -
- {uploadedModuleSchemaBase64.toString().slice(0, 30)} ... -
-
- )} - - )} - {uploadError2 !== '' && ( -
- Error: {uploadError2}. -
- )} - {schemaError !== '' && ( -
- Error: {schemaError}. -
- )} -
- {inputParameterTemplate && ( -
- Input Parameter Template: -
-                                                    {JSON.stringify(JSON.parse(inputParameterTemplate), undefined, 2)}
-                                                
-
- )} - -
- {(dropDown === 'object' || dropDown === 'array') && ( - - )} - {(dropDown === 'string' || dropDown === 'number') && ( - - )} - {parsingError && ( -
- Error: {parsingError}. -
- )} -
- )} -
-
- -
-
- {shouldWarnDifferenceModuleReferences && ( -
- Warning: Module references in step 1 and step 2 are different. -
- )} - {shouldWarnInputParameterInSchemaIgnored && ( -
- Warning: Input parameter schema found but "Has Input Parameter" - checkbox is unchecked. -
- )} - {!txHashInit && transactionErrorInit && ( -
- Error: {transactionErrorInit}. -
- )} - {txHashInit && ( - <> -
- Transaction hash:{' '} - - {txHashInit} - -
-
-
- CCDScan will take a moment to pick up the above transaction, hence the above - link will work in a bit. -
-
- The smart contract index will appear below once the transaction is - finalized. -
- - )} -
-
- {smartContractIndexError !== '' && ( -
- Error: {smartContractIndexError}. -
- )} - {smartContractIndex !== '' && ( -
- Smart Contract Inedex: -
{smartContractIndex}
-
- )} -
+ + + + + + +

+
)} diff --git a/front-end-tools/src/Root.tsx b/front-end-tools/src/Root.tsx index 272b6d59..ef88b0f4 100644 --- a/front-end-tools/src/Root.tsx +++ b/front-end-tools/src/Root.tsx @@ -6,7 +6,7 @@ import Main from './Main'; import { version } from '../package.json'; /** - * Connect to wallet, setup application state context, and render children when the wallet API is ready for use. + * Select mainnet/testnet and display WithWalletConnector component for respective network. */ export default function Root() { const [isTestnet, setIsTestnet] = useState(true); @@ -34,9 +34,17 @@ export default function Root() {
Mainnet

- - {(props) =>
} - + {/* Changes to the network value will remove the activeConnector. We switch between components here without changing the network value. */} + {isTestnet && ( + + {(props) =>
} + + )} + {!isTestnet && ( + + {(props) =>
} + + )}
); diff --git a/front-end-tools/src/WalletConnectorTypeButton.tsx b/front-end-tools/src/WalletConnectorTypeButton.tsx deleted file mode 100644 index b02cbeba..00000000 --- a/front-end-tools/src/WalletConnectorTypeButton.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useCallback } from 'react'; -import { - ConnectorType, - useWalletConnectorSelector, - WalletConnection, - WalletConnectionProps, -} from '@concordium/react-components'; - -function connectorTypeStyle(isSelected: boolean, isConnected: boolean) { - if (isConnected) { - return { backgroundColor: '#823030', border: '1px solid #520C0C' }; - } - if (isSelected) { - return { backgroundColor: '#174039', border: '1px solid #0c221f' }; - } - return {}; -} - -interface Props extends WalletConnectionProps { - connectorType: ConnectorType; - connectorName: string; - setWaitingForUser: (v: boolean) => void; - connection: WalletConnection | undefined; -} - -export function WalletConnectionTypeButton(props: Props) { - const { connectorType, connectorName, setWaitingForUser, connection } = props; - const { isSelected, isConnected, isDisabled, select } = useWalletConnectorSelector( - connectorType, - connection, - props - ); - const onClick = useCallback(() => { - setWaitingForUser(false); - select(); - }, [select]); - return ( - - ); -} diff --git a/front-end-tools/src/components/Box.tsx b/front-end-tools/src/components/Box.tsx new file mode 100644 index 00000000..9845134c --- /dev/null +++ b/front-end-tools/src/components/Box.tsx @@ -0,0 +1,20 @@ +import React, { PropsWithChildren } from 'react'; + +type BoxProps = PropsWithChildren<{ + header: string; +}>; + +/** + * A component that creates a box with a header and background/borderLine styling. + */ +export default function Box(props: BoxProps) { + const { children, header } = props; + + return ( +
+ {header} +
{children}
+
+
+ ); +} diff --git a/front-end-tools/src/components/CCDScanLinks.tsx b/front-end-tools/src/components/CCDScanLinks.tsx new file mode 100644 index 00000000..6aff5585 --- /dev/null +++ b/front-end-tools/src/components/CCDScanLinks.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +function ccdScanUrl(isTestnet: boolean): string { + return `https://${isTestnet ? `testnet.` : ``}ccdscan.io`; +} + +interface TxHashLinkProps { + isTestnet: boolean; + txHash: string; + message: string; +} + +/** + * A component that displays the CCDScan link of a transaction hash. + * A message at the bottom can be used to add some custom description to the link. + * If `isTestnet` is true, the testnet CCDScan link is displayed. + * If `isTestnet` is false, the mainnet CCDScan link is displayed. + */ +export const TxHashLink = function TxHashLink(props: TxHashLinkProps) { + const { isTestnet, txHash, message } = props; + + return ( + <> +
+
+
+ CCDScan will take a moment to pick up the above transaction, hence the above link will work in a bit. +
+
{message}
+ + ); +}; + +interface AccountLinkProps { + isTestnet: boolean; + account: string; +} + +/** + * A component that displays the CCDScan link to an account address. + * If `isTestnet` is true, the testnet CCDScan link is displayed. + * If `isTestnet` is false, the mainnet CCDScan link is displayed. + */ +export const AccountLink = function AccountLink(props: AccountLinkProps) { + const { isTestnet, account } = props; + + return ( + + ); +}; diff --git a/front-end-tools/src/components/DeployComponent.tsx b/front-end-tools/src/components/DeployComponent.tsx new file mode 100644 index 00000000..78b1bcf7 --- /dev/null +++ b/front-end-tools/src/components/DeployComponent.tsx @@ -0,0 +1,283 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { Alert, Button, Form } from 'react-bootstrap'; +import { Buffer } from 'buffer'; + +import { WalletConnection } from '@concordium/react-components'; +import { + ModuleReference, + ConcordiumGRPCClient, + sha256, + TransactionSummaryType, + TransactionKindString, +} from '@concordium/web-sdk'; + +import { TxHashLink } from './CCDScanLinks'; +import Box from './Box'; +import { deploy } from '../writing_to_blockchain'; +import { arraysEqual } from '../utils'; +import { REFRESH_INTERVAL } from '../constants'; + +interface ConnectionProps { + account: string; + connection: WalletConnection; + client: ConcordiumGRPCClient | undefined; + isTestnet: boolean; + setContracts: (contracts: string[]) => void; + setEmbeddedModuleSchemaBase64Init: (embeddedModuleSchemaBase64Init: string) => void; + setModuleReferenceDeployed: (moduleReferenceDeployed: string | undefined) => void; + setModuleReferenceCalculated: (moduleReferenceCalculated: string) => void; + moduleReferenceCalculated: string | undefined; +} + +/** + * A component that manages the input fields and corresponding state to deploy a new smart contract wasm module on chain. + * This components creates a `DeployModule` transaction. + */ +export default function DeployComponenet(props: ConnectionProps) { + const { + isTestnet, + client, + connection, + account, + setContracts, + setModuleReferenceDeployed, + setModuleReferenceCalculated, + moduleReferenceCalculated, + setEmbeddedModuleSchemaBase64Init, + } = props; + + type FormType = { + file: FileList | undefined; + }; + const form = useForm({ mode: 'all' }); + + const [transactionErrorDeploy, setTransactionErrorDeploy] = useState(undefined); + const [uploadError, setUploadError] = useState(undefined); + const [isModuleReferenceAlreadyDeployedStep1, setIsModuleReferenceAlreadyDeployedStep1] = useState(false); + const [txHashDeploy, setTxHashDeploy] = useState(undefined); + const [base64Module, setBase64Module] = useState(undefined); + const [transactionOutcome, setTransactionOutcome] = useState(undefined); + + // Refresh moduleReference periodically. + // eslint-disable-next-line consistent-return + useEffect(() => { + if (connection && client && txHashDeploy !== undefined) { + const interval = setInterval(() => { + client + .getBlockItemStatus(txHashDeploy) + .then((report) => { + if (report !== undefined && report.status === 'finalized') { + if ( + report.outcome.summary.type === TransactionSummaryType.AccountTransaction && + report.outcome.summary.transactionType === TransactionKindString.DeployModule + ) { + setTransactionOutcome('Success'); + setModuleReferenceDeployed(report.outcome.summary.moduleDeployed.contents); + clearInterval(interval); + } else { + setTransactionOutcome('Fail'); + clearInterval(interval); + } + } + }) + .catch((e) => { + setModuleReferenceDeployed(undefined); + setTransactionOutcome(`Fail; Error: ${(e as Error).message}`); + clearInterval(interval); + }); + }, REFRESH_INTERVAL.asMilliseconds()); + return () => clearInterval(interval); + } + }, [connection, client, txHashDeploy]); + + useEffect(() => { + if (connection && client && moduleReferenceCalculated) { + client + .getModuleSource(new ModuleReference(moduleReferenceCalculated)) + .then((value) => { + if (value === undefined) { + setIsModuleReferenceAlreadyDeployedStep1(false); + } else { + setIsModuleReferenceAlreadyDeployedStep1(true); + } + }) + .catch(() => { + setIsModuleReferenceAlreadyDeployedStep1(false); + }); + } + }, [connection, client, moduleReferenceCalculated]); + + function onSubmit() { + setTxHashDeploy(undefined); + setTransactionErrorDeploy(undefined); + setTransactionOutcome(undefined); + + // Send deploy transaction + + const tx = deploy(connection, account, base64Module); + tx.then((txHash) => { + setModuleReferenceDeployed(undefined); + setTxHashDeploy(txHash); + }).catch((err: Error) => setTransactionErrorDeploy((err as Error).message)); + } + + return ( + +
+ + Upload Smart Contract Module File (e.g. myContract.wasm.v1) + { + const register = form.register('file'); + + register.onChange(e); + + setUploadError(undefined); + setModuleReferenceDeployed(undefined); + setTransactionErrorDeploy(undefined); + setTxHashDeploy(undefined); + + const files = form.getValues('file'); + + if (files !== undefined && files !== null && files.length > 0) { + const file = files[0]; + const arrayBuffer = await file.arrayBuffer(); + + // Use `reduce` to be able to convert large modules. + const module = btoa( + new Uint8Array(arrayBuffer).reduce((data, byte) => { + return data + String.fromCharCode(byte); + }, '') + ); + + setBase64Module(module); + setModuleReferenceCalculated( + Buffer.from(sha256([new Uint8Array(arrayBuffer)])).toString('hex') + ); + + // Concordium's tooling create versioned modules e.g. `.wasm.v1` now. + // Unversioned modules `.wasm` cannot be created by Concordium's tooling anymore. + // If the module is versioned, the first 4 bytes are the version, the next 4 bytes are the length, followed by the `magicValue` below. + // If the module is an old unversioned one, the module starts with the `magicValue` below. + // The `magicValue` is the magic value for Wasm modules as specified by the Wasm spec. + const magicValue = new Uint8Array([0x00, 0x61, 0x73, 0x6d]); + let uploadedModuleFirst4Bytes = new Uint8Array([]); + + if (arrayBuffer.byteLength >= 4) { + uploadedModuleFirst4Bytes = new Uint8Array(arrayBuffer).subarray(0, 4); + } else { + setUploadError( + `You might have not uploaded a valid Wasm module. Byte length of a Wasm module needs to be at least 4.` + ); + } + + // If we have an unversioned module, we remove no bytes. + // If we have a versioned module, we remove 8 bytes at the beginning (version and length information). + const slice = arraysEqual(uploadedModuleFirst4Bytes, magicValue) ? 0 : 8; + + let wasmModule; + try { + wasmModule = await WebAssembly.compile(arrayBuffer.slice(slice)); + } catch (err) { + setUploadError( + `You might have not uploaded a Concordium module. Original error: ${ + (err as Error).message + }` + ); + } + + if (wasmModule) { + const moduleFunctions = WebAssembly.Module.exports(wasmModule); + + const contractNames = []; + for (let i = 0; i < moduleFunctions.length; i += 1) { + if (moduleFunctions[i].name.startsWith('init_')) { + contractNames.push(moduleFunctions[i].name.slice(5)); + } + } + setContracts(contractNames); + + const customSection = WebAssembly.Module.customSections( + wasmModule, + 'concordium-schema' + ); + + const schema = new Uint8Array(customSection[0]); + + // Use `reduce` to be able to convert large schema. + const moduleSchemaBase64Embedded = btoa( + new Uint8Array(schema).reduce((data, byte) => { + return data + String.fromCharCode(byte); + }, '') + ); + + setEmbeddedModuleSchemaBase64Init(moduleSchemaBase64Embedded); + } else { + setUploadError('Upload module file is undefined'); + } + } + }} + /> + + + {uploadError !== undefined && Error: {uploadError}. } +
+ {base64Module && moduleReferenceCalculated && ( + <> +
+ Calculated module reference: +
{moduleReferenceCalculated}
+
+
+ Module in base64: +
{base64Module.toString().slice(0, 30)} ...
+
+ {isModuleReferenceAlreadyDeployedStep1 && ( + Module is already deployed. + )} +
+ {!isModuleReferenceAlreadyDeployedStep1 && ( + + )} +
+
+ + )} +
+ + {!txHashDeploy && transactionErrorDeploy && ( + Error: {transactionErrorDeploy}. + )} + {txHashDeploy && ( + + )} + {transactionOutcome === 'Success' && ( + <> +
+
+ Outcome of transaction: +
{transactionOutcome}
+
+ + )} + {transactionOutcome !== undefined && transactionOutcome !== 'Success' && ( + <> +
+
Outcome of transaction:
+
+ Error: {transactionOutcome}. + + )} +
+ ); +} diff --git a/front-end-tools/src/components/InitComponent.tsx b/front-end-tools/src/components/InitComponent.tsx new file mode 100644 index 00000000..da0c7a44 --- /dev/null +++ b/front-end-tools/src/components/InitComponent.tsx @@ -0,0 +1,655 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import Select from 'react-select'; +import { Alert, Button, Form, Row } from 'react-bootstrap'; + +import { WalletConnection } from '@concordium/react-components'; +import { + ModuleReference, + TransactionKindString, + TransactionSummaryType, + displayTypeSchemaTemplate, + toBuffer, + ConcordiumGRPCClient, + getInitContractParameterSchema, +} from '@concordium/web-sdk'; + +import { TxHashLink } from './CCDScanLinks'; +import Box from './Box'; +import { initialize } from '../writing_to_blockchain'; +import { getObjectExample, getArrayExample } from '../utils'; +import { REFRESH_INTERVAL, INPUT_PARAMETER_TYPES_OPTIONS, REG_MODULE_REF } from '../constants'; + +interface ConnectionProps { + isTestnet: boolean; + account: string; + connection: WalletConnection; + client: ConcordiumGRPCClient | undefined; + contracts: string[]; + embeddedModuleSchemaBase64: undefined | string; + moduleReferenceCalculated: undefined | string; + moduleReferenceDeployed: undefined | string; +} + +/** + * A component that manages the input fields and corresponding state to initialize a new smart contract instance on chain. + * This components creates an `InitContract` transaction. + */ +export default function InitComponent(props: ConnectionProps) { + const { + isTestnet, + account, + connection, + client, + moduleReferenceCalculated, + moduleReferenceDeployed, + embeddedModuleSchemaBase64, + contracts, + } = props; + + type FormType = { + moduleReference: string | undefined; + smartContractName: string | undefined; + file: FileList | undefined; + useModuleReferenceFromStep1: boolean; + inputParameterType: string | undefined; + inputParameter: string | undefined; + hasInputParameter: boolean; + maxExecutionEnergy: number; + isPayable: boolean; + cCDAmount: number; + }; + + const form = useForm({ mode: 'all' }); + + const [ + useModuleReferenceFromStep1, + smartContractName, + inputParameterType, + isPayable, + hasInputParameter, + moduleReference, + ] = useWatch({ + control: form.control, + name: [ + 'useModuleReferenceFromStep1', + 'smartContractName', + 'inputParameterType', + 'isPayable', + 'hasInputParameter', + 'moduleReference', + ], + }); + + const [transactionError, setTransactionError] = useState(undefined); + + const [uploadError2, setUploadError2] = useState(undefined); + const [parsingError, setParsingError] = useState(undefined); + const [smartContractIndexError, setSmartContractIndexError] = useState(undefined); + const [moduleReferenceError, setModuleReferenceError] = useState(undefined); + const [moduleReferenceLengthError, setModuleReferenceLengthError] = useState(undefined); + const [schemaError, setSchemaError] = useState(undefined); + + const [isModuleReferenceAlreadyDeployedStep2, setIsModuleReferenceAlreadyDeployedStep2] = useState(false); + + const [txHash, setTxHash] = useState(undefined); + + const [uploadedModuleSchemaBase64, setUploadedModuleSchemaBase64] = useState(undefined); + + const [smartContractIndex, setSmartContractIndex] = useState(undefined); + const [inputParameterTemplate, setInputParameterTemplate] = useState(undefined); + const [displayContracts, setDisplayContracts] = useState([]); + + // Refresh smartContractIndex periodically. + // eslint-disable-next-line consistent-return + useEffect(() => { + if (connection && client && txHash !== undefined) { + const interval = setInterval(() => { + client + .getBlockItemStatus(txHash) + .then((report) => { + if (report !== undefined) { + setSmartContractIndex(undefined); + if (report.status === 'finalized') { + if ( + report.outcome.summary.type === TransactionSummaryType.AccountTransaction && + report.outcome.summary.transactionType === TransactionKindString.InitContract + ) { + setSmartContractIndex( + report.outcome.summary.contractInitialized.address.index.toString() + ); + clearInterval(interval); + } else { + setSmartContractIndexError('Contract initialization failed'); + clearInterval(interval); + } + } + } + }) + .catch((e) => { + setSmartContractIndex(undefined); + setSmartContractIndexError((e as Error).message); + clearInterval(interval); + }); + }, REFRESH_INTERVAL.asMilliseconds()); + return () => clearInterval(interval); + } + }, [connection, client, txHash]); + + useEffect(() => { + if (connection && client && moduleReference) { + if (REG_MODULE_REF.test(moduleReference)) { + client + .getModuleSource(new ModuleReference(moduleReference)) + .then((value) => { + if (value === undefined) { + setIsModuleReferenceAlreadyDeployedStep2(false); + } else { + setIsModuleReferenceAlreadyDeployedStep2(true); + } + }) + .catch(() => { + setIsModuleReferenceAlreadyDeployedStep2(false); + }); + } + } + }, [connection, client, moduleReference]); + + const shouldWarnDifferenceModuleReferences = useMemo(() => { + if ( + moduleReference !== undefined && + moduleReferenceCalculated !== undefined && + moduleReferenceCalculated !== moduleReference + ) { + return true; + } + return false; + }, [moduleReference, moduleReferenceCalculated]); + + const shouldWarnInputParameterInSchemaIgnored = useMemo(() => { + if (inputParameterTemplate !== undefined && hasInputParameter === false) { + return true; + } + return false; + }, [inputParameterTemplate, hasInputParameter]); + + useEffect(() => { + setSchemaError(undefined); + setInputParameterTemplate(undefined); + + let initTemplate; + + try { + if (smartContractName === undefined) { + throw new Error('Set smart contract name'); + } + + let schema = ''; + + const schemaFromModule = useModuleReferenceFromStep1 + ? embeddedModuleSchemaBase64 + : uploadedModuleSchemaBase64; + + if (schemaFromModule !== undefined) { + schema = schemaFromModule; + } + + const inputParamterTypeSchemaBuffer = getInitContractParameterSchema( + toBuffer(schema, 'base64'), + smartContractName, + 2 + ); + + initTemplate = displayTypeSchemaTemplate(inputParamterTypeSchemaBuffer); + + setInputParameterTemplate(initTemplate); + } catch (e) { + if (useModuleReferenceFromStep1) { + setSchemaError( + `Could not get embedded schema from the uploaded module. Uncheck "Use Module from Step 1" checkbox to manually upload a schema or uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } else { + setSchemaError( + `Could not get schema from uploaded schema. Uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } + } + + if (initTemplate) { + if (inputParameterType === 'array') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(initTemplate), undefined, 2)); + } else if (inputParameterType === 'object') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(initTemplate), undefined, 2)); + } + } + }, [ + hasInputParameter, + useModuleReferenceFromStep1, + smartContractName, + uploadedModuleSchemaBase64, + inputParameterType, + ]); + + function onSubmit(data: FormType) { + setTxHash(undefined); + setSmartContractIndexError(undefined); + setSmartContractIndex(undefined); + setTransactionError(undefined); + + const schema = data.useModuleReferenceFromStep1 ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64; + + // Send init transaction + + const tx = initialize( + connection, + account, + isModuleReferenceAlreadyDeployedStep2, + data.moduleReference, + data.inputParameter, + data.smartContractName, + data.hasInputParameter, + data.useModuleReferenceFromStep1, + schema, + data.inputParameterType, + BigInt(data.maxExecutionEnergy), + data.cCDAmount ? BigInt(data.cCDAmount) : BigInt(0) + ); + tx.then(setTxHash).catch((err: Error) => setTransactionError((err as Error).message)); + } + + return ( + +
+ + { + const register = form.register('useModuleReferenceFromStep1'); + + register.onChange(e); + + setModuleReferenceError(undefined); + form.setValue('moduleReference', undefined); + + setUploadedModuleSchemaBase64(undefined); + + const checkboxElement = form.getValues('useModuleReferenceFromStep1'); + + form.setValue('moduleReference', undefined); + setModuleReferenceLengthError(undefined); + + if ( + checkboxElement && + moduleReferenceDeployed === undefined && + moduleReferenceCalculated === undefined + ) { + setModuleReferenceError('No module is uploaded in step 1'); + } + + const newModuleReference = + moduleReferenceDeployed !== undefined + ? moduleReferenceDeployed + : moduleReferenceCalculated; + + if (checkboxElement && newModuleReference !== undefined) { + form.setValue('moduleReference', newModuleReference); + + setDisplayContracts(contracts); + form.setValue('smartContractName', contracts[0]); + } + }} + /> + + + {useModuleReferenceFromStep1 && ( + <> +
+ +
+ This checkbox autofilled the module reference, the{' '} + smart contract name, and the input parameter schema from the + module in step1. +
+
+
+ Uncheck this box, if you want to manually fill in a module reference + , the smart contract name, or an input parameter schema. +
+
+
+ Uncheck and check this box again, if you want to load a new module from + step 1. +
+
+
+ + )} + + {moduleReferenceError && Error: {moduleReferenceError}. } + + + + Module Reference + { + const register = form.register('moduleReference', { + required: true, + }); + + register.onChange(e); + + setModuleReferenceLengthError(undefined); + + const moduleRef = form.getValues('moduleReference'); + + if (moduleRef !== undefined && !REG_MODULE_REF.test(moduleRef)) { + setModuleReferenceLengthError( + 'Module reference has to be a valid hex string `[0-9A-Fa-f]` of length 64' + ); + } + }} + /> + + {form.formState.errors.moduleReference && ( + Module reference is required + )} + + + {useModuleReferenceFromStep1 && displayContracts.length > 0 ? ( + + Smart Contract Name + + { + form.setValue('inputParameterType', e?.value); + form.setValue('inputParameter', undefined); + + setParsingError(undefined); + }} + /> + + + + {(inputParameterType === 'number' || inputParameterType === 'string') && ( + + Add your input parameter ({inputParameterType}): + { + const register = form.register('inputParameter', { + required: true, + }); + + register.onChange(e); + + setParsingError(undefined); + }} + /> + {form.formState.errors.inputParameter && ( + Input parameter is required + )} + + + )} + + {(inputParameterType === 'object' || inputParameterType === 'array') && ( + + Add your input parameter ({inputParameterType}): + + {inputParameterType === 'array' && ( + + )} + {inputParameterType === 'object' && ( + + )} + + {form.formState.errors.inputParameter && ( + Input parameter is required + )} + + + )} + + {parsingError !== undefined && Error: {parsingError}. } + + )} +
+ + +
+
+ {shouldWarnDifferenceModuleReferences && ( + Warning: Module references in step 1 and step 2 are different. + )} + {shouldWarnInputParameterInSchemaIgnored && ( + + {' '} + Warning: Input parameter schema found but "Has Input Parameter" checkbox is unchecked. + + )} + {!txHash && transactionError && Error: {transactionError}.} + {txHash && ( + + )} +
+ {smartContractIndexError !== undefined && ( + Error: {smartContractIndexError}. + )} + {smartContractIndex !== undefined && ( +
+ Smart Contract Inedex: +
{smartContractIndex}
+
+ )} + +
+ ); +} diff --git a/front-end-tools/src/components/ReadComponent.tsx b/front-end-tools/src/components/ReadComponent.tsx new file mode 100644 index 00000000..d43fb2fe --- /dev/null +++ b/front-end-tools/src/components/ReadComponent.tsx @@ -0,0 +1,534 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import Select from 'react-select'; +import { Alert, Button, Form, Row } from 'react-bootstrap'; + +import { WalletConnection } from '@concordium/react-components'; +import { + ModuleReference, + displayTypeSchemaTemplate, + toBuffer, + getUpdateContractParameterSchema, + ConcordiumGRPCClient, +} from '@concordium/web-sdk'; + +import Box from './Box'; +import { read, getEmbeddedSchema, getContractInfo } from '../reading_from_blockchain'; +import { getObjectExample, getArrayExample } from '../utils'; +import { INPUT_PARAMETER_TYPES_OPTIONS } from '../constants'; + +interface ConnectionProps { + account: string; + connection: WalletConnection; + client: ConcordiumGRPCClient | undefined; +} + +/** + * A component that manages the input fields and corresponding state to read from a smart contract instance on the chain. + * The `invoke` action is used in this component which does not create a transaction. + */ +export default function ReadComponenet(props: ConnectionProps) { + const { client } = props; + + type FormType = { + smartContractIndex: number; + smartContractName: string; + entryPointName: string | undefined; + file: FileList | undefined; + hasInputParameter: boolean; + deriveFromSmartContractIndex: boolean; + inputParameterType: string | undefined; + inputParameter: string | undefined; + }; + + const form = useForm({ mode: 'all' }); + + const [deriveContractInfo, smartContractName, inputParameterType, entryPointName, hasInputParameter] = useWatch({ + control: form.control, + name: [ + 'deriveFromSmartContractIndex', + 'smartContractName', + 'inputParameterType', + 'entryPointName', + 'hasInputParameter', + ], + }); + + const [schemaError, setSchemaError] = useState(undefined); + + const [uploadError, setUploadError] = useState(undefined); + const [parsingError, setParsingError] = useState(undefined); + + const [uploadedModuleSchemaBase64, setUploadedModuleSchemaBase64] = useState(undefined); + + const [contractInstanceInfo, setContractInstanceInfo] = useState< + { contractName: string; methods: string[]; sourceModule: ModuleReference } | undefined + >(undefined); + const [returnValue, setReturnValue] = useState(undefined); + const [error, setError] = useState(undefined); + + const [entryPointTemplate, setEntryPointTemplate] = useState(undefined); + + const [embeddedModuleSchemaBase64, setEmbeddedModuleSchemaBase64] = useState(undefined); + + const shouldWarnInputParameterInSchemaIgnored = useMemo(() => { + if (entryPointTemplate !== undefined && form.getValues('hasInputParameter') === false) { + return true; + } + return false; + }, [entryPointTemplate, hasInputParameter]); + + useEffect(() => { + setSchemaError(undefined); + setEntryPointTemplate(undefined); + + let receiveTemplate; + + try { + if (entryPointName === undefined) { + throw new Error('Set entry point name'); + } + + if (smartContractName === undefined) { + throw new Error('Set smart contract name'); + } + + let schema = ''; + + const schemaFromModule = deriveContractInfo ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64; + + if (schemaFromModule !== undefined) { + schema = schemaFromModule; + } + + const readFunctionTemplate = getUpdateContractParameterSchema( + toBuffer(schema, 'base64'), + smartContractName, + entryPointName + ); + + receiveTemplate = displayTypeSchemaTemplate(readFunctionTemplate); + + setEntryPointTemplate(receiveTemplate); + } catch (e) { + if (deriveContractInfo) { + setSchemaError( + `Could not derive the embedded schema from the smart contract index. Uncheck "Derive From Smart Contract Index" checkbox to manually upload a schema or uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } else { + setSchemaError( + `Could not get schema from uploaded schema. Uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } + } + + if (receiveTemplate) { + if (inputParameterType === 'array') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(receiveTemplate), undefined, 2)); + } else if (inputParameterType === 'object') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(receiveTemplate), undefined, 2)); + } + } + }, [entryPointName, hasInputParameter, smartContractName, uploadedModuleSchemaBase64, inputParameterType]); + + function onSubmit(data: FormType) { + setError(undefined); + setReturnValue(undefined); + + const schema = data.deriveFromSmartContractIndex ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64; + + // Invoke smart contract (read) + + const promise = read( + client, + data.smartContractName, + BigInt(data.smartContractIndex), + data.entryPointName, + schema, + data.inputParameter, + data.inputParameterType, + data.hasInputParameter, + data.deriveFromSmartContractIndex + ); + + promise + .then((value) => { + setReturnValue(value); + }) + .catch((err: Error) => setError((err as Error).message)); + } + + return ( + +
+ + + Smart Contract Index + + + {form.formState.errors.smartContractIndex && ( + + {' '} + Smart contract index is required{' '} + + )} + + + {deriveContractInfo && + contractInstanceInfo !== undefined && + contractInstanceInfo.contractName !== undefined ? ( + + Smart Contract Name + + {form.formState.errors.smartContractName && ( + + {' '} + Smart contract name is required{' '} + + )} + + + ) : ( + + Smart Contract Name + + {form.formState.errors.smartContractName && ( + + {' '} + Smart contract name is required{' '} + + )} + + + )} + + {deriveContractInfo && + contractInstanceInfo !== undefined && + contractInstanceInfo.methods.length > 0 ? ( + + Entry Point Name + { + form.setValue('inputParameterType', e?.value); + form.setValue('inputParameter', undefined); + + setParsingError(undefined); + }} + /> + + + + {(inputParameterType === 'number' || inputParameterType === 'string') && ( + + Add your input parameter ({inputParameterType}): + { + const inputParameterRegister = form.register('inputParameter', { + required: true, + }); + + inputParameterRegister.onChange(e); + + setParsingError(undefined); + }} + /> + {form.formState.errors.inputParameter && ( + + {' '} + Input parameter is required{' '} + + )} + + + )} + + {(inputParameterType === 'object' || inputParameterType === 'array') && ( + + Add your input parameter ({inputParameterType}): + + {inputParameterType === 'array' && ( + + )} + {inputParameterType === 'object' && ( + + )} + + {form.formState.errors.inputParameter && ( + + {' '} + Input parameter is required{' '} + + )} + + + )} + + {parsingError !== undefined && Error: {parsingError}. } + + )} + +
+ + +
+
+ {(deriveContractInfo ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64) === undefined && ( + + {' '} + Warning: There is no module schema, so the return value cannot be decoded.{' '} + + )} + {shouldWarnInputParameterInSchemaIgnored && ( + + {' '} + Warning: Input parameter schema found but "Has Input Parameter" checkbox is unchecked.{' '} + + )} + {error && Error: {error}. } + {returnValue && ( +
+ Read value: +
{JSON.stringify(JSON.parse(returnValue), undefined, 2)}
+
+ )} + +
+ ); +} diff --git a/front-end-tools/src/components/UpdateComponent.tsx b/front-end-tools/src/components/UpdateComponent.tsx new file mode 100644 index 00000000..15ddee32 --- /dev/null +++ b/front-end-tools/src/components/UpdateComponent.tsx @@ -0,0 +1,623 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useForm, useWatch } from 'react-hook-form'; +import Select from 'react-select'; +import { Alert, Button, Form, Row } from 'react-bootstrap'; + +import { WalletConnection } from '@concordium/react-components'; +import { + ModuleReference, + TransactionKindString, + TransactionSummaryType, + displayTypeSchemaTemplate, + toBuffer, + getUpdateContractParameterSchema, + ConcordiumGRPCClient, +} from '@concordium/web-sdk'; + +import Box from './Box'; +import { TxHashLink } from './CCDScanLinks'; +import { update } from '../writing_to_blockchain'; +import { getEmbeddedSchema, getContractInfo } from '../reading_from_blockchain'; +import { getObjectExample, getArrayExample } from '../utils'; +import { REFRESH_INTERVAL, INPUT_PARAMETER_TYPES_OPTIONS } from '../constants'; + +interface ConnectionProps { + isTestnet: boolean; + account: string; + connection: WalletConnection; + client: ConcordiumGRPCClient | undefined; +} + +/* A component that manages the input fields and corresponding state to update a smart contract instance on the chain. + * This components creates an `Update` transaction. + */ +export default function UpdateComponenet(props: ConnectionProps) { + const { isTestnet, account, connection, client } = props; + + type FormType = { + smartContractIndex: number; + smartContractName: string; + entryPointName: string | undefined; + file: FileList | undefined; + hasInputParameter: boolean; + deriveFromSmartContractIndex: boolean; + inputParameterType: string | undefined; + inputParameter: string | undefined; + maxExecutionEnergy: number; + isPayable: boolean; + cCDAmount: number; + }; + + const form = useForm({ mode: 'all' }); + + const [deriveContractInfo, hasInputParameter, inputParameterType, isPayable, smartContractName, entryPointName] = + useWatch({ + control: form.control, + name: [ + 'deriveFromSmartContractIndex', + 'hasInputParameter', + 'inputParameterType', + 'isPayable', + 'smartContractName', + 'entryPointName', + ], + }); + + const [uploadError, setUploadError] = useState(undefined); + const [parsingError, setParsingError] = useState(undefined); + const [schemaError, setSchemaError] = useState(undefined); + + const [transactionErrorUpdate, setTransactionErrorUpdate] = useState(undefined); + const [txHashUpdate, setTxHashUpdate] = useState(undefined); + const [uploadedModuleSchemaBase64, setUploadedModuleSchemaBase64] = useState(undefined); + + const [contractInstanceInfo, setContractInstanceInfo] = useState< + { contractName: string; methods: string[]; sourceModule: ModuleReference } | undefined + >(undefined); + const [error, setError] = useState(undefined); + + const [entryPointTemplate, setEntryPointTemplate] = useState(undefined); + + const [transactionOutcome, setTransactionOutcome] = useState(undefined); + + const [embeddedModuleSchemaBase64, setEmbeddedModuleSchemaBase64] = useState(undefined); + + // Refresh transactionOutcome periodically. + // eslint-disable-next-line consistent-return + useEffect(() => { + if (connection && client && txHashUpdate !== undefined) { + const interval = setInterval(() => { + client + .getBlockItemStatus(txHashUpdate) + .then((report) => { + if (report !== undefined && report.status === 'finalized') { + if ( + report.outcome.summary.type === TransactionSummaryType.AccountTransaction && + report.outcome.summary.transactionType === TransactionKindString.Update + ) { + setTransactionOutcome('Success'); + clearInterval(interval); + } else { + setTransactionOutcome('Fail'); + clearInterval(interval); + } + } + }) + .catch((e) => { + setTransactionOutcome(`Fail; Error: ${(e as Error).message}`); + clearInterval(interval); + }); + }, REFRESH_INTERVAL.asMilliseconds()); + return () => clearInterval(interval); + } + }, [connection, client, txHashUpdate]); + + const shouldWarnInputParameterInSchemaIgnored = useMemo(() => { + if (entryPointTemplate !== undefined && hasInputParameter === false) { + return true; + } + return false; + }, [entryPointTemplate, hasInputParameter]); + + useEffect(() => { + setSchemaError(undefined); + setEntryPointTemplate(undefined); + + let receiveTemplate; + + try { + if (entryPointName === undefined) { + throw new Error('Set entry point name'); + } + + if (smartContractName === undefined) { + throw new Error('Set smart contract name'); + } + + let schema = ''; + + const schemaFromModule = deriveContractInfo ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64; + + if (schemaFromModule !== undefined) { + schema = schemaFromModule; + } + + const functionTemplate = getUpdateContractParameterSchema( + toBuffer(schema, 'base64'), + smartContractName, + entryPointName + ); + + receiveTemplate = displayTypeSchemaTemplate(functionTemplate); + + setEntryPointTemplate(receiveTemplate); + } catch (e) { + if (deriveContractInfo) { + setSchemaError( + `Could not derive the embedded schema from the smart contract index. Uncheck "Derive From Smart Contract Index" checkbox to manually upload a schema or uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } else { + setSchemaError( + `Could not get schema from uploaded schema. Uncheck "Has Input Paramter" checkbox if this entrypoint has no input parameter. Original error: ${e}` + ); + } + } + + if (receiveTemplate) { + if (inputParameterType === 'array') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(receiveTemplate), undefined, 2)); + } else if (inputParameterType === 'object') { + form.setValue('inputParameter', JSON.stringify(JSON.parse(receiveTemplate), undefined, 2)); + } + } + }, [entryPointName, hasInputParameter, smartContractName, uploadedModuleSchemaBase64, inputParameterType]); + + function onSubmit(data: FormType) { + setTxHashUpdate(undefined); + setTransactionErrorUpdate(undefined); + setTransactionOutcome(undefined); + + const schema = deriveContractInfo ? embeddedModuleSchemaBase64 : uploadedModuleSchemaBase64; + + // Send update transaction + + const tx = update( + connection, + account, + data.inputParameter, + data.smartContractName, + data.entryPointName, + data.hasInputParameter, + data.deriveFromSmartContractIndex, + schema, + data.inputParameterType, + BigInt(data.maxExecutionEnergy), + BigInt(data.smartContractIndex), + data.cCDAmount ? BigInt(data.cCDAmount) : BigInt(0) + ); + + tx.then(setTxHashUpdate).catch((err: Error) => setTransactionErrorUpdate((err as Error).message)); + } + + return ( + +
+ + + Smart Contract Index + + + {form.formState.errors.smartContractIndex && ( + Smart contract index is required + )} + + + {deriveContractInfo && + contractInstanceInfo !== undefined && + contractInstanceInfo.contractName !== undefined ? ( + + Smart Contract Name + + {form.formState.errors.smartContractName && ( + Smart contract name is required + )} + + + ) : ( + + Smart Contract Name + + {form.formState.errors.smartContractName && ( + Smart contract name is required + )} + + + )} + + {deriveContractInfo && + contractInstanceInfo !== undefined && + contractInstanceInfo.methods.length > 0 ? ( + + Entry Point Name + { + form.setValue('inputParameterType', e?.value); + form.setValue('inputParameter', undefined); + + setParsingError(undefined); + }} + /> + + + + {(inputParameterType === 'number' || inputParameterType === 'string') && ( + + Add your input parameter ({inputParameterType}): + { + const inputParameterRegister = form.register('inputParameter', { + required: true, + }); + + inputParameterRegister.onChange(e); + + setParsingError(undefined); + }} + /> + {form.formState.errors.inputParameter && ( + Input parameter is required + )} + + + )} + + {(inputParameterType === 'object' || inputParameterType === 'array') && ( + + Add your input parameter ({inputParameterType}): + + {inputParameterType === 'array' && ( + + )} + {inputParameterType === 'object' && ( + + )} + + {form.formState.errors.inputParameter && ( + Input parameter is required + )} + + + )} + + {parsingError !== undefined && Error: {parsingError}. } + + )} + +
+ + +
+
+ {shouldWarnInputParameterInSchemaIgnored && ( + + {' '} + Warning: Input parameter schema found but "Has Input Parameter" checkbox is unchecked. + + )} + {!txHashUpdate && transactionErrorUpdate && ( + Error: {transactionErrorUpdate}. + )} + {txHashUpdate && ( + + )} + {transactionOutcome === 'Success' && ( + <> +
+
+ Outcome of transaction: +
{transactionOutcome}
+
+ + )} + {transactionOutcome !== undefined && transactionOutcome !== 'Success' && ( + <> +
+
Outcome of transaction:
+
+ Error: {transactionOutcome}. + + )} + +
+ ); +} diff --git a/front-end-tools/src/constants.ts b/front-end-tools/src/constants.ts index b39c9181..7504616a 100644 --- a/front-end-tools/src/constants.ts +++ b/front-end-tools/src/constants.ts @@ -1,6 +1,7 @@ import { BrowserWalletConnector, ephemeralConnectorType } from '@concordium/react-components'; import moment from 'moment'; +// The refresh interval is used by polling at the front end. export const REFRESH_INTERVAL = moment.duration(5, 'seconds'); export const BROWSER_WALLET = ephemeralConnectorType(BrowserWalletConnector.create); @@ -19,3 +20,17 @@ export const EXAMPLE_JSON_OBJECT = { // These are the example arrays that are shown in the input parameter textarea as a placeholder when the user has no embedded schema in the module // or does not want to use the embedded schema (meaning if the checkbox "Use module from step 1" is unchecked). export const EXAMPLE_ARRAYS = 'Examples: \n\n[1,2,3] or \n\n["abc","def"] or \n\n[{"myFieldKey":"myFieldValue"}]'; + +// The input parameter can have one of these type options. +export const INPUT_PARAMETER_TYPES_OPTIONS = [ + { label: 'number', value: 'number' }, + { label: 'string', value: 'string' }, + { label: 'object', value: 'object' }, + { label: 'array', value: 'array' }, +]; + +// The subindex of all smart contracts. +export const CONTRACT_SUB_INDEX = 0n; + +// Regular expression of a valid module reference which has to be a hex string `[0-9A-Fa-f]` of length 64. +export const REG_MODULE_REF = /^[0-9A-Fa-f]{64}$/; diff --git a/front-end-tools/src/index.css b/front-end-tools/src/index.css index 51b1f761..e6dc0f98 100644 --- a/front-end-tools/src/index.css +++ b/front-end-tools/src/index.css @@ -12,10 +12,11 @@ h3 { } textarea { - left: 10px; - top: 10px; width: 100%; - height: 300px; + height: 200px; + box-sizing: border-box; + border-radius: 4px; + font-size: 16px; } pre { @@ -46,10 +47,6 @@ label, margin-top: 20px; } -.field p { - margin: 0; -} - .link { color: #308274; } @@ -64,12 +61,12 @@ label, justify-content: center; } -.testBoxFields { +.boxFields { padding: 10px 10px 10px 10px; text-align: center; } -.testBox { +.box { border: 1px solid grey; background-color: #f2f2f2; border-radius: 10px; @@ -78,12 +75,6 @@ label, padding: 30px 20px 10px; } -.note { - margin: 0; - color: #666666; - -} - .actionResultBox { padding: 10px 10px 10px 10px; background-color: greenyellow; @@ -115,4 +106,4 @@ label, border: 1px solid #308274; margin: 7px 0px 7px 0px; padding: 9px 184px 9px 20px; -} +} \ No newline at end of file diff --git a/front-end-tools/src/reading_from_blockchain.ts b/front-end-tools/src/reading_from_blockchain.ts new file mode 100644 index 00000000..bde89fa8 --- /dev/null +++ b/front-end-tools/src/reading_from_blockchain.ts @@ -0,0 +1,198 @@ +import { + toBuffer, + ConcordiumGRPCClient, + deserializeReceiveReturnValue, + serializeUpdateContractParameters, + ModuleReference, + InvokeContractFailedResult, + RejectedReceive, + AccountAddress, + AccountInfo, +} from '@concordium/web-sdk'; + +import { CONTRACT_SUB_INDEX } from './constants'; + +/** This function gets the contract info of a smart contract index. */ +export async function getContractInfo(rpcClient: ConcordiumGRPCClient | undefined, contractIndex: bigint) { + if (rpcClient === undefined) { + throw new Error(`rpcClient undefined`); + } + if (contractIndex === undefined) { + throw new Error(`Set smart contract index`); + } + + const info = await rpcClient.getInstanceInfo({ index: contractIndex, subindex: CONTRACT_SUB_INDEX }); + + // Removing the `init_` prefix. + const contractName = info.name.substring(5); + + // Removing the `contractName.` prefix. + const methods = info.methods.map((element) => element.substring(contractName.length + 1)); + + const returnValue = { contractName, methods, sourceModule: info.sourceModule }; + return returnValue; +} + +/** This function gets the embedded schema of a module reference. */ +export async function getEmbeddedSchema( + rpcClient: ConcordiumGRPCClient | undefined, + moduleRef: ModuleReference | undefined +) { + if (rpcClient === undefined) { + throw new Error(`rpcClient undefined`); + } + if (moduleRef === undefined) { + throw new Error(`Set module ref`); + } + + return rpcClient.getEmbeddedSchema(moduleRef); +} + +/** This function gets the account info and its balance. */ +export function getAccountInfo( + client: ConcordiumGRPCClient, + account: string, + setAccountBalance: (arg0: undefined | string) => void, + setAccountExistsOnNetwork: (arg0: boolean) => void, + setViewErrorAccountInfo: (arg0: undefined | string) => void +) { + client + .getAccountInfo(new AccountAddress(account)) + .then((value: AccountInfo) => { + if (value !== undefined) { + setAccountBalance(value.accountAmount.toString()); + setAccountExistsOnNetwork(true); + } else { + setAccountExistsOnNetwork(false); + } + setViewErrorAccountInfo(undefined); + }) + .catch((e) => { + setAccountBalance(undefined); + setViewErrorAccountInfo((e as Error).message.replaceAll('%20', ' ')); + setAccountExistsOnNetwork(false); + }); +} + +/** This function invokes a smart contract entry point and returns its return_value. + * This function expects that the entry point is a `typical` smart contract view/read/getter function that returns a return_value. + * This function throws an error if the entry point does not return a return_value. + * If the moduleSchema parameter is undefined, the return_value is in raw bytes. + * If a valid moduleSchema is provided, the return_value is deserialized. + */ +export async function read( + rpcClient: ConcordiumGRPCClient | undefined, + contractName: string, + contractIndex: bigint, + entryPoint: string | undefined, + moduleSchema: string | undefined, + inputParameter: string | undefined, + inputParameterType: string | undefined, + hasInputParameter: boolean, + deriveContractInfo: boolean +) { + if (rpcClient === undefined) { + throw new Error(`rpcClient undefined`); + } + + if (entryPoint === undefined) { + throw new Error(`Set entry point name`); + } + + let param = toBuffer('', 'hex'); + + if (hasInputParameter) { + if (!deriveContractInfo && moduleSchema === undefined) { + throw new Error(`Set schema`); + } else if (deriveContractInfo && moduleSchema === undefined) { + throw new Error(`No embedded module schema found in module`); + } + + if (inputParameterType === undefined) { + throw new Error(`Select input parameter type`); + } + + let inputParameterFormated; + + if (inputParameter === undefined) { + throw new Error(`Set input parameter`); + } + + switch (inputParameterType) { + case 'number': + inputParameterFormated = Number(inputParameter); + break; + case 'string': + inputParameterFormated = inputParameter; + break; + case 'object': + inputParameterFormated = JSON.parse(inputParameter); + break; + case 'array': + inputParameterFormated = JSON.parse(inputParameter); + break; + default: + throw new Error(`InputParameterType does not exist`); + } + + if (moduleSchema !== undefined) { + param = serializeUpdateContractParameters( + contractName, + entryPoint, + inputParameterFormated, + toBuffer(moduleSchema, 'base64') + ); + } + } + + const res = await rpcClient.invokeContract({ + method: `${contractName}.${entryPoint}`, + contract: { index: contractIndex, subindex: CONTRACT_SUB_INDEX }, + parameter: param, + }); + + if (!res || res.tag === 'failure') { + const rejectReason = JSON.stringify( + ((res as InvokeContractFailedResult)?.reason as RejectedReceive)?.rejectReason + ); + + throw new Error( + `RPC call 'invokeContract' on method '${contractName}.${entryPoint}' of contract '${contractIndex}' failed. + ${rejectReason !== undefined ? `Reject reason: ${rejectReason}` : ''}` + ); + } + if (!res.returnValue) { + throw new Error( + `RPC call 'invokeContract' on method '${contractName}.${entryPoint}' of contract '${contractIndex}' returned no return_value` + ); + } + + if (moduleSchema === undefined) { + // If no schema is provided return the raw bytes + return JSON.stringify(res.returnValue); + } + + let returnValue; + + try { + // If schema is provided deserialize return value + returnValue = deserializeReceiveReturnValue( + toBuffer(res.returnValue, 'hex'), + toBuffer(moduleSchema, 'base64'), + contractName, + entryPoint + ); + } catch (e) { + throw new Error( + `Deserializing the returnValue from the '${contractName}.${entryPoint}' method of contract '${contractIndex}' failed. Original error: ${e}` + ); + } + + if (returnValue === undefined) { + throw new Error( + `Deserializing the returnValue from the '${contractName}.${entryPoint}' method of contract '${contractIndex}' failed.` + ); + } else { + return JSON.stringify(returnValue); + } +} diff --git a/front-end-tools/src/utils.ts b/front-end-tools/src/utils.ts new file mode 100644 index 00000000..5faf2e4c --- /dev/null +++ b/front-end-tools/src/utils.ts @@ -0,0 +1,22 @@ +import { EXAMPLE_ARRAYS, EXAMPLE_JSON_OBJECT } from './constants'; + +export function arraysEqual(a: Uint8Array, b: Uint8Array) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (let i = 0; i < a.length; i += 1) { + if (a[i] !== b[i]) return false; + } + return true; +} + +export function getObjectExample(template: string | undefined) { + return template !== undefined + ? JSON.stringify(JSON.parse(template), undefined, 2) + : JSON.stringify(EXAMPLE_JSON_OBJECT, undefined, 2); +} + +export function getArrayExample(template: string | undefined) { + return template !== undefined ? JSON.stringify(JSON.parse(template), undefined, 2) : EXAMPLE_ARRAYS; +} diff --git a/front-end-tools/src/writing_to_blockchain.ts b/front-end-tools/src/writing_to_blockchain.ts index 86e59ecf..7af43493 100644 --- a/front-end-tools/src/writing_to_blockchain.ts +++ b/front-end-tools/src/writing_to_blockchain.ts @@ -1,17 +1,20 @@ -import { createContext } from 'react'; import { AccountTransactionType, CcdAmount, DeployModulePayload, InitContractPayload, ModuleReference, + UpdateContractPayload, toBuffer, } from '@concordium/web-sdk'; import { WalletConnection } from '@concordium/react-components'; import { moduleSchemaFromBase64 } from '@concordium/wallet-connectors'; +import { CONTRACT_SUB_INDEX } from './constants'; -export async function deploy(connection: WalletConnection, account: string, base64Module: string) { - if (base64Module === '') { +/** This function signs and sends a `DeployModule` transaction. + */ +export async function deploy(connection: WalletConnection, account: string, base64Module: string | undefined) { + if (base64Module === undefined) { throw new Error(`Upload a smart contract module first`); } @@ -20,74 +23,167 @@ export async function deploy(connection: WalletConnection, account: string, base } as DeployModulePayload); } +/** This function signs and sends an `Update` transaction. + * If the transaction should include an input parameter, `hasInputParameter` needs to be true + * and the `inputParameter`, its `inputParameterType`, and the contract `moduleSchema` have to be provided. + */ +export async function update( + connection: WalletConnection, + account: string, + inputParameter: string | undefined, + contractName: string, + entryPoint: string | undefined, + hasInputParameter: boolean, + deriveContractInfo: boolean, + moduleSchema: string | undefined, + inputParameterType: string | undefined, + maxContractExecutionEnergy: bigint, + contractIndex: bigint, + amount: bigint +) { + if (entryPoint === undefined) { + throw new Error(`Set entry point name`); + } + + let schema; + + if (hasInputParameter) { + if (!deriveContractInfo && moduleSchema === undefined) { + throw new Error(`Set schema`); + } else if (deriveContractInfo && moduleSchema === undefined) { + throw new Error(`No embedded module schema found in module`); + } + + if (moduleSchema !== undefined) { + if (inputParameterType === undefined) { + throw new Error(`InputParameterType is undefined`); + } + + if (inputParameter === undefined) { + throw new Error(`Set input parameter`); + } + + switch (inputParameterType) { + case 'number': + schema = { + parameters: Number(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'string': + schema = { + parameters: inputParameter, + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'object': + schema = { + parameters: JSON.parse(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'array': + schema = { + parameters: JSON.parse(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + default: + throw new Error(`Input paramter type option does not exist`); + } + } + } + + return connection.signAndSendTransaction( + account, + AccountTransactionType.Update, + { + amount: new CcdAmount(amount), + address: { index: contractIndex, subindex: CONTRACT_SUB_INDEX }, + receiveName: `${contractName}.${entryPoint}`, + maxContractExecutionEnergy, + } as UpdateContractPayload, + schema + ); +} + +/** This function signs and sends an `InitContract` transaction. + * If the transaction should include an input parameter, + * `hasInputParameter` needs to be true and the `inputParameter`, + * its `inputParameterType`, and the contract `moduleSchema` have to be provided. + */ export async function initialize( connection: WalletConnection, account: string, moduleReferenceAlreadyDeployed: boolean, - moduleReference: string, - inputParameter: string, - contractName: string, + moduleReference: string | undefined, + inputParameter: string | undefined, + contractName: string | undefined, hasInputParameter: boolean, useModuleFromStep1: boolean, - moduleSchema: string, - dropDown: string, - maxContractExecutionEnergy: string, - amount?: string + moduleSchema: string | undefined, + inputParamterType: string | undefined, + maxContractExecutionEnergy: bigint, + amount: bigint ) { if (moduleReferenceAlreadyDeployed === false) { - throw new Error(`Module reference does not exist on chain. First, deploy your module in step 1.`); + throw new Error(`Module reference does not exist on chain. First, deploy your module in step 1 and change/refresh the module reference field in step 2 to remove this error.`); } - if (moduleReference === '') { + if (moduleReference === undefined) { throw new Error(`Set module reference`); } - if (contractName === '') { + if (contractName === undefined) { throw new Error(`Set smart contract name`); } - if (maxContractExecutionEnergy === '') { - throw new Error(`Set max contract execution energy`); - } + let schema; if (hasInputParameter) { - if (!useModuleFromStep1 && moduleSchema === '') { + if (!useModuleFromStep1 && moduleSchema === undefined) { throw new Error(`Set schema`); - } else if (useModuleFromStep1 && moduleSchema === '') { + } else if (useModuleFromStep1 && moduleSchema === undefined) { throw new Error(`No embedded module schema found in module`); } - } - let schema; + if (moduleSchema !== undefined) { + if (inputParamterType === undefined) { + throw new Error(`Set input paramter type`); + } - if (hasInputParameter) { - switch (dropDown) { - case 'number': - schema = { - parameters: Number(inputParameter), - schema: moduleSchemaFromBase64(moduleSchema), - }; - break; - case 'string': - schema = { - parameters: inputParameter, - schema: moduleSchemaFromBase64(moduleSchema), - }; - break; - case 'object': - schema = { - parameters: JSON.parse(inputParameter), - schema: moduleSchemaFromBase64(moduleSchema), - }; - break; - case 'array': - schema = { - parameters: JSON.parse(inputParameter), - schema: moduleSchemaFromBase64(moduleSchema), - }; - break; - default: - throw new Error(`Dropdown option does not exist`); + if (inputParameter === undefined) { + throw new Error(`Set input parameter`); + } + + switch (inputParamterType) { + case 'number': + schema = { + parameters: Number(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'string': + schema = { + parameters: inputParameter, + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'object': + schema = { + parameters: JSON.parse(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + case 'array': + schema = { + parameters: JSON.parse(inputParameter), + schema: moduleSchemaFromBase64(moduleSchema), + }; + break; + default: + throw new Error(`Input paramter type does not exist`); + } } } @@ -95,22 +191,12 @@ export async function initialize( account, AccountTransactionType.InitContract, { - amount: new CcdAmount(BigInt(amount ? Number(amount) : 0)), + amount: new CcdAmount(amount), moduleRef: new ModuleReference(moduleReference), initName: contractName, param: toBuffer(''), - maxContractExecutionEnergy: BigInt(maxContractExecutionEnergy), + maxContractExecutionEnergy, } as InitContractPayload, schema ); } - -/** - * Global application state. - */ -export type State = { - isConnected: boolean; - account: string | undefined; -}; - -export const state = createContext({ isConnected: false, account: undefined }); diff --git a/front-end-tools/yarn.lock b/front-end-tools/yarn.lock index 9cb7dac2..cfcdc1b0 100644 --- a/front-end-tools/yarn.lock +++ b/front-end-tools/yarn.lock @@ -107,6 +107,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.16.7": + version: 7.22.15 + resolution: "@babel/helper-module-imports@npm:7.22.15" + dependencies: + "@babel/types": ^7.22.15 + checksum: ecd7e457df0a46f889228f943ef9b4a47d485d82e030676767e6a2fdcbdaa63594d8124d4b55fd160b41c201025aec01fc27580352b1c87a37c9c6f33d116702 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.21.4": version: 7.21.4 resolution: "@babel/helper-module-imports@npm:7.21.4" @@ -157,6 +166,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.18.6, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -164,6 +180,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.20": + version: 7.22.20 + resolution: "@babel/helper-validator-identifier@npm:7.22.20" + checksum: 136412784d9428266bcdd4d91c32bcf9ff0e8d25534a9d94b044f77fe76bc50f941a90319b05aafd1ec04f7d127cd57a179a3716009ff7f3412ef835ada95bdc + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.21.0": version: 7.21.0 resolution: "@babel/helper-validator-option@npm:7.21.0" @@ -202,6 +225,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3": + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb + languageName: node + linkType: hard + "@babel/runtime@npm:^7.20.7": version: 7.22.3 resolution: "@babel/runtime@npm:7.22.3" @@ -260,6 +292,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.22.15": + version: 7.23.0 + resolution: "@babel/types@npm:7.23.0" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.20 + to-fast-properties: ^2.0.0 + checksum: 215fe04bd7feef79eeb4d33374b39909ce9cad1611c4135a4f7fdf41fe3280594105af6d7094354751514625ea92d0875aba355f53e86a92600f290e77b0e604 + languageName: node + linkType: hard + "@concordium/browser-wallet-api-helpers@npm:^2.5.0": version: 2.5.0 resolution: "@concordium/browser-wallet-api-helpers@npm:2.5.0" @@ -403,6 +446,123 @@ __metadata: languageName: node linkType: hard +"@emotion/babel-plugin@npm:^11.11.0": + version: 11.11.0 + resolution: "@emotion/babel-plugin@npm:11.11.0" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/serialize": ^1.1.2 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: 6b363edccc10290f7a23242c06f88e451b5feb2ab94152b18bb8883033db5934fb0e421e2d67d09907c13837c21218a3ac28c51707778a54d6cd3706c0c2f3f9 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.11.0, @emotion/cache@npm:^11.4.0": + version: 11.11.0 + resolution: "@emotion/cache@npm:11.11.0" + dependencies: + "@emotion/memoize": ^0.8.1 + "@emotion/sheet": ^1.2.2 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 + stylis: 4.2.0 + checksum: 8eb1dc22beaa20c21a2e04c284d5a2630a018a9d51fb190e52de348c8d27f4e8ca4bbab003d68b4f6cd9cc1c569ca747a997797e0f76d6c734a660dc29decf08 + languageName: node + linkType: hard + +"@emotion/hash@npm:^0.9.1": + version: 0.9.1 + resolution: "@emotion/hash@npm:0.9.1" + checksum: 716e17e48bf9047bf9383982c071de49f2615310fb4e986738931776f5a823bc1f29c84501abe0d3df91a3803c80122d24e28b57351bca9e01356ebb33d89876 + languageName: node + linkType: hard + +"@emotion/memoize@npm:^0.8.1": + version: 0.8.1 + resolution: "@emotion/memoize@npm:0.8.1" + checksum: a19cc01a29fcc97514948eaab4dc34d8272e934466ed87c07f157887406bc318000c69ae6f813a9001c6a225364df04249842a50e692ef7a9873335fbcc141b0 + languageName: node + linkType: hard + +"@emotion/react@npm:^11.8.1": + version: 11.11.1 + resolution: "@emotion/react@npm:11.11.1" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.11.0 + "@emotion/cache": ^11.11.0 + "@emotion/serialize": ^1.1.2 + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 + "@emotion/utils": ^1.2.1 + "@emotion/weak-memoize": ^0.3.1 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: aec3c36650f5f0d3d4445ff44d73dd88712b1609645b6af3e6d08049cfbc51f1785fe13dea1a1d4ab1b0800d68f2339ab11e459687180362b1ef98863155aae5 + languageName: node + linkType: hard + +"@emotion/serialize@npm:^1.1.2": + version: 1.1.2 + resolution: "@emotion/serialize@npm:1.1.2" + dependencies: + "@emotion/hash": ^0.9.1 + "@emotion/memoize": ^0.8.1 + "@emotion/unitless": ^0.8.1 + "@emotion/utils": ^1.2.1 + csstype: ^3.0.2 + checksum: 413c352e657f1b5e27ea6437b3ef7dcc3860669b7ae17fd5c18bfbd44e033af1acc56b64d252284a813ca4f3b3e1b0841c42d3fb08e02d2df56fd3cd63d72986 + languageName: node + linkType: hard + +"@emotion/sheet@npm:^1.2.2": + version: 1.2.2 + resolution: "@emotion/sheet@npm:1.2.2" + checksum: d973273c9c15f1c291ca2269728bf044bd3e92a67bca87943fa9ec6c3cd2b034f9a6bfe95ef1b5d983351d128c75b547b43ff196a00a3875f7e1d269793cecfe + languageName: node + linkType: hard + +"@emotion/unitless@npm:^0.8.1": + version: 0.8.1 + resolution: "@emotion/unitless@npm:0.8.1" + checksum: 385e21d184d27853bb350999471f00e1429fa4e83182f46cd2c164985999d9b46d558dc8b9cc89975cb337831ce50c31ac2f33b15502e85c299892e67e7b4a88 + languageName: node + linkType: hard + +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": + version: 1.0.1 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" + peerDependencies: + react: ">=16.8.0" + checksum: 700b6e5bbb37a9231f203bb3af11295eed01d73b2293abece0bc2a2237015e944d7b5114d4887ad9a79776504aa51ed2a8b0ddbc117c54495dd01a6b22f93786 + languageName: node + linkType: hard + +"@emotion/utils@npm:^1.2.1": + version: 1.2.1 + resolution: "@emotion/utils@npm:1.2.1" + checksum: e0b44be0705b56b079c55faff93952150be69e79b660ae70ddd5b6e09fc40eb1319654315a9f34bb479d7f4ec94be6068c061abbb9e18b9778ae180ad5d97c73 + languageName: node + linkType: hard + +"@emotion/weak-memoize@npm:^0.3.1": + version: 0.3.1 + resolution: "@emotion/weak-memoize@npm:0.3.1" + checksum: b2be47caa24a8122622ea18cd2d650dbb4f8ad37b636dc41ed420c2e082f7f1e564ecdea68122b546df7f305b159bf5ab9ffee872abd0f052e687428459af594 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.14.54": version: 0.14.54 resolution: "@esbuild/linux-loong64@npm:0.14.54" @@ -452,6 +612,32 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.4.2": + version: 1.5.0 + resolution: "@floating-ui/core@npm:1.5.0" + dependencies: + "@floating-ui/utils": ^0.1.3 + checksum: 54b4fe26b3c228746ac5589f97303abf158b80aa5f8b99027259decd68d1c2030c4c637648ebd33dfe78a4212699453bc2bd7537fd5a594d3bd3e63d362f666f + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.0.1": + version: 1.5.3 + resolution: "@floating-ui/dom@npm:1.5.3" + dependencies: + "@floating-ui/core": ^1.4.2 + "@floating-ui/utils": ^0.1.3 + checksum: 00053742064aac70957f0bd5c1542caafb3bfe9716588bfe1d409fef72a67ed5e60450d08eb492a77f78c22ed1ce4f7955873cc72bf9f9caf2b0f43ae3561c21 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.1.3": + version: 0.1.6 + resolution: "@floating-ui/utils@npm:0.1.6" + checksum: b34d4b5470869727f52e312e08272edef985ba5a450a76de0917ba0a9c6f5df2bdbeb99448e2c60f39b177fb8981c772ff1831424e75123471a27ebd5b52c1eb + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -1221,6 +1407,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.0": + version: 4.4.8 + resolution: "@types/react-transition-group@npm:4.4.8" + dependencies: + "@types/react": "*" + checksum: ad7ba2bce97631fda9d89b4ed9772489bd050fec3ccd7563041b206dbe219d37d22e0d7731b1f90f56e89daf40e69ba16beba8066c42165bf8a584533feb6a2c + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.4.5": version: 4.4.6 resolution: "@types/react-transition-group@npm:4.4.6" @@ -2116,6 +2311,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-macros@npm:^3.1.0": + version: 3.1.0 + resolution: "babel-plugin-macros@npm:3.1.0" + dependencies: + "@babel/runtime": ^7.12.5 + cosmiconfig: ^7.0.0 + resolve: ^1.19.0 + checksum: 765de4abebd3e4688ebdfbff8571ddc8cd8061f839bb6c3e550b0344a4027b04c60491f843296ce3f3379fb356cc873d57a9ee6694262547eb822c14a25be9a6 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -2660,7 +2866,7 @@ colors@latest: languageName: node linkType: hard -"convert-source-map@npm:^1.7.0": +"convert-source-map@npm:^1.5.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 @@ -2700,7 +2906,7 @@ cors@latest: languageName: node linkType: hard -"cosmiconfig@npm:^7.0.1, cosmiconfig@npm:^7.1.0": +"cosmiconfig@npm:^7.0.0, cosmiconfig@npm:^7.0.1, cosmiconfig@npm:^7.1.0": version: 7.1.0 resolution: "cosmiconfig@npm:7.1.0" dependencies: @@ -4073,6 +4279,13 @@ cors@latest: languageName: node linkType: hard +"find-root@npm:^1.1.0": + version: 1.1.0 + resolution: "find-root@npm:1.1.0" + checksum: b2a59fe4b6c932eef36c45a048ae8f93c85640212ebe8363164814990ee20f154197505965f3f4f102efc33bfb1cbc26fd17c4a2fc739ebc51b886b137cbefaf + languageName: node + linkType: hard + "find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -4202,6 +4415,8 @@ cors@latest: react: ^18.1.0 react-bootstrap: ^2.7.4 react-dom: ^18.1.0 + react-hook-form: ^7.47.0 + react-select: ^5.7.7 react-switch: ^7.0.0 stylelint: ^14.7.1 stylelint-config-prettier: ^9.0.3 @@ -4264,6 +4479,13 @@ cors@latest: languageName: node linkType: hard +"function-bind@npm:^1.1.2": + version: 1.1.2 + resolution: "function-bind@npm:1.1.2" + checksum: 2b0ff4ce708d99715ad14a6d1f894e2a83242e4a52ccfcefaee5e40050562e5f6dafc1adbb4ce2d4ab47279a45dc736ab91ea5042d843c3c092820dfe032efb1 + languageName: node + linkType: hard + "function.prototype.name@npm:^1.1.5": version: 1.1.5 resolution: "function.prototype.name@npm:1.1.5" @@ -4629,6 +4851,24 @@ cors@latest: languageName: node linkType: hard +"hasown@npm:^2.0.0": + version: 2.0.0 + resolution: "hasown@npm:2.0.0" + dependencies: + function-bind: ^1.1.2 + checksum: 6151c75ca12554565098641c98a40f4cc86b85b0fd5b6fe92360967e4605a4f9610f7757260b4e8098dd1c2ce7f4b095f2006fe72a570e3b6d2d28de0298c176 + languageName: node + linkType: hard + +"hoist-non-react-statics@npm:^3.3.1": + version: 3.3.2 + resolution: "hoist-non-react-statics@npm:3.3.2" + dependencies: + react-is: ^16.7.0 + checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -4978,6 +5218,15 @@ cors@latest: languageName: node linkType: hard +"is-core-module@npm:^2.13.0": + version: 2.13.1 + resolution: "is-core-module@npm:2.13.1" + dependencies: + hasown: ^2.0.0 + checksum: 256559ee8a9488af90e4bad16f5583c6d59e92f0742e9e8bb4331e758521ee86b810b93bae44f390766ffbc518a0488b18d9dab7da9a5ff997d499efc9403f7c + languageName: node + linkType: hard + "is-data-descriptor@npm:^0.1.4": version: 0.1.4 resolution: "is-data-descriptor@npm:0.1.4" @@ -5770,6 +6019,13 @@ cors@latest: languageName: node linkType: hard +"memoize-one@npm:^6.0.0": + version: 6.0.0 + resolution: "memoize-one@npm:6.0.0" + checksum: f185ea69f7cceae5d1cb596266dcffccf545e8e7b4106ec6aa93b71ab9d16460dd118ac8b12982c55f6d6322fcc1485de139df07eacffaae94888b9b3ad7675f + languageName: node + linkType: hard + "meow@npm:^9.0.0": version: 9.0.0 resolution: "meow@npm:9.0.0" @@ -6785,7 +7041,7 @@ opn@latest: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -6940,7 +7196,16 @@ proxy-middleware@latest: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.3.2": +"react-hook-form@npm:^7.47.0": + version: 7.47.0 + resolution: "react-hook-form@npm:7.47.0" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: dec192fec9c54e436f9e47008635dd7849b6b119ed477a9b0cd491367a0b2ced3427cd937febfb245e1cb7578c863917181d903eff4519c2787bf713ec7d3426 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1, react-is@npm:^16.3.2, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -6954,6 +7219,26 @@ proxy-middleware@latest: languageName: node linkType: hard +"react-select@npm:^5.7.7": + version: 5.7.7 + resolution: "react-select@npm:5.7.7" + dependencies: + "@babel/runtime": ^7.12.0 + "@emotion/cache": ^11.4.0 + "@emotion/react": ^11.8.1 + "@floating-ui/dom": ^1.0.1 + "@types/react-transition-group": ^4.4.0 + memoize-one: ^6.0.0 + prop-types: ^15.6.0 + react-transition-group: ^4.3.0 + use-isomorphic-layout-effect: ^1.1.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 6fd0c211d377addba6e6762a614ae674936df39a3f46ec19fd06e7acae8d6cadeb93d4723b10e25eff1ff8235077bae9459f293936334d82b28fe5071081c057 + languageName: node + linkType: hard + "react-switch@npm:^7.0.0": version: 7.0.0 resolution: "react-switch@npm:7.0.0" @@ -6966,7 +7251,7 @@ proxy-middleware@latest: languageName: node linkType: hard -"react-transition-group@npm:^4.4.5": +"react-transition-group@npm:^4.3.0, react-transition-group@npm:^4.4.5": version: 4.4.5 resolution: "react-transition-group@npm:4.4.5" dependencies: @@ -7074,6 +7359,13 @@ proxy-middleware@latest: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: 1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3 + languageName: node + linkType: hard + "regex-not@npm:^1.0.0, regex-not@npm:^1.0.2": version: 1.0.2 resolution: "regex-not@npm:1.0.2" @@ -7178,6 +7470,19 @@ proxy-middleware@latest: languageName: node linkType: hard +"resolve@npm:^1.19.0": + version: 1.22.8 + resolution: "resolve@npm:1.22.8" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: f8a26958aa572c9b064562750b52131a37c29d072478ea32e129063e2da7f83e31f7f11e7087a18225a8561cfe8d2f0df9dbea7c9d331a897571c0a2527dbb4c + languageName: node + linkType: hard + "resolve@npm:^2.0.0-next.4": version: 2.0.0-next.4 resolution: "resolve@npm:2.0.0-next.4" @@ -7204,6 +7509,19 @@ proxy-middleware@latest: languageName: node linkType: hard +"resolve@patch:resolve@^1.19.0#~builtin": + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=07638b" + dependencies: + is-core-module: ^2.13.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 5479b7d431cacd5185f8db64bfcb7286ae5e31eb299f4c4f404ad8aa6098b77599563ac4257cb2c37a42f59dfc06a1bec2bcf283bb448f319e37f0feb9a09847 + languageName: node + linkType: hard + "resolve@patch:resolve@^2.0.0-next.4#~builtin": version: 2.0.0-next.4 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin::version=2.0.0-next.4&hash=07638b" @@ -7603,7 +7921,7 @@ send@latest: languageName: node linkType: hard -"source-map@npm:^0.5.6": +"source-map@npm:^0.5.6, source-map@npm:^0.5.7": version: 0.5.7 resolution: "source-map@npm:0.5.7" checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d @@ -8016,6 +8334,13 @@ send@latest: languageName: node linkType: hard +"stylis@npm:4.2.0": + version: 4.2.0 + resolution: "stylis@npm:4.2.0" + checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -8531,6 +8856,18 @@ send@latest: languageName: node linkType: hard +"use-isomorphic-layout-effect@npm:^1.1.2": + version: 1.1.2 + resolution: "use-isomorphic-layout-effect@npm:1.1.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1"