From ef3307d1a6ad33ba995bd39cb02d394f5c7deca8 Mon Sep 17 00:00:00 2001 From: Idris Bowman Date: Thu, 23 Nov 2023 15:14:43 -0500 Subject: [PATCH 1/4] feat: add entrypoint deposits and update keyring to default to using synchronous request --- packages/site/package.json | 2 + packages/site/src/components/Account.tsx | 2 +- .../site/src/components/ConnectToggle.tsx | 131 +++++++++++++++++ packages/site/src/components/Transaction.tsx | 138 ++++++++---------- packages/site/src/components/index.ts | 1 + packages/site/src/hooks/Accounts.tsx | 33 ++++- packages/site/src/hooks/MetamaskContext.tsx | 34 ++++- packages/site/src/pages/index.tsx | 66 +++++++-- packages/site/src/types/app.ts | 2 - packages/site/src/utils/eth.ts | 60 ++++++++ packages/site/src/utils/snap.ts | 22 +-- packages/snap/snap.manifest.json | 2 +- packages/snap/src/index.ts | 47 ++++-- packages/snap/src/keyring/keyring.ts | 55 +++---- packages/snap/src/keyring/permissions.ts | 3 + packages/snap/src/types/request.types.ts | 7 + scripts/get.sh | 24 --- scripts/prepack.sh | 11 -- yarn.lock | 2 + 19 files changed, 441 insertions(+), 201 deletions(-) create mode 100644 packages/site/src/components/ConnectToggle.tsx delete mode 100755 scripts/get.sh delete mode 100755 scripts/prepack.sh diff --git a/packages/site/package.json b/packages/site/package.json index 2384a33..d3bcc0f 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -31,8 +31,10 @@ }, "dependencies": { "@account-abstraction/contracts": "^0.6.0", + "@ethereumjs/tx": "^4.1.2", "@metamask/keyring-api": "^1.0.0", "@metamask/providers": "^13.0.0", + "@metamask/utils": "^3.3.0", "ethers": "^5.7.0", "react": "^18.2.0", "react-blockies": "1.4.1", diff --git a/packages/site/src/components/Account.tsx b/packages/site/src/components/Account.tsx index 1d664a2..d700310 100644 --- a/packages/site/src/components/Account.tsx +++ b/packages/site/src/components/Account.tsx @@ -1,6 +1,6 @@ import { MetaMaskContext, MetamaskActions, useAcount } from '../hooks'; import styled from 'styled-components'; -import { connectSnap, convertToEth, filterPendingRequests, getMMProvider, getSignedTxs, getSnap, handleCopyToClipboard, storeTxHash, trimAccount } from '../utils'; +import { connectSnap, convertToEth, filterPendingRequests, getMMProvider, getSnap, handleCopyToClipboard, storeTxHash, trimAccount } from '../utils'; import { FaCloudDownloadAlt, FaRegLightbulb } from 'react-icons/fa'; import { InstallFlaskButton, ConnectSnapButton, SimpleButton } from './Buttons'; import { AccountActivity, AccountActivityType, SupportedChainIdMap, UserOperation } from '../types'; diff --git a/packages/site/src/components/ConnectToggle.tsx b/packages/site/src/components/ConnectToggle.tsx new file mode 100644 index 0000000..ea5fce1 --- /dev/null +++ b/packages/site/src/components/ConnectToggle.tsx @@ -0,0 +1,131 @@ +import { useContext, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { MetaMaskContext, MetamaskActions } from '../hooks'; + +type CheckedProps = { + readonly checked: boolean; +}; + +const ToggleWrapper = styled.div` + touch-action: pan-x; + display: inline-block; + position: relative; + cursor: pointer; + cursor: not-allowed; /* Use 'not-allowed' cursor to indicate it's disabled */ + background-color: transparent; + border: 0; + padding: 0; + -webkit-touch-callout: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-tap-highlight-color: transparent; + ${({ theme }) => theme.mediaQueries.small} { + margin-bottom: 2.4rem; + } +`; + +const ToggleInput = styled.input` + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +`; + +const IconContainer = styled.div` + position: absolute; + width: 22px; + height: 22px; + top: 0; + bottom: 0; + margin-top: auto; + margin-bottom: auto; + line-height: 0; + opacity: 0; + transition: opacity 0.25s ease; + & > * { + align-items: center; + display: flex; + height: 20px; + justify-content: center; + position: relative; + width: 12px; + } +`; + +const CheckedContainer = styled(IconContainer)` + opacity: ${({ checked }) => (checked ? 1 : 0)}; + left: 10px; +`; + +const UncheckedContainer = styled(IconContainer)` + opacity: ${({ checked }) => (checked ? 0 : 1)}; + right: 10px; +`; + +const ToggleContainer = styled.div` + width: 62px; + height: 26px; + padding: 0; + border-radius: 36px; + background-color: ${({ checked, theme }) => (checked ? 'green' : theme.colors.background.alternative)}; + transition: all 0.2s ease; +`; +const ToggleCircle = styled.div` + transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; + position: absolute; + top: 4px; + left: ${({ checked }) => (checked ? '36px' : '4px')}; + width: 18px; + height: 18px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.14); + border-radius: 50%; + background-color: #ffffff; + box-sizing: border-box; + transition: all 0.25s ease; +`; + +export const ConnectToggle = () => { + const [state, dispatch] = useContext(MetaMaskContext); + const [checked, setChecked] = useState(false); + + useEffect(() => { + try { + + if (state.connectedAccounts.length === 0) { + setChecked(false); + return; + } + + for (const account of state.connectedAccounts) { + if (account.toLowerCase() === state.selectedSnapKeyringAccount.address.toLowerCase()) { + setChecked(true); + break; + } + setChecked(false); + } + + } catch (e) { + dispatch({ type: MetamaskActions.SetError, payload: e }); + } + }, [state.connectedAccounts, state.selectedSnapKeyringAccount]); + + return ( + + + + + + + + + + + + ); +}; diff --git a/packages/site/src/components/Transaction.tsx b/packages/site/src/components/Transaction.tsx index 5d64c33..e445d73 100644 --- a/packages/site/src/components/Transaction.tsx +++ b/packages/site/src/components/Transaction.tsx @@ -5,10 +5,10 @@ import { CommonInputForm } from './Form'; import { FaRegTimesCircle, FaCheckCircle } from 'react-icons/fa'; import { MetaMaskContext, useAcount } from '../hooks'; import { AccountRequestDisplay } from './Account'; -import { convertToEth, convertToWei, estimateGas, parseChainId, trimAccount } from '../utils/eth'; +import { convertToEth, convertToWei, estimateGas, trimAccount } from '../utils/eth'; import { BlockieAccountModal } from './Blockie-Icon'; import { BigNumber, ethers } from 'ethers'; -import { calcPreVerificationGas, estimatCreationGas, estimateUserOperationGas, getDummySignature, getMMProvider, getSignedTxs, getUserOpCallData, handleCopyToClipboard, notify, storeTxHash } from '../utils'; +import { calcPreVerificationGas, estimatCreationGas, estimateUserOperationGas, getDummySignature, getMMProvider, getUserOpCallData, handleCopyToClipboard, notify, storeTxHash } from '../utils'; import { EntryPoint__factory } from '@account-abstraction/contracts'; import { UserOperation } from '../types'; import { FaCopy } from "react-icons/fa"; @@ -102,16 +102,16 @@ const AccountCopy = styled.div` `; enum Stage { - EnterAmount = 'Enter Amount', - Review = 'Review', - Loading = 'Loading', - Sent = 'Sent', - Failed = 'Failed', + EnterAmount = 'Enter Amount', + Review = 'Review', + Loading = 'Loading', + Success = 'Success', + Failed = 'Failed', } export enum TransactionType { - Deposit = 'Deposit', - Withdraw = 'Withdraw', + Deposit = 'Deposit', + Withdraw = 'Withdraw', } export const EthereumTransactionModalComponent = ({ @@ -121,13 +121,13 @@ export const EthereumTransactionModalComponent = ({ }) => { const [state] = useContext(MetaMaskContext); const [status, setStatus] = useState(Stage.EnterAmount); - const [amount, setAmount] = useState(''); const [failMessage, setFailMessage] = useState('User denied the transaction signature.'); const [successMessage, setSuccessMessage] = useState(''); + + const [amount, setAmount] = useState(''); + const { sendRequestSync, - approveRequest, - rejectRequest, getSmartAccount, getAccountActivity, getKeyringSnapAccounts, @@ -148,31 +148,40 @@ export const EthereumTransactionModalComponent = ({ const feeData = await provider.getFeeData() const encodedFunctionData = entryPointContract.interface.encodeFunctionData('depositTo', [state.scAccount.address]); const estimateGasAmount = await estimateGas( - state.selectedSnapKeyringAccount.address, - state.scAccount.entryPoint, - encodedFunctionData, + state.selectedSnapKeyringAccount.address, + state.scAccount.entryPoint, + encodedFunctionData, ); - // set transation data (eth transaction type 2) - const transactionData = await entryPointContract.populateTransaction.depositTo(state.scAccount.address, - { - // Type 2 Transactions (EIP-1559) - from: state.selectedSnapKeyringAccount.address, - nonce: await provider.getTransactionCount(state.selectedSnapKeyringAccount.address, 'latest'), - gasLimit: estimateGasAmount.toNumber(), - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? BigNumber.from(0), - maxFeePerGas: feeData.maxFeePerGas ?? BigNumber.from(0), - value: depositInWei.toString(), - } - ) - transactionData.chainId = parseChainId(state.chainId) - - // send request to keyring for approval - const result = await sendRequestSync( + // TODO: changeto account before sending transaction + + // send transaction + const res = await getMMProvider().request({ + method: 'eth_sendTransaction', + params: [ + { + from: state.selectedSnapKeyringAccount.address, + to: entryPointContract.address, + value: depositInWei.toHexString(), + gasLimit: estimateGasAmount.toHexString(), + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas?.toHexString() ?? BigNumber.from(0).toHexString(), + maxFeePerGas: feeData.maxFeePerGas?.toHexString() ?? BigNumber.from(0).toHexString(), + data: encodedFunctionData, + } + ], + }) as string + + // show success message + await getSmartAccount(state.selectedSnapKeyringAccount.id); + await storeTxHash( state.selectedSnapKeyringAccount.id, - 'eth_signTransaction', - [state.selectedSnapKeyringAccount.address, 'eoa', transactionData] // [from, type, transactionData] + res, + state.chainId, ); + setAmount(''); + setSuccessMessage(`${amount} ETH successfully depoisted to entry point contract.`); + setStatus(Stage.Success); + notify('Deposit Transaction sent (txHash)', 'View activity for details.', res) } const handleWithdrawSubmit = async () => { @@ -255,10 +264,10 @@ export const EthereumTransactionModalComponent = ({ throw new Error('Invalid transaction type'); } - setStatus(Stage.Review); } catch (e) { + console.error(e.message) setAmount(''); - setFailMessage(e.message); + setFailMessage('Transaction failed to send.'); setStatus(Stage.Failed); } }; @@ -272,51 +281,32 @@ export const EthereumTransactionModalComponent = ({ } } - const handleApproveClick = async (event: any, requestId: string) => { + const handleConfirmUserOpClick = async (event: any) => { try { event.preventDefault(); setStatus(Stage.Loading); + // TODO: Send userOp - // sign request - await approveRequest(requestId); - - // send tx - const signedTxs = await getSignedTxs() - - if (signedTxs[requestId]) { - const provider = new ethers.providers.Web3Provider(getMMProvider() as any); - const res = await provider.sendTransaction(signedTxs[requestId]) - await res.wait(); - - await storeTxHash( - state.selectedSnapKeyringAccount.id, - res.hash, - requestId, - state.chainId, - ); - notify('Transaction confirmed (txHash)', 'View activity for details.', res.hash) - } - - setStatus(Stage.Sent); - setSuccessMessage(`${amount} ETH successfully sent.`); - - await getAccountActivity(state.selectedSnapKeyringAccount.id); + setStatus(Stage.Success); + setSuccessMessage(`${amount} ETH successfully depoisted to entry point contract.`); + // TODO: Add activity + // await getAccountActivity(state.selectedSnapKeyringAccount.id); await getSmartAccount(state.selectedSnapKeyringAccount.id); } catch (e) { - setFailMessage(e.message) - setStatus(Stage.Failed); - await getKeyringSnapAccounts(); + setFailMessage(e.message) + setAmount(''); + setStatus(Stage.Failed); } }; - const handleRejectClick = async (event: any, requestId: string) => { + const handleRejectUserOpClick = async (event: any) => { try { event.preventDefault(); setStatus(Stage.Loading); - await rejectRequest(requestId); - setAmount(''); + setAmount('') setStatus(Stage.EnterAmount); } catch (e) { + setAmount('') setFailMessage(e.message) setStatus(Stage.Failed); } @@ -335,13 +325,13 @@ export const EthereumTransactionModalComponent = ({ (owner) - {trimAccount(state.selectedSnapKeyringAccount.options.owner as string)} + {trimAccount(state.selectedSnapKeyringAccount.address)} handleCopyToClipboard( e, - state.selectedSnapKeyringAccount.options.owner as string, + state.selectedSnapKeyringAccount.address, ) } > @@ -391,16 +381,6 @@ export const EthereumTransactionModalComponent = ({ /> ); - case Stage.Review: - return ( - <> -

Review..

- - // - ); case Stage.Loading: return ( @@ -417,7 +397,7 @@ export const EthereumTransactionModalComponent = ({ {failMessage} ); - case Stage.Sent: + case Stage.Success: return ( diff --git a/packages/site/src/components/index.ts b/packages/site/src/components/index.ts index 7d5f55c..f211770 100644 --- a/packages/site/src/components/index.ts +++ b/packages/site/src/components/index.ts @@ -14,3 +14,4 @@ export * from './Account'; export * from './Network'; export * from './Faq'; export * from './Transaction'; +export * from './ConnectToggle'; diff --git a/packages/site/src/hooks/Accounts.tsx b/packages/site/src/hooks/Accounts.tsx index 0c2b00a..52bf58d 100644 --- a/packages/site/src/hooks/Accounts.tsx +++ b/packages/site/src/hooks/Accounts.tsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { MetamaskActions, MetaMaskContext } from '.'; import { AccountActivity, AccountActivityType, BundlerUrls, SmartContractAccount } from "../types"; -import { bundlerUrls, fetchUserOpHashes, getChainId, getKeyringSnapRpcClient, getMMProvider, getNextRequestId, getScAccount, getTxHashes, getUserOperationReceipt, parseChainId, sendSupportedEntryPoints } from "../utils"; +import { bundlerUrls, fetchUserOpHashes, connectedAccounts, getChainId, getKeyringSnapRpcClient, getMMProvider, getNextRequestId, getScAccount, getTxHashes, getUserOperationReceipt, parseChainId, sendSupportedEntryPoints, listConnectedAccounts } from "../utils"; import { KeyringAccount } from "@metamask/keyring-api"; import { KeyringSnapRpcClient } from '@metamask/keyring-api'; import type { Json } from '@metamask/utils'; @@ -187,12 +187,28 @@ export const useAcount = () => { }); }; + const updateConnectedAccounts = async () => { + const accounts = await connectedAccounts(); + dispatch({ + type: MetamaskActions.SetConnectedAccounts, + payload: accounts, + }); + }; + + const getConnectedAccounts = async () => { + const accounts = await listConnectedAccounts(); + dispatch({ + type: MetamaskActions.SetConnectedAccounts, + payload: accounts, + }); + }; + const getWalletChainId = async (): Promise => { return await getChainId() }; - const setChainIdListener = async () => { - if (!state.isChainIdListener) { + const setWalletListener = async () => { + if (!state.isWalletListener) { const provider = getMMProvider() if (provider) { provider.on('chainChanged', async (chainId) => { @@ -202,6 +218,13 @@ export const useAcount = () => { }); }); + provider.on('accountsChanged', async (accounts) => { + dispatch({ + type: MetamaskActions.SetConnectedAccounts, + payload: accounts, + }); + }); + dispatch({ type: MetamaskActions.SetWalletListener, payload: true, @@ -216,7 +239,7 @@ export const useAcount = () => { getSmartAccount, createAccount, deleteAccount, - setChainIdListener, + setWalletListener, getAccountActivity, getBundlerUrls, updateChainId, @@ -226,5 +249,7 @@ export const useAcount = () => { approveRequest, rejectRequest, rejectAllPendingRequests, + updateConnectedAccounts, + getConnectedAccounts, } } \ No newline at end of file diff --git a/packages/site/src/hooks/MetamaskContext.tsx b/packages/site/src/hooks/MetamaskContext.tsx index 690bf8f..b01e56d 100644 --- a/packages/site/src/hooks/MetamaskContext.tsx +++ b/packages/site/src/hooks/MetamaskContext.tsx @@ -16,21 +16,23 @@ export type MetamaskState = { isFlask: boolean; installedSnap?: Snap; error?: Error; - isChainIdListener: boolean; + isWalletListener: boolean; chainId: string; activeTab: AppTab; snapKeyring: KeyringState; selectedSnapKeyringAccount: KeyringAccount; + isSelectedSnapKeyringAccountConnected: boolean; scAccount: SmartContractAccount; accountActivity: AccountActivity[]; bundlerUrls?: BundlerUrls; + connectedAccounts: string[]; }; const initialState: MetamaskState = { isFlask: false, error: undefined, installedSnap: undefined, - isChainIdListener: false, + isWalletListener: false, chainId: '', activeTab: AppTab.SmartAccount, snapKeyring: { @@ -42,11 +44,12 @@ const initialState: MetamaskState = { address: '', options: { name: '', - owner: '', + smartAccountAddress: '', }, methods: [], type: EthAccountType.Eip4337, }, + isSelectedSnapKeyringAccountConnected: false, scAccount: { initCode: '', connected: false, @@ -64,6 +67,7 @@ const initialState: MetamaskState = { }, accountActivity: [], bundlerUrls: undefined, + connectedAccounts: [], }; type MetamaskDispatch = { type: MetamaskActions; payload: any }; @@ -91,6 +95,8 @@ export enum MetamaskActions { SetClearAccount = 'SetClearAccount', SetBundlerUrls = 'SetBundlerUrls', SetSupportedEntryPoints = 'SetSupportedEntryPoints', + SetConnectedAccounts = 'SetConnectedAccounts', + SetIsSelectedSnapKeyringAccount = 'SetIsSelectedSnapKeyringAccount', } const reducer: Reducer = (state, action) => { @@ -140,7 +146,7 @@ const reducer: Reducer = (state, action) => { case MetamaskActions.SetWalletListener: return { ...state, - isChainIdListener: action.payload, + isWalletListener: action.payload, }; case MetamaskActions.SetActiveTab: @@ -175,7 +181,7 @@ const reducer: Reducer = (state, action) => { address: '', options: { name: '', - owner: '', + smartAccountAddress: '', }, methods: [], type: EthAccountType.Eip4337, @@ -190,10 +196,26 @@ const reducer: Reducer = (state, action) => { entryPoint: '', deposit: '', factoryAddress: '', - ownerAddress: '', + owner: { + address: '', + balance: '', // in wei + } }, accountActivity: [], }; + + case MetamaskActions.SetConnectedAccounts: + return { + ...state, + connectedAccounts: action.payload, + }; + + case MetamaskActions.SetIsSelectedSnapKeyringAccount: + return { + ...state, + isSelectedSnapKeyringAccountConnected: action.payload, + }; + default: return state; } diff --git a/packages/site/src/pages/index.tsx b/packages/site/src/pages/index.tsx index 0abc3e8..2bb0706 100644 --- a/packages/site/src/pages/index.tsx +++ b/packages/site/src/pages/index.tsx @@ -9,8 +9,6 @@ import { clearActivityData, parseChainId, addBundlerUrl, - getUserOperationReceipt, - notify, } from '../utils'; import { ConnectSnapButton, @@ -21,12 +19,12 @@ import { SimpleButton, TabMenu, BundlerInputForm, - AccountActivityDisplay, Faq, Modal, EthereumTransactionModalComponent, TransactionType, ModalType, + ConnectToggle, } from '../components'; import { AppTab, BundlerUrls, SupportedChainIdMap } from '../types'; import snapPackageInfo from '../../../snap/package.json'; @@ -142,14 +140,16 @@ const Index = () => { getAccountActivity, getBundlerUrls, updateChainId, - setChainIdListener, + setWalletListener, + updateConnectedAccounts, + getConnectedAccounts, } = useAcount(); useEffect(() => { async function initNetwork() { if (state.isFlask) { await updateChainId() - await setChainIdListener() + await setWalletListener() } } @@ -161,6 +161,7 @@ const Index = () => { if (state.installedSnap) { const account = await getKeyringSnapAccounts() await handleFetchBundlerUrls() + await getConnectedAccounts() if (account.length > 0) { await selectKeyringSnapAccount(account[0]); await getSmartAccount(account[0].id); @@ -333,6 +334,15 @@ const Index = () => { } } + const handleConnectAccountClick = async (e: any) => { + try { + e.preventDefault(); + await updateConnectedAccounts() + } catch (e) { + dispatch({ type: MetamaskActions.SetError, payload: e }); + } + } + const handleClearActivity = async (e: any) => { try { e.preventDefault(); @@ -425,7 +435,7 @@ const Index = () => { { custom: {/* TODO: Comment for now until we can support these features */} - {/* {handleDepositClick(e)}}> */} + {handleDepositClick(e)}}> {/* {handleWithdrawClick(e)}}> */} - {/* {() =>{}}}> - {() =>{}}}> */} }} disabled={!state.isFlask} @@ -462,7 +470,7 @@ const Index = () => { {/* TODO: Add account activity */} - {/* {state.scAccount.connected && state.installedSnap && ( + {/* {state.selectedSnapKeyringAccount.id !== ''&& state.installedSnap && ( { { {/* Mangement tab */} {state.activeTab === AppTab.Management && ( + {state.scAccount.connected && state.installedSnap && ( + +
+

Connected

+ +
+ {/* TODO: Add change owner feature */} + {(e: any)=> {}}}> + + }} + disabled={!state.isFlask} + copyDescription + isAccount + isSmartAccount + fullWidth + /> + )} + {state.installedSnap && ( { { {state.activeTab === AppTab.Settings && ( + {state.installedSnap && state.snapKeyring.accounts.length > 0 && ( + {handleConnectAccountClick(e)}}> + }} + disabled={!state.isFlask} + fullWidth + /> + )} + {state.installedSnap && ( { export type BundlerUrls = { [chainId: string]: string }; -export type SignedTxs = Record; - export type SmartAccountParams = { scOwnerAddress: string; }; diff --git a/packages/site/src/utils/eth.ts b/packages/site/src/utils/eth.ts index 14865bd..b02cda0 100644 --- a/packages/site/src/utils/eth.ts +++ b/packages/site/src/utils/eth.ts @@ -1,6 +1,66 @@ import { BigNumber, ethers } from 'ethers'; import { getMMProvider } from './metamask'; +export const connectedAccounts = async() => { + await getMMProvider().request({ + method: "wallet_requestPermissions", + params: [ + { + "eth_accounts": {} + } + ] + }); + + const accounts = await getMMProvider().request({ method: 'eth_requestAccounts' }) + .catch((err) => { + if (err.code === 4001) { + // EIP-1193 userRejectedRequest error + // If this happens, the user rejected the connection request. + console.log('Please connect to MetaMask.'); + } else { + console.error(err); + } + }) as string[]; + + + return accounts; +} + +export const listConnectedAccounts = async() => { + return await getMMProvider().request({ method: 'eth_requestAccounts' }) + .catch((err) => { + if (err.code === 4001) { + // EIP-1193 userRejectedRequest error + // If this happens, the user rejected the connection request. + console.log('Please connect to MetaMask.'); + } else { + console.error(err); + } + }) as string[]; +} + +export const isAccountConnected = async(address: string): Promise => { + const accounts = await getMMProvider().request({ method: 'eth_requestAccounts' }) + .catch((err) => { + if (err.code === 4001) { + // EIP-1193 userRejectedRequest error + // If this happens, the user rejected the connection request. + console.log('Please connect to MetaMask.'); + } else { + console.error(err); + } + }) as string[]; + + let found = false; + for (let i = 0; i < accounts.length; i++) { + if (accounts[i].toLowerCase() === address.toLowerCase()) { + found = true; + break; + } + } + return found; +} + export const getAccountBalance = async (account: string): Promise => { const ethersProvider = new ethers.providers.Web3Provider( getMMProvider() as any, diff --git a/packages/site/src/utils/snap.ts b/packages/site/src/utils/snap.ts index b605c81..f11d337 100644 --- a/packages/site/src/utils/snap.ts +++ b/packages/site/src/utils/snap.ts @@ -8,10 +8,12 @@ import { BundlerUrls, UserOperation, UserOperationReceipt, - SignedTxs, } from '../types'; import snapPackageInfo from '../../../snap/package.json'; import { getMMProvider } from './metamask'; +import { JsonTx } from '@ethereumjs/tx'; +import { KeyringRequest } from '@metamask/keyring-api'; +import type { Json } from '@metamask/utils'; // Snap management ***************************************************************** /** @@ -126,7 +128,6 @@ export const getScAccount = async ( index: BigNumber.from(parsedResult.index), deposit: BigNumber.from(parsedResult.deposit).toString(), connected: true, - ownerAddress: parsedResult.ownerAddress, owner: { address: parsedResult.owner.address, balance: BigNumber.from(parsedResult.owner.balance).toString(), @@ -134,24 +135,9 @@ export const getScAccount = async ( } as SmartContractAccount; }; -// TODO: remove this method -export const getSignedTxs = async (): Promise => { - const result = (await getMMProvider().request({ - method: 'wallet_invokeSnap', - params: { - snapId: defaultSnapOrigin, - request: { method: 'get_signed_txs', params: [] }, - }, - })) as string; - const parsedResult = JSON.parse(result as string); - return parsedResult as SignedTxs; -}; - -// TODO: Update this method export const storeTxHash = async ( keyringAccountId: string, txHash: string, - keyringRequestId: string, chainId: string, ): Promise => { return (await getMMProvider().request({ @@ -164,7 +150,6 @@ export const storeTxHash = async ( { keyringAccountId, txHash, - keyringRequestId, chainId, }, ], @@ -173,7 +158,6 @@ export const storeTxHash = async ( })) as boolean; }; -// TODO: Update this method export const getTxHashes = async ( keyringAccountId: string, chainId: string, diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 486c602..5db1dcd 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/transeptorlabs/smarthub-snap.git" }, "source": { - "shasum": "enaolr+s+pZ0y3tbkWTBJgfbmAEpAH40xTgtToymSDc=", + "shasum": "nyJtFe+mvI8kTXx54YL4bLaqDw4fedV5poe1Ha+ElzY=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 8b92b5f..f45ce25 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -3,6 +3,7 @@ import type { OnKeyringRequestHandler, } from '@metamask/snaps-types'; import { + EthMethod, KeyringAccount, MethodNotSupportedError, handleKeyringRequest, @@ -21,6 +22,7 @@ import { getNextRequestId, } from './state'; import { + SignEntryPointDepositTxParams, EstimateCreationGasParams, EstimateUserOperationGas, GetTxHashesParams, @@ -47,6 +49,7 @@ import { getSmartAccountAddress, getUserOpCallData, } from './4337'; +import type { Json } from '@metamask/utils'; let keyring: SimpleKeyring; @@ -137,22 +140,17 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ throw new Error('Account not found'); } - const owner = keyringAccount.options.owner as string | undefined; - if (!owner) { - throw new Error('Owner not found'); - } - - const scAddress = await getSmartAccountAddress(owner); + const scAddress = await getSmartAccountAddress(keyringAccount.address); const [smartAcountBalance, ownerBalance, nonce, deposit] = await Promise.all([ await getBalance(scAddress), - await getBalance(owner), + await getBalance(keyringAccount.address), await getNonce(scAddress), await getDeposit(scAddress), ]); result = JSON.stringify({ - initCode: await getAccountInitCode(owner), + initCode: await getAccountInitCode(keyringAccount.address), address: scAddress, balance: smartAcountBalance, // in wei nonce, @@ -160,9 +158,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ entryPoint: DEFAULT_ENTRY_POINT, factoryAddress: DEFAULT_ACCOUNT_FACTORY, deposit, - ownerAddress: owner, owner: { - address: owner, + address: keyringAccount.address, balance: ownerBalance, // in wei }, }); @@ -267,6 +264,36 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ return result; } + case InternalMethod.SignEntryPointDepositTx: { + const params: SignEntryPointDepositTxParams = ( + request.params as any[] + )[0] as SignEntryPointDepositTxParams; + + await intitKeyRing(); + const depositRequest = { + id: (await getNextRequestId()).toString(), + scope: '', + account: params.keyringAccountId, + request: { + method: EthMethod.SignTransaction, + params: [params.type, params.tx] as Json[] + } + } + + const depositResult = await keyring.submitRequest(depositRequest); + result = JSON.stringify({ + request: depositRequest, + result: ( + depositResult as { + pending: false; + result: Json; + } + ).result, + }); + + return result + } + case InternalMethod.ChainId: { return await rpcClient.send(request.method, request.params as any[]); } diff --git a/packages/snap/src/keyring/keyring.ts b/packages/snap/src/keyring/keyring.ts index e525c0f..0269493 100644 --- a/packages/snap/src/keyring/keyring.ts +++ b/packages/snap/src/keyring/keyring.ts @@ -140,9 +140,9 @@ export class SimpleKeyring implements Keyring { id: uuid(), options: { name, - owner: address, - }, // only include name and owner in options - address: await getSmartAccountAddress(address), + smartAccountAddress: await getSmartAccountAddress(address), + }, + address: address, methods: [ EthMethod.PersonalSign, EthMethod.Sign, @@ -216,13 +216,8 @@ export class SimpleKeyring implements Keyring { 'SNAPS/submitRequest requests:', JSON.stringify(request, undefined, 2), ); - - if (request.scope !== 'async' && request.scope !== 'sync') { - throw new Error( - `Invalid request scope: ${request.scope}, must be 'async' or 'sync'`, - ); - } - return request.scope === 'sync' + const scope = 'sync'; + return scope === 'sync' ? this.#syncSubmitRequest(request) : this.#asyncSubmitRequest(request); } @@ -267,7 +262,7 @@ export class SimpleKeyring implements Keyring { request: KeyringRequest, ): Promise { const { method, params = [] } = request.request as JsonRpcRequest; - const result = await this.#handleSigningRequest(method, params); + const result = await this.#handleSigningRequest(method, params, request.account); return { pending: false, result, @@ -276,7 +271,7 @@ export class SimpleKeyring implements Keyring { async approveRequest(id: string): Promise { try { - const { request } = await this.getRequest(id); + const { request, account } = await this.getRequest(id); if (request === undefined) { throw new Error(`Request '${id}' not found`); } @@ -290,6 +285,7 @@ export class SimpleKeyring implements Keyring { const result = await this.#handleSigningRequest( request.method, (request.params as Json) ?? [], + account, ); delete this.#pendingRequests[id]; @@ -318,7 +314,7 @@ export class SimpleKeyring implements Keyring { await this.#emitEvent(KeyringEvent.RequestRejected, { id }); } - async #handleSigningRequest(method: string, params: Json): Promise { + async #handleSigningRequest(method: string, params: Json, accountId: string): Promise { switch (method) { case EthMethod.PersonalSign: { const [from, message] = params as [string, string]; @@ -326,17 +322,15 @@ export class SimpleKeyring implements Keyring { } case EthMethod.SignTransaction: { - const [from, type, tx] = params as [ - string, - string, - JsonTx | UserOperation, - ]; - if (type === 'eoa') { - return await this.#signTransactionEthers(from, tx as JsonTx); - } else if (type === 'eip4337') { - return (await this.#signUserOp(from, tx as UserOperation)) as Json; + const [tx] = params as [any]; + + // check to see if tx initCode is present + if (tx.initCode) { + const userOp = tx as UserOperation; + return await this.#signUserOp(accountId, userOp) as Json; + } else { + return this.#signTransaction(accountId, tx as JsonTx); } - throw new Error(`Unknown account type: ${type}`); } case EthMethod.SignTypedDataV1: { @@ -372,7 +366,7 @@ export class SimpleKeyring implements Keyring { } async #signUserOp( - from: string, + accountId: string, userOp: UserOperation, ): Promise { const provider = new ethers.providers.Web3Provider(ethereum as any); @@ -383,7 +377,7 @@ export class SimpleKeyring implements Keyring { ); // sign the userOp - const { privateKey } = this.#getWalletByAddress(from); + const { privateKey } = this.#getWalletById(accountId); const wallet = new EthersWallet(privateKey); userOp.signature = '0x'; const userOpHash = ethers.utils.arrayify( @@ -393,14 +387,7 @@ export class SimpleKeyring implements Keyring { return deepHexlify({ ...userOp, signature }); } - async #signTransactionEthers(from: string, tx: JsonTx): Promise { - const provider = new ethers.providers.Web3Provider(ethereum as any); - const { privateKey } = this.#getWalletByAddress(from); - const wallet = new EthersWallet(privateKey, provider); - return await wallet.signTransaction(tx as any); - } - - #signTransaction(from: string, tx: JsonTx): Json { + #signTransaction(accountId: string, tx: JsonTx): Json { if (!tx.chainId) { throw new Error('Missing chainId'); } @@ -410,7 +397,7 @@ export class SimpleKeyring implements Keyring { tx.chainId = `0x${parseInt(tx.chainId, 10).toString(16)}`; } - const wallet = this.#getWalletByAddress(from); + const wallet = this.#getWalletById(accountId); const privateKey = Buffer.from(wallet.privateKey, 'hex'); const common = Common.custom( { chainId: Number(tx.chainId) }, diff --git a/packages/snap/src/keyring/permissions.ts b/packages/snap/src/keyring/permissions.ts index 0e0229a..15dd3b9 100644 --- a/packages/snap/src/keyring/permissions.ts +++ b/packages/snap/src/keyring/permissions.ts @@ -10,6 +10,7 @@ export enum InternalMethod { GetUserOpCallData = 'get_user_op_call_data', EstimateCreationGas = 'estimate_creation_gas', Notify = 'notify', + SignEntryPointDepositTx = 'sign_entry_point_deposit_tx', // ERC-4337 methods eth namespace SendUserOperation = 'eth_sendUserOperation', @@ -75,6 +76,7 @@ export const PERMISSIONS = new Map([ InternalMethod.GetUserOpCallData, InternalMethod.EstimateCreationGas, InternalMethod.Notify, + InternalMethod.SignEntryPointDepositTx, // ERC-4337 methods eth namespace InternalMethod.SendUserOperation, @@ -126,6 +128,7 @@ export const PERMISSIONS = new Map([ InternalMethod.GetUserOpCallData, InternalMethod.EstimateCreationGas, InternalMethod.Notify, + InternalMethod.SignEntryPointDepositTx, // ERC-4337 methods eth namespace InternalMethod.SendUserOperation, diff --git a/packages/snap/src/types/request.types.ts b/packages/snap/src/types/request.types.ts index 7e85931..66a55b4 100644 --- a/packages/snap/src/types/request.types.ts +++ b/packages/snap/src/types/request.types.ts @@ -1,5 +1,6 @@ import { BigNumber } from 'ethers'; import { UserOperation } from './erc-4337.types'; +import { JsonTx } from '@ethereumjs/tx'; export type NotifyParams = { heading: string; @@ -54,3 +55,9 @@ export type EstimateCreationGasParams = { export type EstimateUserOperationGas = { userOp: UserOperation; }; + +export type SignEntryPointDepositTxParams = { + keyringAccountId: string; + type: 'eoa' | 'eip4337' + tx: JsonTx +} diff --git a/scripts/get.sh b/scripts/get.sh deleted file mode 100755 index 9c988bb..0000000 --- a/scripts/get.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -u -set -o pipefail - -if [[ ${RUNNER_DEBUG:-0} == 1 ]]; then - set -x -fi - -KEY="${1}" -OUTPUT="${2}" - -if [[ -z $KEY ]]; then - echo "Error: KEY not specified." - exit 1 -fi - -if [[ -z $OUTPUT ]]; then - echo "Error: OUTPUT not specified." - exit 1 -fi - -echo "$OUTPUT=$(jq --raw-output "$KEY" package.json)" >> "$GITHUB_OUTPUT" diff --git a/scripts/prepack.sh b/scripts/prepack.sh deleted file mode 100755 index d14605d..0000000 --- a/scripts/prepack.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -o pipefail - -if [[ -n $SKIP_PREPACK ]]; then - echo "Notice: skipping prepack." - exit 0 -fi - -yarn build:clean diff --git a/yarn.lock b/yarn.lock index abd324b..720a7c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6607,12 +6607,14 @@ __metadata: resolution: "@transeptor-labs/smarthub-site@workspace:packages/site" dependencies: "@account-abstraction/contracts": ^0.6.0 + "@ethereumjs/tx": ^4.1.2 "@metamask/eslint-config": ^10.0.0 "@metamask/eslint-config-jest": ^10.0.0 "@metamask/eslint-config-nodejs": ^10.0.0 "@metamask/eslint-config-typescript": ^10.0.0 "@metamask/keyring-api": ^1.0.0 "@metamask/providers": ^13.0.0 + "@metamask/utils": ^3.3.0 "@svgr/webpack": ^6.4.0 "@types/jest": ^27.5.2 "@types/react": ^18.0.15 From dcfc90d52c0c3061a32e7185660a5897576ca0bb Mon Sep 17 00:00:00 2001 From: Idris Bowman Date: Thu, 23 Nov 2023 15:33:41 -0500 Subject: [PATCH 2/4] feat: make sure selected account is not connected before sending deposit transaction --- packages/site/src/components/Transaction.tsx | 9 ++++++--- packages/site/src/hooks/Accounts.tsx | 1 + packages/site/src/utils/eth.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/site/src/components/Transaction.tsx b/packages/site/src/components/Transaction.tsx index e445d73..e6a451c 100644 --- a/packages/site/src/components/Transaction.tsx +++ b/packages/site/src/components/Transaction.tsx @@ -153,7 +153,10 @@ export const EthereumTransactionModalComponent = ({ encodedFunctionData, ); - // TODO: changeto account before sending transaction + // check the selected account is connected + if (!state.isSelectedSnapKeyringAccountConnected) { + throw new Error('The selected account is not connected. Please connect the account using Settings page.') + } // send transaction const res = await getMMProvider().request({ @@ -181,7 +184,7 @@ export const EthereumTransactionModalComponent = ({ setAmount(''); setSuccessMessage(`${amount} ETH successfully depoisted to entry point contract.`); setStatus(Stage.Success); - notify('Deposit Transaction sent (txHash)', 'View activity for details.', res) + // notify('Deposit Transaction sent (txHash)', 'View activity for details.', res) } const handleWithdrawSubmit = async () => { @@ -267,7 +270,7 @@ export const EthereumTransactionModalComponent = ({ } catch (e) { console.error(e.message) setAmount(''); - setFailMessage('Transaction failed to send.'); + setFailMessage(e.message); setStatus(Stage.Failed); } }; diff --git a/packages/site/src/hooks/Accounts.tsx b/packages/site/src/hooks/Accounts.tsx index 52bf58d..9b7e8cb 100644 --- a/packages/site/src/hooks/Accounts.tsx +++ b/packages/site/src/hooks/Accounts.tsx @@ -197,6 +197,7 @@ export const useAcount = () => { const getConnectedAccounts = async () => { const accounts = await listConnectedAccounts(); + console.log('accounts', accounts) dispatch({ type: MetamaskActions.SetConnectedAccounts, payload: accounts, diff --git a/packages/site/src/utils/eth.ts b/packages/site/src/utils/eth.ts index b02cda0..5b2f976 100644 --- a/packages/site/src/utils/eth.ts +++ b/packages/site/src/utils/eth.ts @@ -33,8 +33,10 @@ export const listConnectedAccounts = async() => { // EIP-1193 userRejectedRequest error // If this happens, the user rejected the connection request. console.log('Please connect to MetaMask.'); + return []; } else { console.error(err); + return []; } }) as string[]; } From dad3cd94656e86d8ec0246edadf4d06e9f8c4ce5 Mon Sep 17 00:00:00 2001 From: Idris Bowman Date: Thu, 23 Nov 2023 16:00:42 -0500 Subject: [PATCH 3/4] feat: add wanrning banner --- packages/site/src/App.tsx | 16 +++- packages/site/src/components/AlertBanner.tsx | 88 ++++++++++++++++++++ packages/site/src/components/index.ts | 1 + packages/site/src/pages/index.tsx | 4 +- 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/site/src/components/AlertBanner.tsx diff --git a/packages/site/src/App.tsx b/packages/site/src/App.tsx index b3d5a2c..a79cf3f 100644 --- a/packages/site/src/App.tsx +++ b/packages/site/src/App.tsx @@ -1,6 +1,6 @@ import { FunctionComponent, ReactNode, useContext } from 'react'; import styled from 'styled-components'; -import { Footer, Header } from './components'; +import { Footer, Header, AlertBanner, AlertType } from './components'; import { GlobalStyle } from './config/theme'; import { ToggleThemeContext } from './Root'; @@ -13,6 +13,12 @@ const Wrapper = styled.div` max-width: 100vw; `; +const BannerWrapper = styled.div` + padding-top: 25px; + padding-left: 5%; + padding-right: 5%; +`; + export type AppProps = { children: ReactNode; }; @@ -29,6 +35,14 @@ export const App: FunctionComponent = ({ children }) => {
+ + + {children}