Skip to content

Commit

Permalink
Added a new API to submit message to a topic (#494)
Browse files Browse the repository at this point in the history
Signed-off-by: Himalayan Dev <kipkap1001@gmail.com>
himalayan-dev authored Jul 11, 2024
1 parent 8416eb2 commit a0c6430
Showing 10 changed files with 442 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*-
*
* 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, SubmitMessageRequestParams } from '../../../types/snap';
import { shouldDisplayReconnectButton, submitMessage } from '../../../utils';
import { Card, SendHelloButton } from '../../base';
import ExternalAccount, {
GetExternalAccountRef,
} from '../../sections/ExternalAccount';

type Props = {
network: string;
mirrorNodeUrl: string;
setAccountInfo: React.Dispatch<React.SetStateAction<Account>>;
};

const CreateSubmitMessage: FC<Props> = ({
network,
mirrorNodeUrl,
setAccountInfo,
}) => {
const [state, dispatch] = useContext(MetaMaskContext);
const [loading, setLoading] = useState(false);
const { showModal } = useModal();
const [topicID, setTopicID] = useState('');
const [message, setMessage] = useState('');

const externalAccountRef = useRef<GetExternalAccountRef>(null);

const handleSubmitMessageClick = async () => {
setLoading(true);
try {
const externalAccountParams =
externalAccountRef.current?.handleGetAccountParams();

const submitMessageParams = {
topicID,
message,
} as SubmitMessageRequestParams;

const response: any = await submitMessage(
network,
mirrorNodeUrl,
submitMessageParams,
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 (
<Card
content={{
title: 'hcs/submitMessage',
description: 'Submit a message to a topic',
form: (
<>
<ExternalAccount ref={externalAccountRef} />
<label>
Topic ID:
<input
type="text"
style={{ width: '100%' }}
value={topicID}
placeholder="Enter topic ID"
onChange={(e) => setTopicID(e.target.value)}
/>
</label>
<br />
<label>
Message:
<input
type="text"
style={{ width: '100%' }}
value={message}
placeholder="Enter message"
onChange={(e) => setMessage(e.target.value)}
/>
</label>
<br />
</>
),
button: (
<SendHelloButton
buttonText="Submit Message"
onClick={handleSubmitMessageClick}
disabled={!state.installedSnap}
loading={loading}
/>
),
}}
disabled={!state.installedSnap}
fullWidth={
state.isFlask &&
Boolean(state.installedSnap) &&
!shouldDisplayReconnectButton(state.installedSnap)
}
/>
);
};

export { CreateSubmitMessage };
7 changes: 7 additions & 0 deletions packages/hedera-wallet-snap/packages/site/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@
import { useContext, useState } from 'react';
import { Col, Container, Form, Row } from 'react-bootstrap';
import Select from 'react-select';
import { CreateSubmitMessage } from '..//components/cards/hcs/SubmitMessage';
import { Card, InstallFlaskButton } from '../components/base';
import { ApproveAllowance } from '../components/cards/ApproveAllowance';
import { ConnectHederaWalletSnap } from '../components/cards/ConnectHederaWalletSnap';
@@ -208,6 +209,12 @@ const Index = () => {
setAccountInfo={setAccountInfo}
/>

<CreateSubmitMessage
network={currentNetwork.value}
mirrorNodeUrl={mirrorNodeUrl}
setAccountInfo={setAccountInfo}
/>

<GetAccountInfo
network={currentNetwork.value}
mirrorNodeUrl={mirrorNodeUrl}
7 changes: 7 additions & 0 deletions packages/hedera-wallet-snap/packages/site/src/types/snap.ts
Original file line number Diff line number Diff line change
@@ -317,3 +317,10 @@ export type UpdateTopicRequestParams = {
autoRenewPeriod?: number;
autoRenewAccount?: string;
};

export type SubmitMessageRequestParams = {
topicID: string;
message: string;
maxChunks?: number;
chunkSize?: number;
};
31 changes: 31 additions & 0 deletions packages/hedera-wallet-snap/packages/site/src/utils/snap.ts
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ import type {
SignMessageRequestParams,
SignScheduledTxParams,
StakeHbarRequestParams,
SubmitMessageRequestParams,
TransferCryptoRequestParams,
UpdateSmartContractRequestParams,
UpdateTokenFeeScheduleRequestParams,
@@ -1286,4 +1287,34 @@ export const updateTopic = async (
});
};

/**
* Invoke the "submitMessage" method from the snap.
* @param network
* @param mirrorNodeUrl
* @param submitMessageRequestParams
* @param externalAccountparams
*/
export const submitMessage = async (
network: string,
mirrorNodeUrl: string,
submitMessageRequestParams: SubmitMessageRequestParams,
externalAccountparams?: ExternalAccountParams,
) => {
return await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {
snapId: defaultSnapOrigin,
request: {
method: 'hcs/submitMessage',
params: {
network,
mirrorNodeUrl,
...submitMessageRequestParams,
...externalAccountparams,
},
},
},
});
};

export const isLocalSnap = (snapId: string) => snapId.startsWith('local:');
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
"url": "git+https://github.com/hashgraph/hedera-metamask-snaps.git"
},
"source": {
"shasum": "7F6ZMyGJ7JdoSJw0AFaTI3DY6emq2nGBb2cvg9FZ6io=",
"shasum": "f6MWu5L0XcYdpUA7d995EydsVCA5H92u3s1sOMGgZqA=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*-
*
* 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 {
TopicMessageSubmitTransaction,
TransactionReceiptQuery,
} from '@hashgraph/sdk';
import type { TxReceipt } from '../../types/hedera';
import { Utils } from '../../utils/Utils';

export class SubmitMessageCommand {
readonly #topicID: string;

readonly #message: string;

readonly #maxChunks: number | undefined;

readonly #chunkSize: number | undefined;

constructor(
topicID: string,
message: string,
maxChunks?: number,
chunkSize?: number,
) {
this.#topicID = topicID;
this.#message = message;
this.#maxChunks = maxChunks;
this.#chunkSize = chunkSize;
}

public async execute(client: Client): Promise<TxReceipt[]> {
const transaction = new TopicMessageSubmitTransaction()
.setTopicId(this.#topicID)
.setMessage(this.#message);

if (this.#maxChunks) {
transaction.setMaxChunks(this.#maxChunks);
}
if (this.#chunkSize) {
transaction.setChunkSize(this.#chunkSize);
}

const txMessage = await transaction.executeAll(client);

// Initialize an array for receipts
const receipts = [];
for (const tx of txMessage) {
const txReceipt = await new TransactionReceiptQuery()
.setTransactionId(tx.transactionId)
.setIncludeChildren(true)
.execute(client);
receipts.push(Utils.formatTransactionReceipt(txReceipt));
}

return receipts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*-
*
* 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 { divider, heading, text } from '@metamask/snaps-sdk';
import { HederaClientImplFactory } from '../../client/HederaClientImplFactory';
import { SubmitMessageCommand } from '../../commands/hcs/SubmitMessageCommand';
import type { TxReceipt } from '../../types/hedera';
import type { SubmitMessageRequestParams } from '../../types/params';
import type { WalletSnapParams } from '../../types/state';
import { SnapUtils } from '../../utils/SnapUtils';

export class SubmitMessageFacade {
/**
* Submits a message to a topic on the Hedera network.
* @param walletSnapParams - Wallet snap params.
* @param submitMessageParams - Parameters for submitting a message.
* @returns Receipt of the transaction.
*/
public static async submitMessage(
walletSnapParams: WalletSnapParams,
submitMessageParams: SubmitMessageRequestParams,
): Promise<TxReceipt[]> {
const { origin, state } = walletSnapParams;

const { hederaEvmAddress, hederaAccountId, network, mirrorNodeUrl } =
state.currentAccount;

const { topicID, message, maxChunks, chunkSize } = submitMessageParams;

const { privateKey, curve } =
state.accountState[hederaEvmAddress][network].keyStore;

let txReceipts: 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('Submit a message to a topic'),
text(
`Learn more about submitting messages [here](https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/consensus/consensussubmitmessage)`,
),
text(
`You are about to submit a message to the topic with the following parameters:`,
),
divider(),
text(`Topic ID: ${topicID}`),
text(`Message: ${message}`),
);

if (maxChunks !== undefined) {
panelToShow.push(text(`Max Chunks: ${maxChunks}`));
}
if (chunkSize !== undefined) {
panelToShow.push(text(`Chunk Size: ${chunkSize}`));
}

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 SubmitMessageCommand(
topicID,
message,
maxChunks,
chunkSize,
);

txReceipts = await command.execute(hederaClient.getClient());
} catch (error: any) {
const errMessage = 'Error while trying to submit a message';
console.error('Error occurred: %s', errMessage, String(error));
throw rpcErrors.transactionRejected(errMessage);
}

return txReceipts;
}
}
12 changes: 12 additions & 0 deletions packages/hedera-wallet-snap/packages/snap/src/index.ts
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@ import { HederaTransactionsStrategy } from './strategies/HederaTransactionsStrat
import type { StakeHbarRequestParams } from './types/params';
import type { WalletSnapParams } from './types/state';
import { HederaUtils } from './utils/HederaUtils';
import { SubmitMessageFacade } from './facades/hcs/SubmitMessageFacade';

/**
* Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`.
@@ -552,6 +553,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
};
}

case 'hcs/submitMessage': {
HederaUtils.isValidSubmitMessageParams(request.params);
return {
currentAccount: state.currentAccount,
receipt: await SubmitMessageFacade.submitMessage(
walletSnapParams,
request.params,
),
};
}

default:
// Throw a known error to avoid crashing the Snap
throw rpcErrors.methodNotFound(request.method);
7 changes: 7 additions & 0 deletions packages/hedera-wallet-snap/packages/snap/src/types/params.ts
Original file line number Diff line number Diff line change
@@ -260,3 +260,10 @@ export type UpdateTopicRequestParams = {
autoRenewPeriod?: number;
autoRenewAccount?: string;
};

export type SubmitMessageRequestParams = {
topicID: string;
message: string;
maxChunks?: number;
chunkSize?: number;
};
26 changes: 26 additions & 0 deletions packages/hedera-wallet-snap/packages/snap/src/utils/HederaUtils.ts
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ import type {
SignScheduledTxParams,
SmartContractFunctionParameter,
StakeHbarRequestParams,
SubmitMessageRequestParams,
TokenCustomFee,
TransferCryptoRequestParams,
UpdateSmartContractRequestParams,
@@ -2402,6 +2403,31 @@ export class HederaUtils {
this.checkValidNumber(parameter, 'updateTopic', 'expirationTime', false);
}

public static isValidSubmitMessageParams(
params: unknown,
): asserts params is SubmitMessageRequestParams {
if (
params === null ||
_.isEmpty(params) ||
!('topicID' in params) ||
!('message' in params)
) {
console.error(
'Invalid submitMessage Params passed. "topicID" and "message" must be passed as parameters',
);
throw rpcErrors.invalidParams(
'Invalid submitMessage Params passed. "topicID" and "message" must be passed as parameters',
);
}

const parameter = params as SubmitMessageRequestParams;

this.checkValidString(parameter, 'submitMessage', 'topicID', true);
this.checkValidString(parameter, 'submitMessage', 'message', true);
this.checkValidNumber(parameter, 'submitMessage', 'maxChunks', false);
this.checkValidNumber(parameter, 'submitMessage', 'chunkSize', false);
}

public static validHederaNetwork(network: string) {
return isIn(hederaNetworks, network);
}

0 comments on commit a0c6430

Please sign in to comment.