From d2e39c1451baa33a2a9f64f52c227019788c84f4 Mon Sep 17 00:00:00 2001 From: Gamaliel Padillo Date: Wed, 8 Jan 2025 11:51:46 -0800 Subject: [PATCH 01/23] feat: add backup page --- packages/extension/src/ui/onboard/App.vue | 4 +- .../src/ui/onboard/create-wallet/routes.ts | 1 + .../restore-wallet/backup-detected.vue | 125 ++++++++++++++++++ .../src/ui/onboard/restore-wallet/routes.ts | 6 + .../onboard/restore-wallet/type-password.vue | 2 +- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue diff --git a/packages/extension/src/ui/onboard/App.vue b/packages/extension/src/ui/onboard/App.vue index c85392d11..ac95a7bdd 100644 --- a/packages/extension/src/ui/onboard/App.vue +++ b/packages/extension/src/ui/onboard/App.vue @@ -46,6 +46,7 @@ const isShowBackButton = () => { route.name != 'user-analytics' && route.name != 'create-wallet-wallet-ready' && route.name != 'restore-wallet-wallet-ready' && + route.name != 'restore-wallet-backup-detected' && !(route.name as string).includes('hardware-wallet') ); }; @@ -54,7 +55,8 @@ const wrapClassObject = () => { return { 'onboard__wrap--ready': route.name == 'create-wallet-wallet-ready' || - route.name == 'restore-wallet-wallet-ready', + route.name == 'restore-wallet-wallet-ready' || + route.name == 'restore-wallet-backup-detected', 'onboard__wrap--auto-height': route.path.match(/hardware-wallet/), }; }; diff --git a/packages/extension/src/ui/onboard/create-wallet/routes.ts b/packages/extension/src/ui/onboard/create-wallet/routes.ts index 6595098b2..aa5f6b789 100644 --- a/packages/extension/src/ui/onboard/create-wallet/routes.ts +++ b/packages/extension/src/ui/onboard/create-wallet/routes.ts @@ -5,6 +5,7 @@ import CheckPhrase from './double-check-phrase.vue'; import WalletReady from './wallet-ready.vue'; import UserAnalytics from '../user-analytics.vue'; import { RouteRecordRaw } from 'vue-router'; + export const routes = { pickPassword: { path: 'pick-password', diff --git a/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue new file mode 100644 index 000000000..38f2c9206 --- /dev/null +++ b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue @@ -0,0 +1,125 @@ + + + + diff --git a/packages/extension/src/ui/onboard/restore-wallet/routes.ts b/packages/extension/src/ui/onboard/restore-wallet/routes.ts index a86a6a640..fdee17e79 100644 --- a/packages/extension/src/ui/onboard/restore-wallet/routes.ts +++ b/packages/extension/src/ui/onboard/restore-wallet/routes.ts @@ -5,6 +5,7 @@ import PickPassword from './pick-password.vue'; import TypePassword from './type-password.vue'; import WalletReady from '../create-wallet/wallet-ready.vue'; import UserAnalytics from '../user-analytics.vue'; +import BackupDetected from './backup-detected.vue'; import { RouteRecordRaw } from 'vue-router'; export const routes = { start: { @@ -37,6 +38,11 @@ export const routes = { name: 'user-analytics', component: UserAnalytics, }, + backupDetected: { + path: 'backup-detected', + name: 'backup-detected', + component: BackupDetected, + }, walletReady: { path: 'wallet-ready', name: 'wallet-ready', diff --git a/packages/extension/src/ui/onboard/restore-wallet/type-password.vue b/packages/extension/src/ui/onboard/restore-wallet/type-password.vue index b785ccbfb..ed212c892 100644 --- a/packages/extension/src/ui/onboard/restore-wallet/type-password.vue +++ b/packages/extension/src/ui/onboard/restore-wallet/type-password.vue @@ -45,7 +45,7 @@ const nextAction = () => { onboardInitializeWallets(store.mnemonic, store.password).then(() => { isInitializing.value = false; router.push({ - name: routes.walletReady.name, + name: routes.backupDetected.name, }); }); } From e81cd7b2a5169f54601dae2df458d3b0997eaf4c Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:27:35 -0800 Subject: [PATCH 02/23] devop: backups are functioning --- .../src/libs/backup-state/configs.ts | 3 + .../extension/src/libs/backup-state/index.ts | 276 ++++++++++++++++++ .../extension/src/libs/backup-state/types.ts | 24 ++ .../src/libs/keyring/public-keyring.ts | 8 + .../src/libs/utils/initialize-wallet.ts | 31 +- .../src/providers/ethereum/networks/ftm.ts | 2 +- packages/extension/src/types/provider.ts | 3 +- packages/extension/src/ui/action/App.vue | 5 +- .../accounts/components/add-account-form.vue | 5 + .../views/swap/libs/send-transactions.ts | 38 +-- .../restore-wallet/backup-detected.vue | 83 ++++-- .../onboard/restore-wallet/type-password.vue | 14 +- packages/signers/ethereum/src/index.ts | 9 +- packages/signers/ethereum/src/utils.ts | 47 --- packages/types/src/index.ts | 1 + packages/utils/package.json | 1 + packages/utils/src/index.ts | 12 + packages/utils/src/nacl-encrypt-decrypt.ts | 119 ++++++++ yarn.lock | 1 + 19 files changed, 584 insertions(+), 98 deletions(-) create mode 100644 packages/extension/src/libs/backup-state/configs.ts create mode 100644 packages/extension/src/libs/backup-state/index.ts create mode 100644 packages/extension/src/libs/backup-state/types.ts delete mode 100644 packages/signers/ethereum/src/utils.ts create mode 100644 packages/utils/src/nacl-encrypt-decrypt.ts diff --git a/packages/extension/src/libs/backup-state/configs.ts b/packages/extension/src/libs/backup-state/configs.ts new file mode 100644 index 000000000..0f7ac2d1d --- /dev/null +++ b/packages/extension/src/libs/backup-state/configs.ts @@ -0,0 +1,3 @@ +const BACKUP_URL = 'https://rabbit.ethvm.dev/'; + +export { BACKUP_URL }; diff --git a/packages/extension/src/libs/backup-state/index.ts b/packages/extension/src/libs/backup-state/index.ts new file mode 100644 index 000000000..ec1cf8a2d --- /dev/null +++ b/packages/extension/src/libs/backup-state/index.ts @@ -0,0 +1,276 @@ +import BrowserStorage from '../common/browser-storage'; +import { InternalStorageNamespace } from '@/types/provider'; +import { + BackupData, + BackupResponseType, + BackupType, + IState, + StorageKeys, +} from './types'; +import PublicKeyRing from '../keyring/public-keyring'; +import sendUsingInternalMessengers from '../messenger/internal-messenger'; +import { InternalMethods } from '@/types/messenger'; +import EthNetwork from '@/providers/ethereum/networks/eth'; +import { + bufferToHex, + hexToBuffer, + NACL_VERSION, + naclEncrypt, +} from '@enkryptcom/utils'; +import { hashPersonalMessage } from '@ethereumjs/util'; +import { v4 as uuidv4 } from 'uuid'; +import { BACKUP_URL } from './configs'; +import { EnkryptAccount, SignerType, WalletType } from '@enkryptcom/types'; +import KeyRingBase from '../keyring/keyring'; + +class BackupState { + private storage: BrowserStorage; + + constructor() { + this.storage = new BrowserStorage(InternalStorageNamespace.backupState); + } + + async getMainWallet(): Promise { + const pkr = new PublicKeyRing(); + const allAccounts = await pkr.getAccounts(); + const mainWallet = allAccounts.find( + acc => + acc.walletType === 'mnemonic' && + acc.pathIndex === 0 && + acc.signerType === 'secp256k1' && + acc.basePath === EthNetwork.basePath, + ); + if (!mainWallet) { + throw new Error('No main wallet found'); + } + return mainWallet; + } + + async getBackups(pubkey?: string): Promise { + if (!pubkey) { + const mainWallet = await this.getMainWallet(); + pubkey = mainWallet.publicKey; + } + const rawResponse = await fetch(`${BACKUP_URL}backups/${pubkey}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }); + const content: BackupResponseType = await rawResponse.json(); + return content.backups; + } + + async restoreBackup( + backup: BackupType, + keyringPassword: string, + ): Promise { + const mainWallet = await this.getMainWallet(); + await sendUsingInternalMessengers({ + method: InternalMethods.unlock, + params: [keyringPassword, false], + }); + await sendUsingInternalMessengers({ + method: InternalMethods.ethereumDecrypt, + params: [backup.payload, mainWallet], + }).then(async res => { + if (res.error) { + console.error(res); + return null; + } else { + const kr = new KeyRingBase(); + await kr.unlock(keyringPassword); + const existingAccounts = await kr.getKeysArray(); + const decryptedBackup: BackupData = JSON.parse( + JSON.parse(res.result as string), + ); + console.log(decryptedBackup); + const highestPathIndex: Record = {}; + decryptedBackup.accounts.forEach(acc => { + const id = `${acc.basePath}###${acc.signerType}`; + const idx = acc.pathIndex; + if (!highestPathIndex[id] || highestPathIndex[id] < idx) { + highestPathIndex[id] = idx; + } + }); + + console.log(highestPathIndex); + const getAccountByIndex = ( + accounts: Omit[], + basePath: string, + signerType: SignerType, + idx: number, + ): EnkryptAccount | null => { + for (const acc of accounts) { + if ( + acc.basePath === basePath && + acc.pathIndex === idx && + acc.signerType === signerType + ) { + return acc as EnkryptAccount; + } + } + return null; + }; + + for (const key of Object.keys(highestPathIndex)) { + const [basePath, signerType] = key.split('###'); + for (let i = 0; i <= highestPathIndex[key]; i++) { + const newAccount = getAccountByIndex( + decryptedBackup.accounts, + basePath, + signerType as SignerType, + i, + ); + const existingAccount = getAccountByIndex( + existingAccounts, + basePath, + signerType as SignerType, + i, + ); + if (existingAccount && newAccount) { + console.log('Account already exists, just renaming'); + await kr.renameAccount(existingAccount.address, newAccount.name); + continue; + } else if (newAccount) { + console.log('creating new account', newAccount); + await kr.saveNewAccount({ + basePath: newAccount.basePath, + name: newAccount.name, + signerType: newAccount.signerType, + walletType: newAccount.walletType, + }); + } else if (!newAccount) { + console.log('edge case shouldnt happen', newAccount); + await kr.saveNewAccount({ + basePath: basePath, + name: `New Account from backup ${i}`, + signerType: signerType as SignerType, + walletType: WalletType.mnemonic, + }); + } + } + } + await this.setUserId(decryptedBackup.uuid); + } + }); + } + + async backup(firstTime: boolean): Promise { + const state = await this.getState(); + if (firstTime && state.lastBackupTime !== 0) { + return true; + } + const pkr = new PublicKeyRing(); + const allAccounts = await pkr.getAccounts(); + const mainWallet = await this.getMainWallet(); + const backupData: BackupData = { + accounts: allAccounts + .filter( + acc => !acc.isTestWallet && acc.walletType !== WalletType.privkey, + ) + .map(acc => { + return { + basePath: acc.basePath, + pathIndex: acc.pathIndex, + name: acc.name, + signerType: acc.signerType, + walletType: acc.walletType, + isHardware: acc.isHardware, + HWOptions: acc.HWOptions, + }; + }), + uuid: state.userId, + }; + const encryptPubKey = await sendUsingInternalMessengers({ + method: InternalMethods.getEthereumEncryptionPublicKey, + params: [mainWallet], + }).then(res => { + if (res.error) { + console.error(res); + return null; + } else { + return JSON.parse(res.result as string); + } + }); + if (!encryptPubKey) { + console.error('No encrypt public key found'); + return false; + } + const encryptedStr = naclEncrypt({ + publicKey: encryptPubKey, + data: JSON.stringify(backupData), + version: NACL_VERSION, + }); + const msgHash = bufferToHex(hashPersonalMessage(hexToBuffer(encryptedStr))); + return sendUsingInternalMessengers({ + method: InternalMethods.sign, + params: [msgHash, mainWallet], + }).then(async res => { + if (res.error) { + console.error(res); + return false; + } else { + const rawResponse = await fetch( + `${BACKUP_URL}backups/${mainWallet.publicKey}/${state.userId}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + payload: encryptedStr, + signature: JSON.parse(res.result as string), + }), + }, + ); + const content = await rawResponse.json(); + if (content.message === 'Ok') { + await this.setState({ + lastBackupTime: new Date().getTime(), + userId: state.userId, + }); + return true; + } + console.error(content); + return false; + } + }); + } + + async setState(state: IState): Promise { + return this.storage.set(StorageKeys.backupInfo, state); + } + + async getState(): Promise { + const state = await this.storage.get(StorageKeys.backupInfo); + if (!state) { + const newState: IState = { + lastBackupTime: 0, + userId: uuidv4(), + }; + await this.setState(newState); + return newState; + } + return state; + } + + async getLastUpdatedTime(): Promise { + const state: IState = await this.getState(); + return new Date(state.lastBackupTime); + } + + async getUserId(): Promise { + const state: IState = await this.getState(); + return state.userId; + } + + async setUserId(userId: string): Promise { + const state: IState = await this.getState(); + await this.setState({ ...state, userId }); + } +} + +export default BackupState; diff --git a/packages/extension/src/libs/backup-state/types.ts b/packages/extension/src/libs/backup-state/types.ts new file mode 100644 index 000000000..80acb885f --- /dev/null +++ b/packages/extension/src/libs/backup-state/types.ts @@ -0,0 +1,24 @@ +import { EnkryptAccount } from '@enkryptcom/types'; + +export enum StorageKeys { + backupInfo = 'backup-info', +} + +export interface IState { + lastBackupTime: number; + userId: string; +} + +export interface BackupType { + userId: string; + payload: string; + updatedAt: string; +} +export interface BackupResponseType { + backups: BackupType[]; +} + +export interface BackupData { + accounts: Omit[]; + uuid: string; +} diff --git a/packages/extension/src/libs/keyring/public-keyring.ts b/packages/extension/src/libs/keyring/public-keyring.ts index bdfd73589..e5cbb58b6 100644 --- a/packages/extension/src/libs/keyring/public-keyring.ts +++ b/packages/extension/src/libs/keyring/public-keyring.ts @@ -23,6 +23,7 @@ class PublicKeyRing { signerType: SignerType.secp256k1, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; allKeys['0xb1ea5a3e5ea7fa1834d48058ecda26d8c59e8251'] = { address: '0xb1ea5a3e5ea7fa1834d48058ecda26d8c59e8251', //optimism nfts @@ -33,6 +34,7 @@ class PublicKeyRing { signerType: SignerType.secp256k1, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; allKeys['0xe5dc07bdcdb8c98850050c7f67de7e164b1ea391'] = { address: '0xe5dc07bdcdb8c98850050c7f67de7e164b1ea391', @@ -43,6 +45,7 @@ class PublicKeyRing { signerType: SignerType.secp256k1, walletType: WalletType.ledger, isHardware: true, + isTestWallet: true, }; allKeys['5E56EZk6jmpq1q3Har3Ms99D9TLN9ra2inFh7Q1Hj6GpUx6D'] = { address: '5E56EZk6jmpq1q3Har3Ms99D9TLN9ra2inFh7Q1Hj6GpUx6D', @@ -53,6 +56,7 @@ class PublicKeyRing { signerType: SignerType.sr25519, walletType: WalletType.ledger, isHardware: true, + isTestWallet: true, }; allKeys['5E56EZk6jmpq1q3Har3Ms99D9TLN9ra2inFh7Q1Hj6GpUx6D'] = { address: '5CFnoCsP3pDK2thhSqYPwKELJFLQ1hBodqzSUypexyh7eHkB', @@ -63,6 +67,7 @@ class PublicKeyRing { signerType: SignerType.sr25519, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; allKeys[ 'bc1puzz9tmxawd7zdd7klfgtywrgpma3u22fz5ecxhucd4j8tygqe5ms2vdd9y' @@ -76,6 +81,7 @@ class PublicKeyRing { signerType: SignerType.secp256k1btc, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; allKeys['77hREDDaAiimedtD9bR1JDMgYLW3AA5yPvD91pvrueRp'] = { address: '77hREDDaAiimedtD9bR1JDMgYLW3AA5yPvD91pvrueRp', @@ -86,6 +92,7 @@ class PublicKeyRing { signerType: SignerType.ed25519sol, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; allKeys['tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP'] = { address: 'tQvduDby4rvC6VU4rSirhVWuRYxbJz3rvUrVMkUWsZP', @@ -96,6 +103,7 @@ class PublicKeyRing { signerType: SignerType.ed25519sol, walletType: WalletType.mnemonic, isHardware: false, + isTestWallet: true, }; } return allKeys; diff --git a/packages/extension/src/libs/utils/initialize-wallet.ts b/packages/extension/src/libs/utils/initialize-wallet.ts index d5978605a..b205af19d 100644 --- a/packages/extension/src/libs/utils/initialize-wallet.ts +++ b/packages/extension/src/libs/utils/initialize-wallet.ts @@ -6,12 +6,23 @@ import KadenaNetworks from '@/providers/kadena/networks'; import SolanaNetworks from '@/providers/solana/networks'; import { NetworkNames, WalletType } from '@enkryptcom/types'; import { getAccountsByNetworkName } from '@/libs/utils/accounts'; +import BackupState from '../backup-state'; export const initAccounts = async (keyring: KeyRing) => { - const secp256k1btc = await getAccountsByNetworkName(NetworkNames.Bitcoin); - const secp256k1 = await getAccountsByNetworkName(NetworkNames.Ethereum); - const sr25519 = await getAccountsByNetworkName(NetworkNames.Polkadot); - const ed25519kda = await getAccountsByNetworkName(NetworkNames.Kadena); - const ed25519sol = await getAccountsByNetworkName(NetworkNames.Solana); + const secp256k1btc = ( + await getAccountsByNetworkName(NetworkNames.Bitcoin) + ).filter(acc => !acc.isTestWallet); + const secp256k1 = ( + await getAccountsByNetworkName(NetworkNames.Ethereum) + ).filter(acc => !acc.isTestWallet); + const sr25519 = ( + await getAccountsByNetworkName(NetworkNames.Polkadot) + ).filter(acc => !acc.isTestWallet); + const ed25519kda = ( + await getAccountsByNetworkName(NetworkNames.Kadena) + ).filter(acc => !acc.isTestWallet); + const ed25519sol = ( + await getAccountsByNetworkName(NetworkNames.Solana) + ).filter(acc => !acc.isTestWallet); if (secp256k1.length == 0) await keyring.saveNewAccount({ basePath: EthereumNetworks.ethereum.basePath, @@ -51,7 +62,15 @@ export const initAccounts = async (keyring: KeyRing) => { export const onboardInitializeWallets = async ( mnemonic: string, password: string, -): Promise => { +): Promise<{ backupsFound: boolean }> => { const kr = new KeyRing(); + const backupsState = new BackupState(); await kr.init(mnemonic, password); + await kr.unlock(password); + const mainAccount = await kr.getNewAccount({ + basePath: EthereumNetworks.ethereum.basePath, + signerType: EthereumNetworks.ethereum.signer[0], + }); + const backups = await backupsState.getBackups(mainAccount.publicKey); + return { backupsFound: backups.length > 0 }; }; diff --git a/packages/extension/src/providers/ethereum/networks/ftm.ts b/packages/extension/src/providers/ethereum/networks/ftm.ts index 4c3f109f3..c21c31abb 100644 --- a/packages/extension/src/providers/ethereum/networks/ftm.ts +++ b/packages/extension/src/providers/ethereum/networks/ftm.ts @@ -15,7 +15,7 @@ const ftmOptions: EvmNetworkOptions = { isTestNetwork: false, currencyName: 'FTM', currencyNameLong: 'Fantom', - node: 'https://rpc.ankr.com/fantom/', + node: 'wss://fantom.callstaticrpc.com', icon, coingeckoID: 'fantom', coingeckoPlatform: CoingeckoPlatform.Fantom, diff --git a/packages/extension/src/types/provider.ts b/packages/extension/src/types/provider.ts index 601e67b47..862fb1c81 100644 --- a/packages/extension/src/types/provider.ts +++ b/packages/extension/src/types/provider.ts @@ -53,6 +53,7 @@ export enum InternalStorageNamespace { rateState = 'RateState', recentlySentAddresses = 'RecentlySentAddresses', updatesState = 'UpdatesState', + backupState = 'BackupState', } export enum EnkryptProviderEventMethods { persistentEvents = 'PersistentEvents', @@ -130,7 +131,7 @@ export abstract class BackgroundProviderInterface extends EventEmitter { export abstract class ProviderAPIInterface { abstract node: string; // eslint-disable-next-line @typescript-eslint/no-unused-vars - constructor(node: string, options?: unknown) { } + constructor(node: string, options?: unknown) {} abstract init(): Promise; abstract getBalance(address: string): Promise; abstract getTransactionStatus( diff --git a/packages/extension/src/ui/action/App.vue b/packages/extension/src/ui/action/App.vue index 0ade5518e..c8583612e 100644 --- a/packages/extension/src/ui/action/App.vue +++ b/packages/extension/src/ui/action/App.vue @@ -192,11 +192,13 @@ import UpdatedIcon from '@/ui/action/icons/updates/updated.vue'; import HeartIcon from '@/ui/action/icons/updates/heart.vue'; import { getLatestEnkryptUpdates } from '@action/utils/browser'; import { Updates } from '@/ui/action/types/updates'; +import BackupState from '@/libs/backup-state'; const domainState = new DomainState(); const networksState = new NetworksState(); const rateState = new RateState(); const updatesState = new UpdatesState(); +const backupState = new BackupState(); const appMenuRef = ref(null); const showDepositWindow = ref(false); const accountHeaderData = ref({ @@ -369,7 +371,7 @@ const openBuyPage = () => { case NetworkNames.SyscoinNEVM: case NetworkNames.Rollux: return `${(currentNetwork.value as EvmNetwork).options.buyLink}&address=${currentNetwork.value.displayAddress( - accountHeaderData.value.selectedAccount!.address + accountHeaderData.value.selectedAccount!.address, )}`; case NetworkNames.SyscoinNEVMTest: case NetworkNames.RolluxTest: @@ -421,6 +423,7 @@ onMounted(async () => { .then(() => (isLoading.value = false)); } else { init(); + backupState.backup(true).catch(console.error); setTimeout(() => { rateState.showPopup().then(show => { if (show) { diff --git a/packages/extension/src/ui/action/views/accounts/components/add-account-form.vue b/packages/extension/src/ui/action/views/accounts/components/add-account-form.vue index fac1b9ea9..c907ce028 100644 --- a/packages/extension/src/ui/action/views/accounts/components/add-account-form.vue +++ b/packages/extension/src/ui/action/views/accounts/components/add-account-form.vue @@ -54,6 +54,7 @@ import { sendToBackgroundFromAction } from '@/libs/messenger/extension'; import { InternalMethods } from '@/types/messenger'; import { EnkryptAccount, KeyRecordAdd, WalletType } from '@enkryptcom/types'; import Keyring from '@/libs/keyring/public-keyring'; +import BackupState from '@/libs/backup-state'; const isFocus = ref(false); const accountName = ref(''); @@ -120,6 +121,10 @@ const addAccount = async () => { params: [keyReq], }), }).then(() => { + const backupState = new BackupState(); + backupState.backup(false).catch(() => { + console.error('Failed to backup'); + }); emit('update:init'); emit('window:close'); }); diff --git a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts index 192357bb7..d5da3724a 100644 --- a/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts +++ b/packages/extension/src/ui/action/views/swap/libs/send-transactions.ts @@ -75,7 +75,7 @@ const getBaseActivity = (options: ExecuteSwapOptions): Activity => { */ export const executeSwap = async ( options: ExecuteSwapOptions, -): Promise<{ hash: string, sentAt: number }[]> => { +): Promise<{ hash: string; sentAt: number }[]> => { const activityState = new ActivityState(); const api = await options.network.api(); if (options.networkType === NetworkType.Bitcoin) { @@ -173,12 +173,12 @@ export const executeSwap = async ( { address: txActivity.from, network: options.network.name }, ); }); - return [{ hash, sentAt: Date.now(), }]; + return [{ hash, sentAt: Date.now() }]; } else if (options.networkType === NetworkType.Solana) { // Execute the swap on Solana const conn = (api as SolanaAPI).api.web3; - const solTxs: { hash: string, sentAt: number, }[] = []; + const solTxs: { hash: string; sentAt: number }[] = []; /** Enkrypt representation of the swap transactions */ const enkSolTxs = options.swap.transactions as EnkryptSolanaTransaction[]; @@ -219,7 +219,7 @@ export const executeSwap = async ( // Might need to update the block hash console.warn( `Failed to get fee for legacy transaction while checking` + - ` whether to update block hash: ${String(err)}`, + ` whether to update block hash: ${String(err)}`, ); shouldUpdateBlockHash = true; } @@ -228,7 +228,7 @@ export const executeSwap = async ( if (shouldUpdateBlockHash) { console.warn( `Unsigned legacy transaction might have an` + - ` out-of-date block hash, trying to update it...`, + ` out-of-date block hash, trying to update it...`, ); const backoff = [0, 500, 1_000, 2_000]; let backoffi = 0; @@ -238,7 +238,7 @@ export const executeSwap = async ( // Just continue and hope for the best with old block hash... console.warn( `Failed to get latest blockhash after ${backoffi} attempts,` + - ` continuing with old block hash for legacy transaction...`, + ` continuing with old block hash for legacy transaction...`, ); break update_block_hash; } @@ -246,7 +246,7 @@ export const executeSwap = async ( if (backoffMs > 0) { console.warn( `Waiting ${backoffMs}ms before retrying latest block` + - ` hash for legacy transaction...`, + ` hash for legacy transaction...`, ); await new Promise(res => setTimeout(res, backoffMs)); } @@ -257,7 +257,7 @@ export const executeSwap = async ( } catch (err) { console.warn( `Failed to get latest blockhash on attempt` + - ` ${backoffi + 1}: ${String(err)}`, + ` ${backoffi + 1}: ${String(err)}`, ); } backoffi++; @@ -330,7 +330,7 @@ export const executeSwap = async ( // Might need to update the block hash console.warn( `Failed to get fee for versioned transaction while checking` + - ` whether to update block hash: ${String(err)}`, + ` whether to update block hash: ${String(err)}`, ); shouldUpdateBlockHash = true; } @@ -339,7 +339,7 @@ export const executeSwap = async ( if (shouldUpdateBlockHash) { console.warn( `Unsigned versioned transaction might have an` + - ` out-of-date block hash, trying to update it...`, + ` out-of-date block hash, trying to update it...`, ); const backoff = [0, 500, 1_000, 2_000]; let backoffi = 0; @@ -349,7 +349,7 @@ export const executeSwap = async ( // Just continue and hope for the best with old block hash... console.warn( `Failed to get latest blockhash after ${backoffi} attempts,` + - ` continuing with old block hash for versioned transaction...`, + ` continuing with old block hash for versioned transaction...`, ); break update_block_hash; } @@ -357,7 +357,7 @@ export const executeSwap = async ( if (backoffMs > 0) { console.warn( `Waiting ${backoffMs}ms before retrying latest block` + - ` hash for versioned transaction...`, + ` hash for versioned transaction...`, ); await new Promise(res => setTimeout(res, backoffMs)); } @@ -368,7 +368,7 @@ export const executeSwap = async ( } catch (err) { console.warn( `Failed to get latest blockhash on attempt` + - ` ${backoffi + 1}: ${String(err)}`, + ` ${backoffi + 1}: ${String(err)}`, ); } backoffi++; @@ -434,8 +434,8 @@ export const executeSwap = async ( ); throw new Error( 'Failed to send Solana swap transaction: blockhash not found.' + - ' Too much time may have passed between the creation and sending' + - ' of the transaction', + ' Too much time may have passed between the creation and sending' + + ' of the transaction', ); } @@ -454,7 +454,7 @@ export const executeSwap = async ( } else { console.error( `Failed to send Solana swap transaction,` + - ` unhandled error ${(err as Error).name}`, + ` unhandled error ${(err as Error).name}`, ); } // Solana transactions can have big errors @@ -475,7 +475,7 @@ export const executeSwap = async ( network: options.network.name, }); - solTxs.push({ hash: txHash, sentAt: Date.now(), }); + solTxs.push({ hash: txHash, sentAt: Date.now() }); } // Finished executing the swap on Solana @@ -504,7 +504,7 @@ export const executeSwap = async ( ); const txs = await Promise.all(txsPromises); /** Hashes of transactions successfully sent & mined, in order of execution */ - const txPromises: { hash: `0x${string}`, sentAt: number, }[] = []; + const txPromises: { hash: `0x${string}`; sentAt: number }[] = []; for (const txInfo of txs) { // Submit each transaction, in-order one-by-one @@ -566,7 +566,7 @@ export const executeSwap = async ( ); }), ); - txPromises.push({ hash, sentAt: Date.now(), }); + txPromises.push({ hash, sentAt: Date.now() }); } return txPromises; } else { diff --git a/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue index 38f2c9206..3869aa118 100644 --- a/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue +++ b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue @@ -7,21 +7,26 @@ - + @@ -29,26 +34,70 @@ diff --git a/packages/extension/src/ui/onboard/restore-wallet/type-password.vue b/packages/extension/src/ui/onboard/restore-wallet/type-password.vue index ed212c892..2fc15603e 100644 --- a/packages/extension/src/ui/onboard/restore-wallet/type-password.vue +++ b/packages/extension/src/ui/onboard/restore-wallet/type-password.vue @@ -42,11 +42,17 @@ const isInitializing = ref(false); const nextAction = () => { if (!isDisabled.value) { isInitializing.value = true; - onboardInitializeWallets(store.mnemonic, store.password).then(() => { + onboardInitializeWallets(store.mnemonic, store.password).then(res => { isInitializing.value = false; - router.push({ - name: routes.backupDetected.name, - }); + if (res.backupsFound) { + router.push({ + name: routes.backupDetected.name, + }); + } else { + router.push({ + name: routes.walletReady.name, + }); + } }); } }; diff --git a/packages/signers/ethereum/src/index.ts b/packages/signers/ethereum/src/index.ts index c44392d96..248010cc8 100644 --- a/packages/signers/ethereum/src/index.ts +++ b/packages/signers/ethereum/src/index.ts @@ -8,11 +8,16 @@ import { } from "@ethereumjs/util"; import { mnemonicToSeed } from "bip39"; import { Errors, SignerInterface, KeyPair } from "@enkryptcom/types"; -import { hexToBuffer, bufferToHex } from "@enkryptcom/utils"; +import { + hexToBuffer, + bufferToHex, + encryptedDataStringToJson, + naclDecodeHex, + naclDecrypt, +} from "@enkryptcom/utils"; import HDkey from "hdkey"; import { box as naclBox } from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import { encryptedDataStringToJson, naclDecodeHex, naclDecrypt } from "./utils"; export class EthereumSigner implements SignerInterface { async generate(mnemonic: string, derivationPath = ""): Promise { diff --git a/packages/signers/ethereum/src/utils.ts b/packages/signers/ethereum/src/utils.ts deleted file mode 100644 index 5bbbfb0aa..000000000 --- a/packages/signers/ethereum/src/utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { EthEncryptedData } from "@enkryptcom/types"; -import { hexToBuffer } from "@enkryptcom/utils"; -import { decodeBase64, encodeUTF8 } from "tweetnacl-util"; -import { box as naclBox } from "tweetnacl"; - -const naclDecodeHex = (msgHex: string): Uint8Array => - decodeBase64(hexToBuffer(msgHex).toString("base64")); - -const encryptedDataStringToJson = (strData: string): EthEncryptedData => { - const buf = hexToBuffer(strData); - return JSON.parse(buf.toString("utf8")); -}; -const naclDecrypt = ({ - encryptedData, - privateKey, -}: { - encryptedData: EthEncryptedData; - privateKey: string; -}): string => { - switch (encryptedData.version) { - case "x25519-xsalsa20-poly1305": { - const recieverPrivateKeyUint8Array = naclDecodeHex(privateKey); - const recieverEncryptionPrivateKey = naclBox.keyPair.fromSecretKey( - recieverPrivateKeyUint8Array, - ).secretKey; - const nonce = decodeBase64(encryptedData.nonce); - const ciphertext = decodeBase64(encryptedData.ciphertext); - const ephemPublicKey = decodeBase64(encryptedData.ephemPublicKey); - const decryptedMessage = naclBox.open( - ciphertext, - nonce, - ephemPublicKey, - recieverEncryptionPrivateKey, - ); - let output; - try { - output = encodeUTF8(decryptedMessage); - return output; - } catch (err) { - throw new Error("Decryption failed."); - } - } - default: - throw new Error("Encryption type/version not supported."); - } -}; -export { naclDecodeHex, encryptedDataStringToJson, naclDecrypt }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index e93fe1d48..de3dcb0fe 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -71,6 +71,7 @@ interface HWwalletOptions { interface EnkryptAccount extends KeyRecord { isHardware: boolean; + isTestWallet?: boolean; HWOptions?: HWwalletOptions; } diff --git a/packages/utils/package.json b/packages/utils/package.json index 1d9aba1db..fb55254b3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -43,6 +43,7 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsup": "^8.3.5", + "tweetnacl-util": "^0.15.1", "typescript": "^5.6.3", "typescript-eslint": "8.14.0", "vitest": "^2.1.4" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0a68c2efd..8159f2964 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -11,6 +11,13 @@ import { encrypt, decrypt } from "./encrypt"; import MemoryStorage from "./memory-storage"; import { fromBase, toBase, isValidDecimals } from "./units"; import { DebugLogger } from "./debug-logger"; +import { + naclDecodeHex, + encryptedDataStringToJson, + naclDecrypt, + naclEncrypt, + NACL_VERSION, +} from "./nacl-encrypt-decrypt"; const bufferToHex = (buf: Buffer | Uint8Array, nozerox = false): string => nozerox @@ -42,4 +49,9 @@ export { toBase, isValidDecimals, DebugLogger, + naclDecodeHex, + encryptedDataStringToJson, + naclDecrypt, + naclEncrypt, + NACL_VERSION, }; diff --git a/packages/utils/src/nacl-encrypt-decrypt.ts b/packages/utils/src/nacl-encrypt-decrypt.ts new file mode 100644 index 000000000..d09a8fa51 --- /dev/null +++ b/packages/utils/src/nacl-encrypt-decrypt.ts @@ -0,0 +1,119 @@ +import { EthEncryptedData } from "@enkryptcom/types"; +import { box as naclBox, randomBytes } from "tweetnacl"; +import { + decodeBase64, + encodeUTF8, + decodeUTF8, + encodeBase64, +} from "tweetnacl-util"; +import { hexToBuffer, utf8ToHex } from "."; + +const NACL_VERSION = "x25519-xsalsa20-poly1305"; + +const naclDecodeHex = (msgHex: string): Uint8Array => + decodeBase64(hexToBuffer(msgHex).toString("base64")); + +const encryptedDataStringToJson = (strData: string): EthEncryptedData => { + const buf = hexToBuffer(strData); + return JSON.parse(buf.toString("utf8")); +}; + +const JsonToEncryptedDataString = (strData: EthEncryptedData): string => { + const hex = utf8ToHex(JSON.stringify(strData)); + return hex; +}; + +const naclDecrypt = ({ + encryptedData, + privateKey, +}: { + encryptedData: EthEncryptedData; + privateKey: string; +}): string => { + switch (encryptedData.version) { + case NACL_VERSION: { + const recieverPrivateKeyUint8Array = naclDecodeHex(privateKey); + const recieverEncryptionPrivateKey = naclBox.keyPair.fromSecretKey( + recieverPrivateKeyUint8Array, + ).secretKey; + const nonce = decodeBase64(encryptedData.nonce); + const ciphertext = decodeBase64(encryptedData.ciphertext); + const ephemPublicKey = decodeBase64(encryptedData.ephemPublicKey); + const decryptedMessage = naclBox.open( + ciphertext, + nonce, + ephemPublicKey, + recieverEncryptionPrivateKey, + ); + let output; + try { + output = encodeUTF8(decryptedMessage); + return output; + } catch (err) { + throw new Error("Decryption failed."); + } + } + default: + throw new Error("Encryption type/version not supported."); + } +}; + +const naclEncrypt = ({ + publicKey, + data, + version, +}: { + publicKey: string; + data: unknown; + version: string; +}): string => { + if (!publicKey) { + throw new Error("Missing publicKey parameter"); + } else if (!data) { + throw new Error("Missing data parameter"); + } else if (!version) { + throw new Error("Missing version parameter"); + } + + switch (version) { + case NACL_VERSION: { + if (typeof data !== "string") { + throw new Error("Message data must be given as a string"); + } + const ephemeralKeyPair = naclBox.keyPair(); + + let pubKeyUInt8Array; + try { + pubKeyUInt8Array = decodeBase64(publicKey); + } catch (err) { + throw new Error("Bad public key"); + } + const msgParamsUInt8Array = decodeUTF8(data); + const nonce = randomBytes(24); + const encryptedMessage = naclBox( + msgParamsUInt8Array, + nonce, + pubKeyUInt8Array, + ephemeralKeyPair.secretKey, + ); + const output = { + version: NACL_VERSION, + nonce: encodeBase64(nonce), + ephemPublicKey: encodeBase64(ephemeralKeyPair.publicKey), + ciphertext: encodeBase64(encryptedMessage), + }; + return JsonToEncryptedDataString(output); + } + + default: + throw new Error("Encryption type/version not supported."); + } +}; + +export { + naclDecodeHex, + encryptedDataStringToJson, + naclDecrypt, + naclEncrypt, + NACL_VERSION, +}; diff --git a/yarn.lock b/yarn.lock index e382f92c0..5e1daf567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2086,6 +2086,7 @@ __metadata: ts-node: "npm:^10.9.2" tsconfig-paths: "npm:^4.2.0" tsup: "npm:^8.3.5" + tweetnacl-util: "npm:^0.15.1" typescript: "npm:^5.6.3" typescript-eslint: "npm:8.14.0" vitest: "npm:^2.1.4" From 40d095cf9dfe039878c5bd99010d7f757f31c416 Mon Sep 17 00:00:00 2001 From: Gamaliel Padillo Date: Tue, 4 Feb 2025 17:02:46 -0800 Subject: [PATCH 03/23] fix: minor ui cleanup --- .../restore-wallet/backup-detected.vue | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue index 3869aa118..35e4061da 100644 --- a/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue +++ b/packages/extension/src/ui/onboard/restore-wallet/backup-detected.vue @@ -105,8 +105,9 @@ const skip = () => { @import '@action/styles/theme.less'; .selected { - background: @default; + background: @primary; border-radius: 10px; + color: @white; } .backup-detected { width: 100%; @@ -134,16 +135,29 @@ const skip = () => { } &__backup-items-container { - height: 150px; + padding: 16px; + min-height: 150px; + max-height: 300px; + overflow-y: auto; + background: @white; + border-radius: 8px; } &__backup-item { - height: 50px; - padding: 0 16px; + // height: 50px; + margin: 4px; + padding: 16px; display: flex; font-size: 16px; align-items: center; justify-content: center; + cursor: pointer; + border: 1px solid @white; + + &:hover { + border: 1px solid @primary; + border-radius: 10px; + } } &__backups { From 274981ecdfa120942b79e4d39da7d765459bb4b4 Mon Sep 17 00:00:00 2001 From: kvhnuke <10602065+kvhnuke@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:33:54 -0800 Subject: [PATCH 04/23] devop: cleanup --- .../src/libs/utils/initialize-wallet.ts | 20 ++++++++----- packages/extension/src/ui/onboard/App.vue | 13 ++++----- .../onboard/restore-wallet/type-password.vue | 29 +++++++++++-------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/packages/extension/src/libs/utils/initialize-wallet.ts b/packages/extension/src/libs/utils/initialize-wallet.ts index b205af19d..feaf10adc 100644 --- a/packages/extension/src/libs/utils/initialize-wallet.ts +++ b/packages/extension/src/libs/utils/initialize-wallet.ts @@ -66,11 +66,17 @@ export const onboardInitializeWallets = async ( const kr = new KeyRing(); const backupsState = new BackupState(); await kr.init(mnemonic, password); - await kr.unlock(password); - const mainAccount = await kr.getNewAccount({ - basePath: EthereumNetworks.ethereum.basePath, - signerType: EthereumNetworks.ethereum.signer[0], - }); - const backups = await backupsState.getBackups(mainAccount.publicKey); - return { backupsFound: backups.length > 0 }; + try { + await kr.unlock(password); + const mainAccount = await kr.getNewAccount({ + basePath: EthereumNetworks.ethereum.basePath, + signerType: EthereumNetworks.ethereum.signer[0], + }); + const backups = await backupsState.getBackups(mainAccount.publicKey); + kr.lock(); + return { backupsFound: backups.length > 0 }; + } catch (e) { + console.error(e); + return { backupsFound: false }; + } }; diff --git a/packages/extension/src/ui/onboard/App.vue b/packages/extension/src/ui/onboard/App.vue index ac95a7bdd..855561743 100644 --- a/packages/extension/src/ui/onboard/App.vue +++ b/packages/extension/src/ui/onboard/App.vue @@ -2,11 +2,7 @@
- +