diff --git a/package.json b/package.json index 0703539..2a05458 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bobanetwork/snap-account-abstraction-keyring-monorepo", - "version": "1.1.13", + "version": "1.1.18", "private": true, "description": "An account abstraction keyring snap that integrates with MetaMask accounts on Boba Network", "keywords": [ diff --git a/packages/site/.env.development b/packages/site/.env.development index ba45662..059c510 100644 --- a/packages/site/.env.development +++ b/packages/site/.env.development @@ -1,2 +1,2 @@ -GATSBY_SNAP_ORIGIN=npm:@bobanetwork/snap-account-abstraction-keyring-hc +GATSBY_SNAP_ORIGIN=local:http://localhost:8080 USE_LOCAL_NETWORK=false diff --git a/packages/site/.env.development.hc b/packages/site/.env.development.hc index ba45662..059c510 100644 --- a/packages/site/.env.development.hc +++ b/packages/site/.env.development.hc @@ -1,2 +1,2 @@ -GATSBY_SNAP_ORIGIN=npm:@bobanetwork/snap-account-abstraction-keyring-hc +GATSBY_SNAP_ORIGIN=local:http://localhost:8080 USE_LOCAL_NETWORK=false diff --git a/packages/site/package.json b/packages/site/package.json index 9bcc7ee..28881db 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -1,6 +1,6 @@ { "name": "@bobanetwork/snap-account-abstraction-keyring-site", - "version": "1.1.13", + "version": "1.1.18", "private": true, "license": "(MIT-0 OR Apache-2.0)", "scripts": { diff --git a/packages/site/src/components/Buttons.tsx b/packages/site/src/components/Buttons.tsx index 21b79c1..b4ee511 100644 --- a/packages/site/src/components/Buttons.tsx +++ b/packages/site/src/components/Buttons.tsx @@ -40,6 +40,7 @@ const Button = styled.button` align-items: center; justify-content: center; margin-top: auto; + ${({ theme }) => theme.mediaQueries.small} { width: 100%; } @@ -50,7 +51,7 @@ const ButtonText = styled.span` `; const ConnectedContainer = styled.div` - display: flex; + display: inline-flex; align-self: flex-start; align-items: center; justify-content: center; @@ -61,6 +62,8 @@ const ConnectedContainer = styled.div` color: ${(props) => props.theme.colors.text?.inverse}; font-weight: bold; padding: 1.2rem; + margin-right: 1rem; + margin-left: 1rem; `; const ConnectedIndicator = styled.div` @@ -115,7 +118,7 @@ export const ConnectButton = (props: ComponentProps) => { return ( ); }; @@ -151,6 +154,12 @@ export const HeaderButtons = ({ updateAvailable: boolean; onConnectClick(): unknown; }) => { + const getNetworkName = () => { + return window.ethereum.networkVersion === '28882' + ? 'Boba Sepolia' + : 'Boba Mainnet'; + }; + if (!state.hasMetaMask && !state.installedSnap) { return ; } @@ -172,10 +181,16 @@ export const HeaderButtons = ({ } return ( - - - Connected - +
+
+ + + + Connected to: {getNetworkName()} + + +
+
); }; diff --git a/packages/site/src/components/Header.tsx b/packages/site/src/components/Header.tsx index c367b14..0f59554 100644 --- a/packages/site/src/components/Header.tsx +++ b/packages/site/src/components/Header.tsx @@ -8,7 +8,7 @@ import packageInfo from '../../package.json'; import Logo from '../assets/boba-logo.png'; import { defaultSnapOrigin } from '../config'; import { MetamaskActions, MetaMaskContext } from '../hooks'; -import { connectSnap, getSnap } from '../utils'; +import { connectSnap, connectSnapWithNetwork, getSnap } from '../utils'; const HeaderWrapper = styled.header` display: flex; @@ -47,10 +47,10 @@ const RightContainer = styled.div` `; const VersionStyle = styled.p` + width: fit-content; margin-top: 1.2rem; font-size: 1.6rem; - margin: auto; - padding-right: 2rem; + padding-right: 4rem; color: ${({ theme }) => theme.colors.text?.muted}; `; @@ -86,35 +86,59 @@ export const Header = () => { return (
- Dapp V: - {packageInfo.version} -
-
- Snap V: + {defaultSnapOrigin.startsWith('local') && + `(from ${defaultSnapOrigin})` && ( + + Local Snap + + )} - Expected: {snapPackageInfo.version} - {' '} - | + DApp: {packageInfo.version} + - Installed: {state.installedSnap?.version} - + Expected: {snapPackageInfo.version} + {' '} + {state.installedSnap?.version ? ( + + Installed: {state.installedSnap?.version} + + ) : ( + + Installed: No Snap found + + )}
- - {defaultSnapOrigin.startsWith('local') && `(from ${defaultSnapOrigin})`}
); }; @@ -127,6 +151,24 @@ export const Header = () => { + {state.installedSnap?.version && ( + + )} { const client = new KeyringSnapRpcClient(snapId, window.ethereum as any); const abiCoder = new ethers.AbiCoder(); - useEffect(() => { /** * Return the current state of the snap. @@ -401,7 +400,7 @@ const Index = () => { const sendBobaTx = async () => { if (!snapState?.accounts || !selectedAccount) { - throw new Error('Source account not connected'); + throw new Error('Please connect your wallet first!'); } // Paymaster Setup steps (only first time or when required) @@ -471,17 +470,6 @@ const Index = () => { if (bobaPaymasterSelected) { method = 'eth_sendUserOpBobaPM'; } - console.log({ - method: 'wallet_invokeSnap', - params: { - snapId: defaultSnapOrigin, - request: { - method, - params: [transactionDetails], - id: snapState.accounts[0]?.id ?? '', - }, - }, - }); const submitRes = await window.ethereum.request({ method: 'wallet_invokeSnap', @@ -521,7 +509,8 @@ const Index = () => { const accountManagementMethods = [ { name: 'Create account', - description: 'Create a 4337 account using an admin private key', + description: + 'Create a 4337 account using an admin private key and a salt, which you need to write down or store to re-create the wallet.', inputs: [ { id: 'create-account-private-key', @@ -534,7 +523,7 @@ const Index = () => { }, { id: 'create-account-salt', - title: 'Salt (optional)', + title: 'Salt (optional, write it down)', value: salt, type: InputType.TextField, placeholder: 'E.g. 0x123', @@ -551,7 +540,7 @@ const Index = () => { { name: 'Create account (Deterministic)', description: - 'Create a 4337 account using a deterministic key generated through the snap', + 'Create a 4337 account using a deterministic key generated through the snap. If the account cannot be found or already exists, try to remove and re-install the snap via the Metamask UI.', inputs: [ { id: 'create-account-deterministic', @@ -610,15 +599,15 @@ const Index = () => { onChange: (event: any) => setTransferAmount(event.currentTarget.value), }, - { - id: 'transfer-fund-boba-paymaster', - title: 'Select boba as paymaster.', - value: bobaPaymasterSelected, - type: InputType.CheckBox, - placeholder: 'E.g. 0.00', - onChange: (event: any) => - setBobaPaymasterSelected(event.target.checked), - }, + // { + // id: 'transfer-fund-boba-paymaster', + // title: 'Select boba as paymaster.', + // value: bobaPaymasterSelected, + // type: InputType.CheckBox, + // placeholder: 'E.g. 0.00', + // onChange: (event: any) => + // setBobaPaymasterSelected(event.target.checked), + // }, ], action: { callback: async () => await sendBobaTx(), diff --git a/packages/site/src/utils/snap.ts b/packages/site/src/utils/snap.ts index 7847283..714afa2 100644 --- a/packages/site/src/utils/snap.ts +++ b/packages/site/src/utils/snap.ts @@ -1,8 +1,41 @@ import snapPackageInfo from '../../../snap/package.json'; import { defaultSnapOrigin } from '../config'; -import { isLocalNetwork } from '../config/snap'; import type { GetSnapsResponse, Snap } from '../types'; +// Network configuration type +type NetworkConfig = { + chainId: string; + hexChainId: string; + chainName: string; + rpcUrl: string; + blockExplorerUrl: string; +}; + +// Network configurations +const NETWORKS: Record = { + local: { + chainId: '901', + hexChainId: '0x385', + chainName: 'Boba Local', + rpcUrl: 'http://localhost:9545', + blockExplorerUrl: '', + }, + mainnet: { + chainId: '288', + hexChainId: '0x120', + chainName: 'Boba Mainnet', + rpcUrl: 'wss://gateway.tenderly.co/public/boba-ethereum', + blockExplorerUrl: 'https://bobascan.com', + }, + sepolia: { + chainId: '28882', + hexChainId: '0x70d2', + chainName: 'Boba Sepolia', + rpcUrl: 'https://sepolia.boba.network', + blockExplorerUrl: 'https://testnet.bobascan.com', + }, +}; + /** * Get the installed snaps in MetaMask. * @@ -14,30 +47,22 @@ export const getSnaps = async (): Promise => { })) as unknown as GetSnapsResponse; }; -/** - * Connect a snap to MetaMask. - * - * @param snapId - The ID of the snap. - * @param params - The params to pass with the snap to connect. - */ -export const connectSnap = async ( - snapId: string = defaultSnapOrigin, - params: Record<'version' | string, unknown> = { - version: snapPackageInfo.version, - }, +export const switchToNetwork = async ( + networkType: 'local' | 'mainnet' | 'sepolia', ) => { - // check for current connected chain and force user to switch to boba sepolia. + const network = NETWORKS[networkType]; + if (!network) { + throw new Error('invalid network'); + } const currentChain = window.ethereum.networkVersion; - // 901 = local boba network - const desiredChain: string = isLocalNetwork ? '901' : '28882'; - if (currentChain !== desiredChain) { - const hexDesiredChain = isLocalNetwork ? '0x385' : '0x70d2'; + + if (currentChain !== network.chainId) { try { await window.ethereum.request({ method: 'wallet_switchEthereumChain', params: [ { - chainId: hexDesiredChain, + chainId: network.hexChainId, }, ], }); @@ -47,26 +72,36 @@ export const connectSnap = async ( method: 'wallet_addEthereumChain', params: [ { - chainId: hexDesiredChain, - chainName: isLocalNetwork ? 'Boba Local' : 'Boba Sepolia', - rpcUrls: [ - isLocalNetwork - ? 'http://localhost:9545' - : 'https://sepolia.boba.network', - ], + chainId: network.hexChainId, + chainName: network.chainName, + rpcUrls: [network.rpcUrl], nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18, }, - blockExplorerUrls: ['https://testnet.bobascan.com'], + blockExplorerUrls: [network.blockExplorerUrl], }, ], }); } } } +}; +/** + * Connect a snap to MetaMask. + * + * @param snapId - The ID of the snap. + * @param params - The params to pass with the snap to connect. + */ +// Separate snap connection function that doesn't force network switching +export const connectSnap = async ( + snapId: string = defaultSnapOrigin, + params: Record<'version' | string, unknown> = { + version: snapPackageInfo.version, + }, +) => { await window.ethereum.request({ method: 'wallet_requestSnaps', params: { @@ -75,6 +110,18 @@ export const connectSnap = async ( }); }; +// Utility function that combines network switching and snap connection +export const connectSnapWithNetwork = async ( + networkType: 'local' | 'mainnet' | 'sepolia', + snapId: string = defaultSnapOrigin, + params: Record<'version' | string, unknown> = { + version: snapPackageInfo.version, + }, +) => { + await switchToNetwork(networkType); + await connectSnap(snapId, params); +}; + export const loadAccountConnected = async () => { const accounts: any = await window.ethereum.request({ method: 'eth_requestAccounts', diff --git a/packages/site/src/utils/theme.ts b/packages/site/src/utils/theme.ts index a5b6a85..a8ce823 100644 --- a/packages/site/src/utils/theme.ts +++ b/packages/site/src/utils/theme.ts @@ -1,5 +1,3 @@ -import { getLocalStorage, setLocalStorage } from './localStorage'; - /** * Get the user's preferred theme in local storage. * Will default to the browser's preferred theme if there is no value in local storage. @@ -7,21 +5,5 @@ import { getLocalStorage, setLocalStorage } from './localStorage'; * @returns True if the theme is "dark" otherwise, false. */ export const getThemePreference = () => { - if (typeof window === 'undefined') { - return false; - } - - const darkModeSystem = window?.matchMedia( - '(prefers-color-scheme: dark)', - ).matches; - - const localStoragePreference = getLocalStorage('theme'); - const systemPreference = darkModeSystem ? 'dark' : 'light'; - const preference = localStoragePreference ?? systemPreference; - - if (!localStoragePreference) { - setLocalStorage('theme', systemPreference); - } - - return preference === 'dark'; + return false; // enforce light-mode in v1 }; diff --git a/packages/snap/package.json b/packages/snap/package.json index 257c5a5..b8d9538 100644 --- a/packages/snap/package.json +++ b/packages/snap/package.json @@ -1,6 +1,6 @@ { "name": "@bobanetwork/snap-account-abstraction-keyring-hc", - "version": "1.1.13", + "version": "1.1.18", "description": "An account abstraction keyring snap that integrates with MetaMask accounts on Boba Network", "keywords": [ "metamask", diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index e668a67..07b7e5c 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -1,5 +1,5 @@ { - "version": "1.1.13", + "version": "1.1.18", "description": "An account abstraction keyring snap that integrates with MetaMask accounts on Boba Network", "proposedName": "Boba Network Account Abstraction Keyring", "repository": { @@ -7,7 +7,7 @@ "url": "git+https://github.com/bobanetwork/snap-account-abstraction-keyring.git" }, "source": { - "shasum": "EeKFCRVshVk7hnvwGmz1qjpHULMKwIqKW+mHv4RZdNA=", + "shasum": "pzOC9aRKvu78HphSfpPHH4S69LOzR+my1Ul5uxcNZwE=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -26,7 +26,10 @@ "https://hc-wallet.sepolia.boba.network", "https://aa-hc-example-fe.onrender.com/", "https://boba-blockchain-busters-frontend.onrender.com", - "https://presibot.onrender.com" + "https://presibot.onrender.com", + "https://staging.gateway.boba.network", + "https://stagingv2.gateway.boba.network", + "https://codecaster.onrender.com" ] }, "endowment:rpc": { diff --git a/packages/snap/src/constants/aa-config.ts b/packages/snap/src/constants/aa-config.ts index 4e080f7..920eb85 100644 --- a/packages/snap/src/constants/aa-config.ts +++ b/packages/snap/src/constants/aa-config.ts @@ -7,7 +7,7 @@ export const AA_CONFIG = { simpleAccountFactory: '0x9406cc6185a346906296840746125a0e44976454', bobaPaymaster: '0x', bobaToken: '0xa18bF3994C0Cc6E3b63ac420308E5383f53120D7', - bundlerUrl: '', + bundlerUrl: 'https://bundler-hc.mainnet.boba.network', }, // TESTNETS [CHAIN_IDS.SEPOLIA]: { diff --git a/packages/snap/src/constants/dummy-values.ts b/packages/snap/src/constants/dummy-values.ts index 4a6988e..de23179 100644 --- a/packages/snap/src/constants/dummy-values.ts +++ b/packages/snap/src/constants/dummy-values.ts @@ -3,6 +3,8 @@ import { ethers } from 'ethers'; export const DUMMY_SIGNATURE = '0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c'; +// https://docs.metamask.io/snaps/reference/keyring-api/chain-methods/ +export const DUMMY_PAYMASTER = `0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`; /** * Get the dummy paymaster and data. @@ -12,7 +14,7 @@ export const DUMMY_SIGNATURE = */ export function getDummyPaymasterAndData(paymasterAddress?: string): string { if (!paymasterAddress) { - return '0x'; + return DUMMY_PAYMASTER; } const encodedValidUntilAfter = stripHexPrefix( diff --git a/packages/snap/src/encryption.ts b/packages/snap/src/encryption.ts index 3170495..865483e 100644 --- a/packages/snap/src/encryption.ts +++ b/packages/snap/src/encryption.ts @@ -1,47 +1,107 @@ -import { randomBytes, createCipheriv, createDecipheriv } from 'crypto'; +import { bytesToHex, hexToBytes } from '@metamask/utils'; -const ENCRYPTION_KEY_NAME = 'encryptionKey'; +// Changed from 80 to 32 bytes (256 bits) for AES-GCM +const ENCRYPTION_KEY_LENGTH = 32; -type SnapState = Record; +export async function getOrCreateEncryptionKey(): Promise { + try { + const state = await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, + }); + // If we have a stored key, verify it's valid before returning + if (state?.encryptionKey) { + const key = hexToBytes(state.encryptionKey as string); + // Verify key length + if (key.length !== ENCRYPTION_KEY_LENGTH) { + throw new Error('Invalid stored key length'); + } + // verify it's valid for AES-GCM + await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, [ + 'encrypt', + ]); + return key; + } -async function getOrCreateEncryptionKey(): Promise { - const state = (await snap.request({ - method: 'snap_manageState', - params: { operation: 'get' }, - })) as SnapState | null; + const entropy = await snap.request({ + method: 'snap_getEntropy', + params: { + version: 1, + }, + }); - if (state && typeof state[ENCRYPTION_KEY_NAME] === 'string') { - return Buffer.from(state[ENCRYPTION_KEY_NAME], 'hex'); + const key = hexToBytes(entropy.slice(2, 66)); + await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, [ + 'encrypt', + ]); + await snap.request({ + method: 'snap_manageState', + params: { + operation: 'update', + newState: { + ...state, + encryptionKey: bytesToHex(key), + }, + }, + }); + + return key; + } catch (error) { + console.error('Error in getOrCreateEncryptionKey:', error); + throw error; } +} + +export async function encrypt(data: string): Promise { + const key = await getOrCreateEncryptionKey(); + const iv = new Uint8Array(16); + crypto.getRandomValues(iv); + const ivHex = bytesToHex(iv); - const newKey = randomBytes(32); // 256 bits - await snap.request({ - method: 'snap_manageState', - params: { - operation: 'update', - newState: { [ENCRYPTION_KEY_NAME]: newKey.toString('hex') }, + const dataBytes = new TextEncoder().encode(data); + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'AES-GCM' }, + false, + ['encrypt'], + ); + const encryptedBytes = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, }, - }); + cryptoKey, + dataBytes, + ); - return newKey; + const encryptedHex = bytesToHex(new Uint8Array(encryptedBytes)); + return `${ivHex}:${encryptedHex}`; } -export async function encrypt(text: string): Promise { +export async function decrypt(encryptedData: string): Promise { + const [ivHex, dataHex] = encryptedData.split(':'); + if (!ivHex || !dataHex) { + throw new Error('Invalid encrypted data format'); + } const key = await getOrCreateEncryptionKey(); - const iv = randomBytes(16); - const cipher = createCipheriv('aes-256-cbc', key, iv); - let encrypted = cipher.update(text); - encrypted = Buffer.concat([encrypted, cipher.final()]); - return `${iv.toString('hex')}:${encrypted.toString('hex')}`; -} + const iv = hexToBytes(ivHex); + const encryptedBytes = hexToBytes(dataHex); + const cryptoKey = await crypto.subtle.importKey( + 'raw', + key, + { name: 'AES-GCM' }, + false, + ['decrypt'], + ); + const decryptedBytes = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + cryptoKey, + encryptedBytes, + ); -export async function decrypt(text: string): Promise { - const key = await getOrCreateEncryptionKey(); - const textParts = text.split(':'); - const iv = Buffer.from(textParts.shift() ?? '', 'hex'); - const encryptedText = Buffer.from(textParts.join(':'), 'hex'); - const decipher = createDecipheriv('aes-256-cbc', key, iv); - let decrypted = decipher.update(encryptedText); - decrypted = Buffer.concat([decrypted, decipher.final()]); - return decrypted.toString(); + return new TextDecoder().decode(decryptedBytes); } diff --git a/packages/snap/src/keyring.ts b/packages/snap/src/keyring.ts index da324f7..407c716 100644 --- a/packages/snap/src/keyring.ts +++ b/packages/snap/src/keyring.ts @@ -44,7 +44,7 @@ import { DUMMY_SIGNATURE, getDummyPaymasterAndData, } from './constants/dummy-values'; -import { encrypt, decrypt } from './encryption'; +import { decrypt, encrypt } from './encryption'; import { logger } from './logger'; import { InternalMethod } from './permissions'; import { SecurePrivateKey } from './secureKey'; @@ -58,11 +58,11 @@ import { CaipNamespaces, isEvmChain, toCaipChainId } from './utils/caip'; import { getUserOperationHash } from './utils/ecdsa'; import { getSigner, provider } from './utils/ethers'; import { + fetchWithRetry, + getSignerPrivateKey, isUniqueAddress, runSensitive, throwError, - getSignerPrivateKey, - fetchWithRetry, } from './utils/util'; const unsupportedAAMethods = [ @@ -120,14 +120,6 @@ type IUserOpGasEstimate = { verificationGasLimit: string | undefined; }; -const KeyringRequestSchema = z.object({ - id: z.string(), - request: z.object({ - method: z.string(), - params: z.array(z.unknown()).optional().default([]), - }), -}); - // eslint-disable-next-line jsdoc/require-jsdoc export function packUserOp(op: any, forSignature = true): string { if (forSignature) { @@ -329,7 +321,8 @@ export class AccountAbstractionKeyring implements Keyring { // 4337 methods EthMethod.PrepareUserOperation, EthMethod.PatchUserOperation, - EthMethod.SignUserOperation, + // TODO - disabled until MM Flask allows 0.7 UserOperations + // EthMethod.SignUserOperation, ], type: EthAccountType.Erc4337, }; @@ -424,29 +417,29 @@ export class AccountAbstractionKeyring implements Keyring { return this.#syncSubmitRequest(request); } - async #syncSubmitRequest(request: unknown): Promise { + async #syncSubmitRequest(request: any): Promise { try { - const validatedRequest = KeyringRequestSchema.parse(request); - - const { method, params = [] } = validatedRequest.request; - const { scope } = (params[0] as Record) || {}; - - console.log( - `handling goes here`, - scope, - JSON.stringify(validatedRequest), - ); + const { method, params } = request.request; + const scope = request.scope ? request.scope : params[0].scope; + let selectedWallet; + + // @DEV todo create one uniform way of retrieving the wallet addr + try { + selectedWallet = this.#getWalletByAddress(request.account); + } catch (error) { + selectedWallet = this.#getWalletById(request.id); + } - const signature = await this.#handleSigningRequest({ - account: this.#getWalletById(validatedRequest.id).account, + const response = await this.#handleSigningRequest({ + account: selectedWallet.account, method, params: params as Json, - scope: scope as string, + scope, }); return { pending: false, - result: signature, + result: response, }; } catch (error) { if (error instanceof z.ZodError) { @@ -465,12 +458,15 @@ export class AccountAbstractionKeyring implements Keyring { } #getWalletByAddress(address: string): Wallet { - const match = Object.values(this.#state.wallets).find( - (wallet) => - wallet.account.address.toLowerCase() === address.toLowerCase(), - ); - - return match ?? throwError(`Account '${address}' not found`); + const walletByIdentifier = this.#state.wallets[address]; + if (!walletByIdentifier) { + const match = Object.values(this.#state.wallets).find( + (wallet) => + wallet.account.address.toLowerCase() === address.toLowerCase(), + ); + return match ?? throwError(`Account '${address}' not found`); + } + return walletByIdentifier; } #getKeyPair(privateKey?: string): { @@ -535,7 +531,10 @@ export class AccountAbstractionKeyring implements Keyring { } // params is always an array, payload can be an array, or single tx - const payload = (params as any)[0]?.payload; + let payload = (params as any)[0]?.payload; + if (!payload) { + payload = params; + } const mapParamsToTransactions = (): EthBaseTransaction[] => { if (Array.isArray(payload)) { return payload as EthBaseTransaction[]; @@ -549,8 +548,6 @@ export class AccountAbstractionKeyring implements Keyring { switch (method) { case InternalMethod.SendUserOpBoba: { - console.log('Trigger boba send request'); - return await this.#prepareAndSignUserOperationBoba( account.address, mapParamsToTransactions(), @@ -580,18 +577,20 @@ export class AccountAbstractionKeyring implements Keyring { } case EthMethod.PrepareUserOperation: { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error will fix type in next PR return await this.#prepareUserOperation( account.address, mapParamsToTransactions(), ); } - // case EthMethod.PatchUserOperation: { - // const [userOp] = params as [EthUserOperation]; - // return await this.#patchUserOperation(account.address, userOp); - // } + case EthMethod.PatchUserOperation: { + const [userOp] = params as [EthUserOperation]; + console.log( + 'Metamask sent UserOperation back for patching', + JSON.stringify(userOp), + ); + return await this.#patchUserOperation(userOp); + } case EthMethod.SignUserOperation: { const [userOp] = params as [EthUserOperation]; @@ -628,7 +627,6 @@ export class AccountAbstractionKeyring implements Keyring { const wallet = this.#getWalletByAddress(address); const decryptedPrivateKey = await decrypt(wallet.encryptedPrivateKey); const signer = getSigner(decryptedPrivateKey); - // eslint-disable-next-line camelcase const aaInstance = SimpleAccount__factory.connect( wallet.account.address, // AA address @@ -737,10 +735,10 @@ export class AccountAbstractionKeyring implements Keyring { let preVerificationGasReq = calcPreVerificationGas(partialUserOp); // TODO: (replace) the public bundler on sepolia expects more preVerifGas - if (chainId.toString() === '11155111') { - preVerificationGasReq += 10000; - } - preVerificationGasReq *= overrides?.preVerificationGasReqMultiplier ?? 1; + // if (chainId.toString() === '11155111') { + preVerificationGasReq += 10_000; + // } + preVerificationGasReq *= overrides?.preVerificationGasReqMultiplier ?? 3; // check if calculated preVerificationGas is adequate by calling eth_estimateUserOperationGas on the bundler here @@ -815,6 +813,8 @@ export class AccountAbstractionKeyring implements Keyring { // for general tx show general dialog const sourceAddress = address; // The address sending the transaction + console.log('final eth base userop: ', ethBaseUserOp); + const result = await snap.request({ method: 'snap_dialog', params: { @@ -852,7 +852,7 @@ export class AccountAbstractionKeyring implements Keyring { ); const signedUserOp = await this.#signUserOperation(address, ethBaseUserOp); - console.log(signedUserOp); + console.log('Signed: ', signedUserOp); ethBaseUserOp.signature = signedUserOp!; @@ -861,7 +861,7 @@ export class AccountAbstractionKeyring implements Keyring { await entryPoint.getAddress(), chainConfig.bundlerUrl, ); - console.log(bundlerRes); + if (!bundlerRes.result) { console.log(bundlerRes.error); // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -953,13 +953,47 @@ export class AccountAbstractionKeyring implements Keyring { transaction.value ?? '0x00', transaction.data ?? ethers.ZeroHash, ]), + gasLimits: { + callGasLimit: '0x58a83', + verificationGasLimit: '0xe8c4', + preVerificationGas: '0xc57c', + }, dummySignature: DUMMY_SIGNATURE, - dummyPaymasterAndData: getDummyPaymasterAndData(), + dummyPaymasterAndData: getDummyPaymasterAndData(), // TODO paymaster bundlerUrl: chainConfig.bundlerUrl, }; return ethBaseUserOp; } + async #patchUserOperation(userOperation: EthUserOperation): Promise { + const { chainId } = await provider.getNetwork(); + + const chainConfig = this.#getChainConfig(Number(chainId)); + if (!chainConfig) { + throwError(`Invalid Chain Configuration for ${Number(chainId)}`); + } + + console.log('Estimating User Operation: ', JSON.stringify(userOperation)); + + // TODO estimation is done without the paymasterAndData field + const estimate = await this.#estimateUserOpGas( + userOperation, + chainConfig.entryPoint, + chainConfig.bundlerUrl, + ); + + console.log('UserOperation estimated: ', estimate); + + return { + // TODO paymasterAndData | No paymasterAndData for v.07 allowed but required in docs (?) + // TODO paymasterAndData needs to be submitted to the Chain API + paymasterAndData: '0x', + callGasLimit: estimate.callGasLimit, // ~360k gas + verificationGasLimit: estimate.verificationGasLimit, // ~60k gas + preVerificationGas: estimate.preVerificationGas, // ~50k gas + } as Json; + } + async #sendUserOperation( userOp: any, entryPointAddress: string, @@ -981,7 +1015,7 @@ export class AccountAbstractionKeyring implements Keyring { }); const data = await response.json(); - console.log('Response:', data); + console.log('Response:', JSON.stringify(data)); return data; // Return the data } catch (error) { console.error('Error:', error); @@ -997,10 +1031,23 @@ export class AccountAbstractionKeyring implements Keyring { // for v0.7 EntryPoint this field is not in the UserOperation RPC request const { chainId } = await provider.getNetwork(); const chainConfig = this.#getChainConfig(Number(chainId)); + if (chainConfig?.version !== '0.6.0') { + // @DEV needed for v0.7 operations delete userOp.paymasterAndData; + + if (userOp.initCode.length >= 42) { + userOp.factory = userOp.initCode.substring(0, 42); + userOp.factoryData = `0x${String(userOp.initCode).substring(42)}`; + } } + userOp.callGasLimit = '0x0'; + userOp.verificationGasLimit = '0x0'; + userOp.preVerificationGas = '0x0'; + + console.log('estimate: ', JSON.stringify(userOp)); + const requestBody = { method: 'eth_estimateUserOperationGas', id: 1, @@ -1017,7 +1064,7 @@ export class AccountAbstractionKeyring implements Keyring { }); const data = await response.json(); - console.log('Response:', data); + console.log('Gas Estimation Response', JSON.stringify(data)); if (data.error?.message) { console.error( 'JSON ESTIMATE: ', @@ -1113,6 +1160,57 @@ export class AccountAbstractionKeyring implements Keyring { return signature; } + /** + * Draft method + * -- + * The transaction submitted to MM Flask via prepareUserOperation and patchUserOperation contains + * a paymasterAndData field which likely leads to a AA23 error. If userOP is sent by the snap, + * it succeeds internally, but the Metamask Flask UI labels it as Failed - as it most likely tries to + * send it itself, which does not succeed due to AA23 and the way things are build on the MM side. + * @param address + * @param userOp + * @param chainId + * @param signer + */ + // async #signAndSendUserOperationV07( + // address: string, + // userOp: EthUserOperation, + // ): Promise { + // const wallet = this.#getWalletByAddress(address); + // const decryptedPrivateKey = await decrypt(wallet.encryptedPrivateKey); + // const secureKey = new SecurePrivateKey(decryptedPrivateKey); + // const EP = await entryPoint.getAddress(); + // const { chainId } = await provider.getNetwork(); + // const entryPoint = await this.#getEntryPoint( + // Number(chainId), + // new ethers.Wallet(decryptedPrivateKey), + // ); + // + // userOp.signature = '0x'; + // delete userOp.paymasterAndData; + // + // console.log('Hashing UserOperation: ', JSON.stringify(userOp)); + // + // const userOpHash = getUserOperationHash(userOp, EP, chainId.toString(10)); + // const signature = await secureKey.sign(ethers.getBytes(userOpHash)); + // secureKey.destroy(); + // console.log('UserOp:', userOp); + // console.log('EntryPoint:', EP); + // console.log('Generated signature:', signature); + + // const res = await this.#sendUserOperation( + // { + // ...userOp, + // signature, + // }, + // EP, + // 'https://bundler-hc.sepolia.boba.network', + // ); + // console.log('Broadcasted UP --> ', res); + + // return signature; + // } + async #getAAFactory(chainId: number, signer: ethers.Wallet) { if (!this.#isSupportedChain(chainId)) { throwError(`[Snap] Unsupported chain ID: ${chainId}`); diff --git a/packages/snap/src/permissions.ts b/packages/snap/src/permissions.ts index 3738509..c0aa863 100644 --- a/packages/snap/src/permissions.ts +++ b/packages/snap/src/permissions.ts @@ -64,6 +64,48 @@ export const originPermissions = new Map([ InternalMethod.SendUserOpBobaPM, ], ], + [ + 'https://staging.gateway.boba.network', + [ + // Keyring methods + KeyringRpcMethod.ListAccounts, + KeyringRpcMethod.GetAccount, + KeyringRpcMethod.CreateAccount, + KeyringRpcMethod.FilterAccountChains, + KeyringRpcMethod.UpdateAccount, + KeyringRpcMethod.DeleteAccount, + KeyringRpcMethod.ExportAccount, + KeyringRpcMethod.SubmitRequest, + KeyringRpcMethod.ListRequests, + KeyringRpcMethod.GetRequest, + KeyringRpcMethod.ApproveRequest, + KeyringRpcMethod.RejectRequest, + // Custom methods + InternalMethod.SendUserOpBoba, + InternalMethod.SendUserOpBobaPM, + ], + ], + [ + 'https://stagingv2.gateway.boba.network', + [ + // Keyring methods + KeyringRpcMethod.ListAccounts, + KeyringRpcMethod.GetAccount, + KeyringRpcMethod.CreateAccount, + KeyringRpcMethod.FilterAccountChains, + KeyringRpcMethod.UpdateAccount, + KeyringRpcMethod.DeleteAccount, + KeyringRpcMethod.ExportAccount, + KeyringRpcMethod.SubmitRequest, + KeyringRpcMethod.ListRequests, + KeyringRpcMethod.GetRequest, + KeyringRpcMethod.ApproveRequest, + KeyringRpcMethod.RejectRequest, + // Custom methods + InternalMethod.SendUserOpBoba, + InternalMethod.SendUserOpBobaPM, + ], + ], [ 'https://hc-wallet.sepolia.boba.network', [ @@ -127,4 +169,25 @@ export const originPermissions = new Map([ InternalMethod.SendUserOpBobaPM, ], ], + [ + 'https://codecaster.onrender.com', + [ + // Keyring methods + KeyringRpcMethod.ListAccounts, + KeyringRpcMethod.GetAccount, + KeyringRpcMethod.CreateAccount, + KeyringRpcMethod.FilterAccountChains, + KeyringRpcMethod.UpdateAccount, + KeyringRpcMethod.DeleteAccount, + KeyringRpcMethod.ExportAccount, + KeyringRpcMethod.SubmitRequest, + KeyringRpcMethod.ListRequests, + KeyringRpcMethod.GetRequest, + KeyringRpcMethod.ApproveRequest, + KeyringRpcMethod.RejectRequest, + // Custom methods + InternalMethod.SendUserOpBoba, + InternalMethod.SendUserOpBobaPM, + ], + ], ]); diff --git a/packages/snap/src/utils/wallets.json b/packages/snap/src/utils/wallets.json new file mode 100644 index 0000000..e69de29