From 8416eb2f9bfa9576ecb12174baa1e0eadf9b4dae Mon Sep 17 00:00:00 2001 From: Himalayan Dev <72465553+himalayan-dev@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:13:45 -0400 Subject: [PATCH] Added a new API to update a topic (#493) Signed-off-by: Himalayan Dev --- .../src/components/cards/hcs/UpdateTopic.tsx | 134 +++++++++++++++ .../packages/site/src/pages/index.tsx | 7 + .../packages/site/src/types/snap.ts | 10 ++ .../packages/site/src/utils/snap.ts | 31 ++++ .../packages/snap/snap.manifest.json | 2 +- .../src/commands/hcs/UpdateTopicCommand.ts | 83 +++++++++ .../snap/src/facades/hcs/UpdateTopicFacade.ts | 160 ++++++++++++++++++ .../packages/snap/src/index.ts | 12 ++ .../packages/snap/src/types/params.ts | 10 ++ .../packages/snap/src/utils/HederaUtils.ts | 24 +++ 10 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 packages/hedera-wallet-snap/packages/site/src/components/cards/hcs/UpdateTopic.tsx create mode 100644 packages/hedera-wallet-snap/packages/snap/src/commands/hcs/UpdateTopicCommand.ts create mode 100644 packages/hedera-wallet-snap/packages/snap/src/facades/hcs/UpdateTopicFacade.ts diff --git a/packages/hedera-wallet-snap/packages/site/src/components/cards/hcs/UpdateTopic.tsx b/packages/hedera-wallet-snap/packages/site/src/components/cards/hcs/UpdateTopic.tsx new file mode 100644 index 00000000..07581100 --- /dev/null +++ b/packages/hedera-wallet-snap/packages/site/src/components/cards/hcs/UpdateTopic.tsx @@ -0,0 +1,134 @@ +/*- + * + * Hedera Wallet Snap + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { FC, useContext, useRef, useState } from 'react'; +import { + MetaMaskContext, + MetamaskActions, +} from '../../../contexts/MetamaskContext'; +import useModal from '../../../hooks/useModal'; +import { Account, UpdateTopicRequestParams } from '../../../types/snap'; +import { shouldDisplayReconnectButton, updateTopic } from '../../../utils'; +import { Card, SendHelloButton } from '../../base'; +import ExternalAccount, { + GetExternalAccountRef, +} from '../../sections/ExternalAccount'; + +type Props = { + network: string; + mirrorNodeUrl: string; + setAccountInfo: React.Dispatch>; +}; + +const UpdateTopic: FC = ({ network, mirrorNodeUrl, setAccountInfo }) => { + const [state, dispatch] = useContext(MetaMaskContext); + const [loading, setLoading] = useState(false); + const { showModal } = useModal(); + const [topicID, setTopicID] = useState(''); + const [memo, setMemo] = useState(''); + + const externalAccountRef = useRef(null); + + const handleUpdateTopicClick = async () => { + setLoading(true); + try { + const externalAccountParams = + externalAccountRef.current?.handleGetAccountParams(); + + const updateTopicParams = { + topicID, + memo, + } as UpdateTopicRequestParams; + + const response: any = await updateTopic( + network, + mirrorNodeUrl, + updateTopicParams, + externalAccountParams, + ); + + const { receipt, currentAccount } = response; + + setAccountInfo(currentAccount); + console.log('receipt: ', receipt); + + showModal({ + title: 'Transaction Receipt', + content: JSON.stringify({ receipt }, null, 4), + }); + } catch (e) { + console.error(e); + dispatch({ type: MetamaskActions.SetError, payload: e }); + } + setLoading(false); + }; + + return ( + + + +
+ +
+ + ), + button: ( + + ), + }} + disabled={!state.installedSnap} + fullWidth={ + state.isFlask && + Boolean(state.installedSnap) && + !shouldDisplayReconnectButton(state.installedSnap) + } + /> + ); +}; + +export { UpdateTopic }; diff --git a/packages/hedera-wallet-snap/packages/site/src/pages/index.tsx b/packages/hedera-wallet-snap/packages/site/src/pages/index.tsx index 56c1d6c5..05d1c7b2 100644 --- a/packages/hedera-wallet-snap/packages/site/src/pages/index.tsx +++ b/packages/hedera-wallet-snap/packages/site/src/pages/index.tsx @@ -38,6 +38,7 @@ import Tokens from '../components/cards/Tokens'; import { TransferCrypto } from '../components/cards/TransferCrypto'; import { UnstakeHbar } from '../components/cards/UnstakeHbar'; import { CreateTopic } from '../components/cards/hcs/CreateTopic'; +import { UpdateTopic } from '../components/cards/hcs/UpdateTopic'; import { CallSmartContractFunction } from '../components/cards/hscs/CallSmartContractFunction'; import { CreateSmartContract } from '../components/cards/hscs/CreateSmartContract'; import { DeleteSmartContract } from '../components/cards/hscs/DeleteSmartContract'; @@ -201,6 +202,12 @@ const Index = () => { setAccountInfo={setAccountInfo} /> + + { + return await window.ethereum.request({ + method: 'wallet_invokeSnap', + params: { + snapId: defaultSnapOrigin, + request: { + method: 'hcs/updateTopic', + params: { + network, + mirrorNodeUrl, + ...updateTopicRequestParams, + ...externalAccountparams, + }, + }, + }, + }); +}; + export const isLocalSnap = (snapId: string) => snapId.startsWith('local:'); diff --git a/packages/hedera-wallet-snap/packages/snap/snap.manifest.json b/packages/hedera-wallet-snap/packages/snap/snap.manifest.json index 7c66757b..dfc04ab9 100644 --- a/packages/hedera-wallet-snap/packages/snap/snap.manifest.json +++ b/packages/hedera-wallet-snap/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "git+https://github.com/hashgraph/hedera-metamask-snaps.git" }, "source": { - "shasum": "B7AY3TIR598Hpkjge9P0Cw6ChbU+qAuxaIoFAfuedgk=", + "shasum": "7F6ZMyGJ7JdoSJw0AFaTI3DY6emq2nGBb2cvg9FZ6io=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/hedera-wallet-snap/packages/snap/src/commands/hcs/UpdateTopicCommand.ts b/packages/hedera-wallet-snap/packages/snap/src/commands/hcs/UpdateTopicCommand.ts new file mode 100644 index 00000000..e0f5dcc7 --- /dev/null +++ b/packages/hedera-wallet-snap/packages/snap/src/commands/hcs/UpdateTopicCommand.ts @@ -0,0 +1,83 @@ +/*- + * + * Hedera Wallet Snap + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { Client } from '@hashgraph/sdk'; +import { PublicKey, TopicUpdateTransaction } from '@hashgraph/sdk'; +import type { TxReceipt } from '../../types/hedera'; +import { Utils } from '../../utils/Utils'; + +export class UpdateTopicCommand { + readonly #topicID: string; + + readonly #memo: string | undefined; + + readonly #expirationTime: number | undefined; + + readonly #adminKey: string | undefined; + + readonly #submitKey: string | undefined; + + readonly #autoRenewPeriod: number | undefined; + + readonly #autoRenewAccount: string | undefined; + + constructor( + topicID: string, + memo?: string, + expirationTime?: number, + adminKey?: string, + submitKey?: string, + autoRenewPeriod?: number, + autoRenewAccount?: string, + ) { + this.#topicID = topicID; + this.#memo = memo; + this.#expirationTime = expirationTime; + this.#adminKey = adminKey; + this.#submitKey = submitKey; + this.#autoRenewPeriod = autoRenewPeriod; + this.#autoRenewAccount = autoRenewAccount; + } + + public async execute(client: Client): Promise { + const transaction = new TopicUpdateTransaction().setTopicId(this.#topicID); + + if (this.#memo) { + transaction.setTopicMemo(this.#memo); + } + if (this.#adminKey) { + transaction.setAdminKey(PublicKey.fromString(this.#adminKey)); + } + if (this.#submitKey) { + transaction.setSubmitKey(PublicKey.fromString(this.#submitKey)); + } + if (this.#autoRenewPeriod) { + transaction.setAutoRenewPeriod(this.#autoRenewPeriod); + } + if (this.#autoRenewAccount) { + transaction.setAutoRenewAccountId(this.#autoRenewAccount); + } + if (this.#expirationTime) { + transaction.setExpirationTime(new Date(this.#expirationTime)); + } + + return await Utils.executeTransaction(client, transaction); + } +} diff --git a/packages/hedera-wallet-snap/packages/snap/src/facades/hcs/UpdateTopicFacade.ts b/packages/hedera-wallet-snap/packages/snap/src/facades/hcs/UpdateTopicFacade.ts new file mode 100644 index 00000000..bcf0356b --- /dev/null +++ b/packages/hedera-wallet-snap/packages/snap/src/facades/hcs/UpdateTopicFacade.ts @@ -0,0 +1,160 @@ +/*- + * + * Hedera Wallet Snap + * + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { rpcErrors } from '@metamask/rpc-errors'; +import type { DialogParams, NodeType } from '@metamask/snaps-sdk'; +import { copyable, divider, heading, text } from '@metamask/snaps-sdk'; +import { HederaClientImplFactory } from '../../client/HederaClientImplFactory'; +import { UpdateTopicCommand } from '../../commands/hcs/UpdateTopicCommand'; +import type { TxReceipt } from '../../types/hedera'; +import type { UpdateTopicRequestParams } from '../../types/params'; +import type { WalletSnapParams } from '../../types/state'; +import { SnapUtils } from '../../utils/SnapUtils'; + +export class UpdateTopicFacade { + /** + * Updates a topic on the Hedera network. + * @param walletSnapParams - Wallet snap params. + * @param updateTopicParams - Parameters for updating a topic. + * @returns Receipt of the transaction. + */ + public static async updateTopic( + walletSnapParams: WalletSnapParams, + updateTopicParams: UpdateTopicRequestParams, + ): Promise { + const { origin, state } = walletSnapParams; + + const { hederaEvmAddress, hederaAccountId, network, mirrorNodeUrl } = + state.currentAccount; + + const { + topicID, + memo, + expirationTime, + adminKey, + submitKey, + autoRenewPeriod, + autoRenewAccount, + } = updateTopicParams; + + const { privateKey, curve } = + state.accountState[hederaEvmAddress][network].keyStore; + + let txReceipt = {} as TxReceipt; + try { + const panelToShow: ( + | { + value: string; + type: NodeType.Heading; + } + | { + value: string; + type: NodeType.Text; + markdown?: boolean | undefined; + } + | { + type: NodeType.Divider; + } + | { + value: string; + type: NodeType.Copyable; + sensitive?: boolean | undefined; + } + )[] = []; + + panelToShow.push( + heading('Update a topic'), + text( + `Learn more about updating topics [here](https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/consensus/consensusupdatetopic)`, + ), + text(`You are about to update a topic with the following parameters:`), + divider(), + text(`Topic ID: ${topicID}`), + ); + + if (memo !== undefined) { + panelToShow.push(text(`Memo: ${memo}`)); + } + if (adminKey !== undefined) { + panelToShow.push(text(`Admin Key:`), copyable(adminKey)); + } + if (submitKey !== undefined) { + panelToShow.push(text(`Submit Key:`), copyable(submitKey)); + } + if (autoRenewPeriod !== undefined) { + panelToShow.push(text(`Auto Renew Period: ${autoRenewPeriod}`)); + } + if (autoRenewAccount !== undefined) { + panelToShow.push( + text(`Auto Renew Account:`), + copyable(autoRenewAccount), + ); + } + if (expirationTime !== undefined) { + panelToShow.push(text(`Expiration Time: ${expirationTime}`)); + } + + const dialogParams: DialogParams = { + type: 'confirmation', + content: await SnapUtils.generateCommonPanel( + origin, + network, + mirrorNodeUrl, + panelToShow, + ), + }; + const confirmed = await SnapUtils.snapDialog(dialogParams); + if (!confirmed) { + const errMessage = 'User rejected the transaction'; + console.error(errMessage); + throw rpcErrors.transactionRejected(errMessage); + } + + const hederaClientFactory = new HederaClientImplFactory( + hederaAccountId, + network, + curve, + privateKey, + ); + + const hederaClient = await hederaClientFactory.createClient(); + if (hederaClient === null) { + throw rpcErrors.resourceUnavailable('hedera client returned null'); + } + const command = new UpdateTopicCommand( + topicID, + memo, + expirationTime, + adminKey, + submitKey, + autoRenewPeriod, + autoRenewAccount, + ); + + txReceipt = await command.execute(hederaClient.getClient()); + } catch (error: any) { + const errMessage = 'Error while trying to update a topic'; + console.error('Error occurred: %s', errMessage, String(error)); + throw rpcErrors.transactionRejected(errMessage); + } + + return txReceipt; + } +} diff --git a/packages/hedera-wallet-snap/packages/snap/src/index.ts b/packages/hedera-wallet-snap/packages/snap/src/index.ts index 01aaee08..bb4ff281 100644 --- a/packages/hedera-wallet-snap/packages/snap/src/index.ts +++ b/packages/hedera-wallet-snap/packages/snap/src/index.ts @@ -37,6 +37,7 @@ import { GetAccountInfoFacade } from './facades/account/GetAccountInfoFacade'; import { ApproveAllowanceFacade } from './facades/allowance/ApproveAllowanceFacade'; import { DeleteAllowanceFacade } from './facades/allowance/DeleteAllowanceFacade'; import { CreateTopicFacade } from './facades/hcs/CreateTopicFacade'; +import { UpdateTopicFacade } from './facades/hcs/UpdateTopicFacade'; import { CallSmartContractFunctionFacade } from './facades/hscs/CallSmartContractFunctionFacade'; import { CreateSmartContractFacade } from './facades/hscs/CreateSmartContractFacade'; import { DeleteSmartContractFacade } from './facades/hscs/DeleteSmartContractFacade'; @@ -540,6 +541,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ }; } + case 'hcs/updateTopic': { + HederaUtils.isValidUpdateTopicParams(request.params); + return { + currentAccount: state.currentAccount, + receipt: await UpdateTopicFacade.updateTopic( + walletSnapParams, + request.params, + ), + }; + } + default: // Throw a known error to avoid crashing the Snap throw rpcErrors.methodNotFound(request.method); diff --git a/packages/hedera-wallet-snap/packages/snap/src/types/params.ts b/packages/hedera-wallet-snap/packages/snap/src/types/params.ts index 5d4c1e54..7e9e3583 100644 --- a/packages/hedera-wallet-snap/packages/snap/src/types/params.ts +++ b/packages/hedera-wallet-snap/packages/snap/src/types/params.ts @@ -250,3 +250,13 @@ export type CreateTopicRequestParams = { autoRenewPeriod?: number; autoRenewAccount?: string; }; + +export type UpdateTopicRequestParams = { + topicID: string; + memo?: string; + expirationTime?: number; + adminKey?: string; + submitKey?: string; + autoRenewPeriod?: number; + autoRenewAccount?: string; +}; diff --git a/packages/hedera-wallet-snap/packages/snap/src/utils/HederaUtils.ts b/packages/hedera-wallet-snap/packages/snap/src/utils/HederaUtils.ts index 6c43c247..6a4aaa84 100644 --- a/packages/hedera-wallet-snap/packages/snap/src/utils/HederaUtils.ts +++ b/packages/hedera-wallet-snap/packages/snap/src/utils/HederaUtils.ts @@ -78,6 +78,7 @@ import type { UpdateSmartContractRequestParams, UpdateTokenFeeScheduleRequestParams, UpdateTokenRequestParams, + UpdateTopicRequestParams, WipeTokenRequestParams, } from '../types/params'; import { CryptoUtils } from './CryptoUtils'; @@ -2378,6 +2379,29 @@ export class HederaUtils { this.checkValidString(parameter, 'createTopic', 'autoRenewAccount', false); } + public static isValidUpdateTopicParams( + params: unknown, + ): asserts params is UpdateTopicRequestParams { + if (params === null || _.isEmpty(params) || !('topicID' in params)) { + console.error( + 'Invalid updateTopic Params passed. "topicID" must be passed as a parameter', + ); + throw rpcErrors.invalidParams( + 'Invalid updateTopic Params passed. "topicID" must be passed as a parameter', + ); + } + + const parameter = params as UpdateTopicRequestParams; + + this.checkValidString(parameter, 'updateTopic', 'topicID', true); + this.checkValidString(parameter, 'updateTopic', 'memo', false); + this.checkValidString(parameter, 'updateTopic', 'adminKey', false); + this.checkValidString(parameter, 'updateTopic', 'submitKey', false); + this.checkValidString(parameter, 'updateTopic', 'autoRenewAccount', false); + this.checkValidNumber(parameter, 'updateTopic', 'autoRenewPeriod', false); + this.checkValidNumber(parameter, 'updateTopic', 'expirationTime', false); + } + public static validHederaNetwork(network: string) { return isIn(hederaNetworks, network); }