From f28acf43619d152c3f8601abbbea45e0bd98a1c8 Mon Sep 17 00:00:00 2001 From: Kiran Pachhai Date: Mon, 25 Sep 2023 16:13:57 -0400 Subject: [PATCH] Now handling parameters for the snap RPC APIs --- packages/site/package.json | 2 + .../src/components/cards/GetAccountInfo.tsx | 22 ++++- ...HbarToAccountId.tsx => TransferCrypto.tsx} | 45 ++++++--- packages/site/src/pages/index.tsx | 4 +- packages/site/src/utils/snap.ts | 9 +- packages/snap/snap.manifest.json | 2 +- packages/snap/src/index.ts | 17 +++- .../snap/src/rpc/account/getAccountInfo.ts | 39 +++++--- ...ndHbarToAccountId.ts => transferCrypto.ts} | 16 ++- packages/snap/src/services/hedera.ts | 6 +- .../impl/hedera/client/transferCrypto.ts | 21 ++-- packages/snap/src/snap/account.ts | 2 +- packages/snap/src/types/account.ts | 3 +- packages/snap/src/types/params.ts | 2 + packages/snap/src/utils/params.ts | 99 ++++++++++++++++++- yarn.lock | 2 + 16 files changed, 219 insertions(+), 72 deletions(-) rename packages/site/src/components/cards/{SendHbarToAccountId.tsx => TransferCrypto.tsx} (66%) rename packages/snap/src/rpc/account/{sendHbarToAccountId.ts => transferCrypto.ts} (84%) diff --git a/packages/site/package.json b/packages/site/package.json index 9da61cf..ae8cdc3 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -29,6 +29,7 @@ "@metamask/providers": "^9.0.0", "bignumber.js": "^9.1.2", "bootstrap": "^5.3.2", + "lodash": "^4.17.21", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.2.0", @@ -47,6 +48,7 @@ "@testing-library/user-event": "^13.5.0", "@types/bootstrap": "^5.2.7", "@types/jest": "^29.5.0", + "@types/lodash.clonedeep": "^4.5.7", "@types/react": "^18.0.15", "@types/react-bootstrap": "^0.32.32", "@types/react-dom": "^18.0.6", diff --git a/packages/site/src/components/cards/GetAccountInfo.tsx b/packages/site/src/components/cards/GetAccountInfo.tsx index e64cfaf..1517bef 100644 --- a/packages/site/src/components/cards/GetAccountInfo.tsx +++ b/packages/site/src/components/cards/GetAccountInfo.tsx @@ -12,9 +12,7 @@ import { } from '../../utils'; import { hederaNetworks } from '../../utils/hedera'; import { Card, SendHelloButton } from '../base'; -import ExternalAccount, { - GetExternalAccountRef, -} from '../sections/ExternalAccount'; +import { GetExternalAccountRef } from '../sections/ExternalAccount'; type Props = { setCurrentNetwork: React.Dispatch>; @@ -30,6 +28,7 @@ const GetAccountInfo: FC = ({ const [state, dispatch] = useContext(MetaMaskContext); const [loading, setLoading] = useState(false); const { showModal } = useModal(); + const [accountId, setAccountId] = useState(''); const externalAccountRef = useRef(null); @@ -46,6 +45,7 @@ const GetAccountInfo: FC = ({ const response: any = await getAccountInfo( network, + accountId, externalAccountParams, ); @@ -68,7 +68,21 @@ const GetAccountInfo: FC = ({ content={{ title: 'getAccountInfo', description: 'Get the current account information', - form: , + form: ( + <> + {/* */} + + + ), button: ( >; @@ -22,7 +20,7 @@ type Props = { setAccountInfo: React.Dispatch>; }; -const SendHbarToAccountId: FC = ({ +const TransferCrypto: FC = ({ setCurrentNetwork, setMetamaskAddress, setAccountInfo, @@ -30,10 +28,11 @@ const SendHbarToAccountId: FC = ({ const [state, dispatch] = useContext(MetaMaskContext); const [loading, setLoading] = useState(false); const { showModal } = useModal(); + const [sendToAddress, setSendToAddress] = useState(''); const externalAccountRef = useRef(null); - const handleSendHbarToAccountIdClick = async () => { + const handleTransferCryptoClick = async () => { setLoading(true); try { const network = hederaNetworks.get('testnet') as string; @@ -45,17 +44,19 @@ const SendHbarToAccountId: FC = ({ externalAccountRef.current?.handleGetAccountParams(); // 1 ℏ + // 0.0.633893 + // 0x7d871f006d97498ea338268a956af94ab2e65cdd const transfers: SimpleTransfer[] = [ { asset: 'HBAR', - to: '0.0.633893', + to: sendToAddress, amount: 0.01, } as SimpleTransfer, ]; const memo = ''; - // const maxFee: BigNumber = new BigNumber(1); // Note that this value is in tinybars and if you don't pass it, default is 1 HBAR + // const maxFee = 1; // Note that this value is in tinybars and if you don't pass it, default is 1 HBAR - const response: any = await sendHbarToAccountId( + const response: any = await transferCrypto( network, transfers, memo, @@ -80,13 +81,29 @@ const SendHbarToAccountId: FC = ({ return ( , + title: 'sendHbar', + description: + 'Send HBAR to another account(can pass in Account Id or EVM address but not both)', + form: ( + <> + {/* */} + +
+ + ), button: ( @@ -102,4 +119,4 @@ const SendHbarToAccountId: FC = ({ ); }; -export { SendHbarToAccountId }; +export { TransferCrypto }; diff --git a/packages/site/src/pages/index.tsx b/packages/site/src/pages/index.tsx index c0aa64c..1362524 100644 --- a/packages/site/src/pages/index.tsx +++ b/packages/site/src/pages/index.tsx @@ -4,9 +4,9 @@ import { Card, InstallFlaskButton } from '../components/base'; import { ConnectPulseSnap } from '../components/cards/ConnectPulseSnap'; import { GetAccountInfo } from '../components/cards/GetAccountInfo'; import { ReconnectPulseSnap } from '../components/cards/ReconnectPulseSnap'; -import { SendHbarToAccountId } from '../components/cards/SendHbarToAccountId'; import { SendHelloHessage } from '../components/cards/SendHelloMessage'; import { Todo } from '../components/cards/Todo'; +import { TransferCrypto } from '../components/cards/TransferCrypto'; import { CardContainer, ErrorMessage, @@ -107,7 +107,7 @@ const Index = () => { setAccountInfo={setAccountInfo} /> - { export const getAccountInfo = async ( network: string, + accountId?: string, externalAccountparams?: ExternalAccountParams, ) => { return await window.ethereum.request({ @@ -110,7 +111,7 @@ export const getAccountInfo = async ( snapId: defaultSnapOrigin, request: { method: 'getAccountInfo', - params: { network, ...externalAccountparams }, + params: { network, accountId, ...externalAccountparams }, }, }, }); @@ -137,10 +138,10 @@ export const getAccountBalance = async ( }; /** - * Invoke the "sendHbarToAccountId" method from the snap. + * Invoke the "transferCrypto" method from the snap. */ -export const sendHbarToAccountId = async ( +export const transferCrypto = async ( network: string, transfers: SimpleTransfer[], memo?: string, @@ -152,7 +153,7 @@ export const sendHbarToAccountId = async ( params: { snapId: defaultSnapOrigin, request: { - method: 'sendHbarToAccountId', + method: 'transferCrypto', params: { network, transfers, memo, maxFee, ...externalAccountparams }, }, }, diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index a19a1ab..b0f0198 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/tuum-tech/hedera-pulse.git" }, "source": { - "shasum": "gJTZMzOaJpk9WUq2jmiaVJ1S75KIF6t0JbcP4LwpHgM=", + "shasum": "4HzdG2v/QPI7U/K81PfXpwj1SYIs6caC/g7U0mOwods=", "location": { "npm": { "filePath": "dist/snap.js", diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 40475dd..10650b7 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -4,12 +4,15 @@ import { panel, text } from '@metamask/snaps-ui'; import _ from 'lodash'; import { getAccountBalance } from './rpc/account/getAccountBalance'; import { getAccountInfo } from './rpc/account/getAccountInfo'; -import { sendHbarToAccountId } from './rpc/account/sendHbarToAccountId'; +import { transferCrypto } from './rpc/account/transferCrypto'; import { setCurrentAccount } from './snap/account'; import { getSnapStateUnchecked } from './snap/state'; import { PulseSnapParams } from './types/state'; import { init } from './utils/init'; -import { isValidTransferCryptoParams } from './utils/params'; +import { + isValidGetAccountInfoRequest, + isValidTransferCryptoParams, +} from './utils/params'; /** * Handle incoming JSON-RPC requests, sent through `wallet_invokeSnap`. @@ -75,9 +78,13 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ currentAccount: state.currentAccount, }; case 'getAccountInfo': { + isValidGetAccountInfoRequest(request.params); return { currentAccount: state.currentAccount, - accountInfo: await getAccountInfo(pulseSnapParams), + accountInfo: await getAccountInfo( + pulseSnapParams, + request.params.accountId, + ), }; } case 'getAccountBalance': { @@ -86,11 +93,11 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ accountBalance: await getAccountBalance(pulseSnapParams), }; } - case 'sendHbarToAccountId': { + case 'transferCrypto': { isValidTransferCryptoParams(request.params); return { currentAccount: state.currentAccount, - record: await sendHbarToAccountId(pulseSnapParams, request.params), + record: await transferCrypto(pulseSnapParams, request.params), }; } default: diff --git a/packages/snap/src/rpc/account/getAccountInfo.ts b/packages/snap/src/rpc/account/getAccountInfo.ts index bf07e97..f51d4ba 100644 --- a/packages/snap/src/rpc/account/getAccountInfo.ts +++ b/packages/snap/src/rpc/account/getAccountInfo.ts @@ -1,21 +1,26 @@ +import _ from 'lodash'; +import { HederaAccountInfo } from 'src/services/hedera'; import { createHederaClient } from '../../snap/account'; import { updateSnapState } from '../../snap/state'; -import { AccountInfo } from '../../types/account'; import { PulseSnapParams } from '../../types/state'; /** * Get account info such as address, did, public key, etc. * * @param pulseSnapParams - Pulse snap params. + * @param accountId - Hedera Account Id. * @returns Account Info. */ export async function getAccountInfo( pulseSnapParams: PulseSnapParams, -): Promise { + accountId?: string, +): Promise { const { state } = pulseSnapParams; const { metamaskAddress, hederaAccountId, network } = state.currentAccount; + let accountInfo = {}; + try { const hederaClient = await createHederaClient( state.accountState[metamaskAddress].keyStore.privateKey, @@ -23,23 +28,27 @@ export async function getAccountInfo( network, ); - const response = await hederaClient.getAccountInfo(hederaAccountId); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - state.accountState[metamaskAddress].accountInfo.balance!.hbars = Number( - response.balance.toString().replace(' ℏ', ''), - ); + let response: HederaAccountInfo; + if (accountId && !_.isEmpty(accountId)) { + response = await hederaClient.getAccountInfo(accountId); + } else { + response = await hederaClient.getAccountInfo(hederaAccountId); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + state.accountState[metamaskAddress].accountInfo.balance!.hbars = Number( + response.balance.toString().replace(' ℏ', ''), + ); - // Let's massage the info we want rather than spitting out everything - state.accountState[metamaskAddress].accountInfo.extraData = JSON.parse( - JSON.stringify(response), - ); - await updateSnapState(snap, state); - // TODO: still need to update state.accountState[metamaskAddress].accountInfo.balance + // Let's massage the info we want rather than spitting out everything + state.accountState[metamaskAddress].accountInfo.extraData = JSON.parse( + JSON.stringify(response), + ); + await updateSnapState(snap, state); + } + accountInfo = JSON.parse(JSON.stringify(response)); } catch (error: any) { console.error(`Error while trying to get account info: ${String(error)}`); throw new Error(`Error while trying to get account info: ${String(error)}`); } - return state.accountState[metamaskAddress].accountInfo; + return accountInfo; } diff --git a/packages/snap/src/rpc/account/sendHbarToAccountId.ts b/packages/snap/src/rpc/account/transferCrypto.ts similarity index 84% rename from packages/snap/src/rpc/account/sendHbarToAccountId.ts rename to packages/snap/src/rpc/account/transferCrypto.ts index 5dd38d6..d2f9553 100644 --- a/packages/snap/src/rpc/account/sendHbarToAccountId.ts +++ b/packages/snap/src/rpc/account/transferCrypto.ts @@ -1,4 +1,8 @@ -import { AccountBalance, TxRecord } from '../../services/hedera'; +import { + AccountBalance, + SimpleTransfer, + TxRecord, +} from '../../services/hedera'; import { createHederaClient } from '../../snap/account'; import { TransferCryptoRequestParams } from '../../types/params'; import { PulseSnapParams } from '../../types/state'; @@ -10,13 +14,17 @@ import { PulseSnapParams } from '../../types/state'; * @param transferCryptoParams - Parameters for transferring crypto. * @returns Account Info. */ -export async function sendHbarToAccountId( +export async function transferCrypto( pulseSnapParams: PulseSnapParams, transferCryptoParams: TransferCryptoRequestParams, ): Promise { const { state } = pulseSnapParams; - const { transfers, memo = null, maxFee = null } = transferCryptoParams; + const { + transfers = [] as SimpleTransfer[], + memo = null, + maxFee = null, + } = transferCryptoParams; const { metamaskAddress, hederaAccountId, network } = state.currentAccount; @@ -45,7 +53,5 @@ export async function sendHbarToAccountId( throw new Error(`Error while trying to transfer crypto: ${String(error)}`); } - console.log('record: ', JSON.stringify(record, null, 4)); - return record; } diff --git a/packages/snap/src/services/hedera.ts b/packages/snap/src/services/hedera.ts index 3265bbf..518e2d7 100644 --- a/packages/snap/src/services/hedera.ts +++ b/packages/snap/src/services/hedera.ts @@ -22,10 +22,10 @@ import { Wallet } from '../domain/wallet/abstract'; export type SimpleTransfer = { // HBAR or Token ID (as string) - asset?: string; - to?: string; + asset: string; + to: string; // amount must be in low denom - amount?: number; + amount: number; }; export type Token = { diff --git a/packages/snap/src/services/impl/hedera/client/transferCrypto.ts b/packages/snap/src/services/impl/hedera/client/transferCrypto.ts index 5ac3d32..c1a1414 100644 --- a/packages/snap/src/services/impl/hedera/client/transferCrypto.ts +++ b/packages/snap/src/services/impl/hedera/client/transferCrypto.ts @@ -42,7 +42,7 @@ export async function transferCrypto( for (const transfer of options.transfers) { if (transfer.asset === 'HBAR') { - transaction.addHbarTransfer(transfer.to ?? '', transfer.amount); + transaction.addHbarTransfer(transfer.to, transfer.amount); if (transfer.amount !== undefined) { outgoingHbarAmount += -transfer.amount; } @@ -52,25 +52,18 @@ export async function transferCrypto( if (transfer.amount !== undefined) { const multiplier = Math.pow( 10, - ( - options.currentBalance.tokens.get( - transfer.asset as string, - ) as TokenBalance - )?.decimals, + (options.currentBalance.tokens.get(transfer.asset) as TokenBalance) + ?.decimals, ); amount = transfer.amount * multiplier; } - transaction.addTokenTransfer( - transfer.asset ?? '', - transfer.to ?? '', - amount, - ); + transaction.addTokenTransfer(transfer.asset, transfer.to, amount); const negatedAmount = amount === undefined ? undefined : -amount; transaction.addTokenTransfer( - transfer.asset ?? '', + transfer.asset, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion client.operatorAccountId!, negatedAmount, @@ -86,10 +79,14 @@ export async function transferCrypto( ); } + transaction.freezeWith(client); + const txResponse = await transaction.execute(client); const record: TransactionRecord = await txResponse.getVerboseRecord(client); + console.log('record: ', JSON.stringify(record, null, 4)); + const uint8ArrayToHex = (data: Uint8Array | null | undefined) => { if (!data) { return ''; diff --git a/packages/snap/src/snap/account.ts b/packages/snap/src/snap/account.ts index 27a220d..fd62cec 100644 --- a/packages/snap/src/snap/account.ts +++ b/packages/snap/src/snap/account.ts @@ -158,7 +158,7 @@ export async function importMetaMaskAccount( // eslint-disable-next-line require-atomic-updates state.accountState[metamaskAddress].accountInfo = { alias: accountInfo.alias, - createdTime: accountInfo.created_timestamp, + createdTime: accountInfo.created_timestamp.toDate().toISOString(), memo: accountInfo.memo, balance, // TODO: Run a cronjob occasionally that runs getAccountInfo and getBalance diff --git a/packages/snap/src/types/account.ts b/packages/snap/src/types/account.ts index 0ec6a92..50dcf0b 100644 --- a/packages/snap/src/types/account.ts +++ b/packages/snap/src/types/account.ts @@ -1,4 +1,3 @@ -import { Timestamp } from '@hashgraph/sdk'; import { AccountBalance } from '../services/hedera'; export type Account = { @@ -11,7 +10,7 @@ export type Account = { export type AccountInfo = { alias?: string; - createdTime?: Timestamp; + createdTime?: string; memo?: string; balance?: AccountBalance; extraData?: object; diff --git a/packages/snap/src/types/params.ts b/packages/snap/src/types/params.ts index 515fc99..796981a 100644 --- a/packages/snap/src/types/params.ts +++ b/packages/snap/src/types/params.ts @@ -1,5 +1,7 @@ import { SimpleTransfer } from '../services/hedera'; +export type GetAccountInfoRequestParams = { accountId?: string }; + export type TransferCryptoRequestParams = { transfers: SimpleTransfer[]; memo?: string; diff --git a/packages/snap/src/utils/params.ts b/packages/snap/src/utils/params.ts index 9c25dcc..a0cd552 100644 --- a/packages/snap/src/utils/params.ts +++ b/packages/snap/src/utils/params.ts @@ -1,14 +1,105 @@ -import { TransferCryptoRequestParams } from '../types/params'; +import _ from 'lodash'; +import { + GetAccountInfoRequestParams, + TransferCryptoRequestParams, +} from '../types/params'; /** - * Check Validation of Resolve DID request. + * Check Validation of getAccountInfo request. + * + * @param params - Request params. + */ +export function isValidGetAccountInfoRequest( + params: unknown, +): asserts params is GetAccountInfoRequestParams { + const parameter = params as GetAccountInfoRequestParams; + + if ( + 'accountId' in parameter && + (parameter.accountId === null || typeof parameter.accountId !== 'string') + ) { + console.error( + 'Invalid getAccountInfo Params passed. "accountId" must be a string', + ); + throw new Error( + 'Invalid getAccountInfo Params passed. "accountId" must be a string', + ); + } +} + +/** + * Check Validation of transferCrypto request. * * @param params - Request params. */ export function isValidTransferCryptoParams( params: unknown, ): asserts params is TransferCryptoRequestParams { + if (params === null || _.isEmpty(params) || !('transfers' in params)) { + console.error( + 'Invalid transferCrypto Params passed. "transfers" must be passed as a parameter', + ); + throw new Error( + 'Invalid transferCrypto Params passed. "transfers" must be passed as a parameter', + ); + } + const parameter = params as TransferCryptoRequestParams; - console.log('parameter: ', parameter); - // TODO + + if (parameter.transfers) { + parameter.transfers.forEach((transfer: object) => { + if ( + !('asset' in transfer) || + typeof transfer.asset !== 'string' || + _.isEmpty(transfer.asset) + ) { + console.error( + `Invalid transferCrypto Params passed. "transfers[].asset" is not a string or is empty`, + ); + throw new Error( + `Invalid transferCrypto Params passed. "transfers[].asset" is not a string or is empty`, + ); + } + if ( + !('to' in transfer) || + typeof transfer.to !== 'string' || + _.isEmpty(transfer.to) + ) { + console.error( + `Invalid transferCrypto Params passed. "transfers[].to" is not a string or is empty`, + ); + throw new Error( + `Invalid transferCrypto Params passed. "transfers[].to" is not a string or is empty`, + ); + } + if (!('amount' in transfer) || typeof transfer.amount !== 'number') { + console.error( + `Invalid transferCrypto Params passed. "transfers[].amount" is not a number`, + ); + throw new Error( + `Invalid transferCrypto Params passed. "transfers[].to" is not a number`, + ); + } + }); + } + + // Check if memo is valid + if ('memo' in parameter && typeof parameter.memo !== 'string') { + console.error( + `Invalid transferCrypto Params passed. "memo" is not a string`, + ); + throw new Error( + `Invalid transferCrypto Params passed. "memo" is not a string`, + ); + } + + // Check if maxFee is valid + if ('maxFee' in parameter && typeof parameter.maxFee !== 'number') { + console.error( + `Invalid transferCrypto Params passed. "maxFee" is not a number`, + ); + throw new Error( + `Invalid transferCrypto Params passed. "maxFee" is not a number`, + ); + } } diff --git a/yarn.lock b/yarn.lock index 3872a8b..2deff34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6831,6 +6831,7 @@ __metadata: "@testing-library/user-event": ^13.5.0 "@types/bootstrap": ^5.2.7 "@types/jest": ^29.5.0 + "@types/lodash.clonedeep": ^4.5.7 "@types/react": ^18.0.15 "@types/react-bootstrap": ^0.32.32 "@types/react-dom": ^18.0.6 @@ -6851,6 +6852,7 @@ __metadata: gatsby-plugin-manifest: ^4.24.0 gatsby-plugin-styled-components: ^5.24.0 gatsby-plugin-svgr: ^3.0.0-beta.0 + lodash: ^4.17.21 prettier: ^2.8.7 prettier-plugin-packagejson: ^2.4.3 react: ^18.2.0