diff --git a/packages/extension/package.json b/packages/extension/package.json
index d0778bbbd..d3389c582 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -67,6 +67,7 @@
"ethereum-cryptography": "^2.2.1",
"ethereumjs-abi": "^0.6.8",
"eventemitter3": "^5.0.1",
+ "jdenticon": "^3.3.0",
"lodash": "^4.17.21",
"memoize-one": "^6.0.0",
"moment": "^2.30.1",
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..2837d11af
--- /dev/null
+++ b/packages/extension/src/libs/backup-state/configs.ts
@@ -0,0 +1,6 @@
+const BACKUP_URL = 'https://backupstore.enkrypt.com/';
+const HEADERS = {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+};
+export { BACKUP_URL, HEADERS };
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..346c26739
--- /dev/null
+++ b/packages/extension/src/libs/backup-state/index.ts
@@ -0,0 +1,363 @@
+import BrowserStorage from '../common/browser-storage';
+import { InternalStorageNamespace } from '@/types/provider';
+import {
+ BackupData,
+ BackupResponseType,
+ BackupType,
+ IState,
+ ListBackupType,
+ 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,
+ utf8ToHex,
+} from '@enkryptcom/utils';
+import { hashPersonalMessage } from '@ethereumjs/util';
+import { v4 as uuidv4 } from 'uuid';
+import { BACKUP_URL, HEADERS } 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 #getSignature(
+ msgHash: string,
+ mainWallet: EnkryptAccount,
+ ): Promise {
+ return sendUsingInternalMessengers({
+ method: InternalMethods.sign,
+ params: [msgHash, mainWallet],
+ }).then(res => {
+ if (res.error) {
+ console.error(res);
+ return null;
+ } else {
+ return JSON.parse(res.result as string);
+ }
+ });
+ }
+
+ async getMainWallet(): Promise {
+ const pkr = new PublicKeyRing();
+ const allAccounts = await pkr.getAccounts();
+ const mainWallet = allAccounts.find(
+ acc =>
+ acc.walletType === WalletType.mnemonic &&
+ acc.pathIndex === 0 &&
+ acc.signerType === EthNetwork.signer[0] &&
+ acc.basePath === EthNetwork.basePath,
+ );
+ if (!mainWallet) {
+ throw new Error('No main wallet found');
+ }
+ return mainWallet;
+ }
+
+ getListBackupMsgHash(pubkey: string): string {
+ const now = new Date();
+ const messageToSign = `${pubkey}-GET-BACKUPS-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}-${now.getUTCFullYear()}`;
+ return bufferToHex(
+ hashPersonalMessage(hexToBuffer(utf8ToHex(messageToSign))),
+ );
+ }
+
+ async listBackups(options?: {
+ signature: string;
+ pubkey: string;
+ }): Promise {
+ let signature: string = '';
+ let pubkey: string = '';
+ if (options) {
+ signature = options.signature;
+ pubkey = options.pubkey;
+ } else {
+ const mainWallet = await this.getMainWallet();
+ pubkey = mainWallet.publicKey;
+ const msgHash = this.getListBackupMsgHash(mainWallet.publicKey);
+ signature = await this.#getSignature(msgHash, mainWallet);
+ }
+ if (!signature) {
+ console.error('No signature found');
+ return [];
+ }
+ const rawResponse = await fetch(
+ `${BACKUP_URL}backups/${pubkey}?signature=${signature}`,
+ {
+ method: 'GET',
+ headers: HEADERS,
+ },
+ );
+ const content = (await rawResponse.json()) as {
+ backups: ListBackupType[];
+ };
+ return content.backups;
+ }
+
+ async getBackup(userId: string): Promise {
+ const mainWallet = await this.getMainWallet();
+ const now = new Date();
+ const messageToSign = `${userId}-GET-BACKUP-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}-${now.getUTCFullYear()}`;
+ const msgHash = bufferToHex(
+ hashPersonalMessage(hexToBuffer(utf8ToHex(messageToSign))),
+ );
+ const signature = await this.#getSignature(msgHash, mainWallet);
+ const rawResponse = await fetch(
+ `${BACKUP_URL}backups/${mainWallet.publicKey}/users/${userId}?signature=${signature}`,
+ {
+ method: 'GET',
+ headers: HEADERS,
+ },
+ );
+ const content = (await rawResponse.json()) as BackupResponseType;
+ return content.backup;
+ }
+
+ async deleteBackup(userId: string): Promise {
+ const mainWallet = await this.getMainWallet();
+ const now = new Date();
+ const messageToSign = `${userId}-DELETE-BACKUP-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}-${now.getUTCFullYear()}`;
+ const msgHash = bufferToHex(
+ hashPersonalMessage(hexToBuffer(utf8ToHex(messageToSign))),
+ );
+ const signature = await this.#getSignature(msgHash, mainWallet);
+ if (!signature) {
+ console.error('No signature found');
+ return false;
+ }
+ return fetch(
+ `${BACKUP_URL}backups/${mainWallet.publicKey}/users/${userId}?signature=${signature}`,
+ {
+ method: 'DELETE',
+ headers: HEADERS,
+ },
+ )
+ .then(res => res.json())
+ .then(content => {
+ if ((content as { message: string }).message === 'Ok') {
+ return true;
+ }
+ console.error(content);
+ return false;
+ });
+ }
+
+ async restoreBackup(userId: string, keyringPassword: string): Promise {
+ const mainWallet = await this.getMainWallet();
+ await sendUsingInternalMessengers({
+ method: InternalMethods.unlock,
+ params: [keyringPassword, false],
+ });
+ const backup = await this.getBackup(userId);
+ if (!backup) {
+ console.error('No backup found');
+ return;
+ }
+ 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),
+ );
+ 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;
+ }
+ });
+ 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) {
+ await kr.renameAccount(existingAccount.address, newAccount.name);
+ continue;
+ } else if (newAccount) {
+ await kr.saveNewAccount({
+ basePath: newAccount.basePath,
+ name: newAccount.name,
+ signerType: newAccount.signerType,
+ walletType: newAccount.walletType,
+ });
+ } else if (!newAccount) {
+ await kr.saveNewAccount({
+ basePath: basePath,
+ name: `New Account from backup ${i}`,
+ signerType: signerType as SignerType,
+ walletType: WalletType.mnemonic,
+ });
+ }
+ }
+ }
+ }
+ });
+ }
+
+ async backup(firstTime: boolean): Promise {
+ const state = await this.getState();
+ if (firstTime && state.lastBackupTime !== 0) {
+ return true;
+ }
+ if (!state.enabled) {
+ 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 this.#getSignature(msgHash, mainWallet).then(async signature => {
+ const rawResponse = await fetch(
+ `${BACKUP_URL}backups/${mainWallet.publicKey}/users/${state.userId}?signature=${signature}`,
+ {
+ method: 'POST',
+ headers: HEADERS,
+ body: JSON.stringify({
+ payload: encryptedStr,
+ }),
+ },
+ );
+ const content = (await rawResponse.json()) as { message: string };
+ if (content.message === 'Ok') {
+ await this.setState({
+ lastBackupTime: new Date().getTime(),
+ userId: state.userId,
+ enabled: state.enabled,
+ });
+ 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(),
+ enabled: true,
+ };
+ 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 disableBackups(): Promise {
+ const state: IState = await this.getState();
+ await this.setState({ ...state, enabled: false });
+ }
+ async enableBackups(): Promise {
+ const state: IState = await this.getState();
+ await this.setState({ ...state, enabled: true });
+ }
+ async isBackupEnabled(): Promise {
+ const state: IState = await this.getState();
+ return state.enabled;
+ }
+}
+
+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..ef86ce012
--- /dev/null
+++ b/packages/extension/src/libs/backup-state/types.ts
@@ -0,0 +1,29 @@
+import { EnkryptAccount } from '@enkryptcom/types';
+
+export enum StorageKeys {
+ backupInfo = 'backup-info',
+}
+
+export interface IState {
+ lastBackupTime: number;
+ userId: string;
+ enabled: boolean;
+}
+
+export interface ListBackupType {
+ userId: string;
+ updatedAt: string;
+}
+
+export interface BackupType extends ListBackupType {
+ payload: string;
+}
+
+export interface BackupResponseType {
+ backup: 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..ff66e07c2 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,31 @@ 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);
+ try {
+ await kr.unlock(password);
+ const mainAccount = await kr.getNewAccount({
+ basePath: EthereumNetworks.ethereum.basePath,
+ signerType: EthereumNetworks.ethereum.signer[0],
+ });
+ const sigHash = backupsState.getListBackupMsgHash(mainAccount.publicKey);
+ const signature = await kr.sign(sigHash as `0x${string}`, {
+ basePath: EthereumNetworks.ethereum.basePath,
+ signerType: EthereumNetworks.ethereum.signer[0],
+ pathIndex: 0,
+ walletType: WalletType.mnemonic,
+ });
+ const backups = await backupsState.listBackups({
+ pubkey: mainAccount.publicKey,
+ signature,
+ });
+ kr.lock();
+ return { backupsFound: backups.length > 0 };
+ } catch (e) {
+ console.error(e);
+ return { backupsFound: false };
+ }
};
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..ff8617cd8 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:
@@ -408,6 +410,7 @@ const init = async () => {
setNetwork(defaultNetwork);
}
await setActiveNetworks();
+ backupState.backup(true).catch(console.error);
isLoading.value = false;
};
diff --git a/packages/extension/src/ui/action/components/switch/index.vue b/packages/extension/src/ui/action/components/switch/index.vue
index 4eec1d802..0db605bb0 100644
--- a/packages/extension/src/ui/action/components/switch/index.vue
+++ b/packages/extension/src/ui/action/components/switch/index.vue
@@ -1,25 +1,26 @@
-
+
diff --git a/packages/extension/src/ui/action/views/settings/views/settings-backups/index.vue b/packages/extension/src/ui/action/views/settings/views/settings-backups/index.vue
new file mode 100644
index 000000000..defe0eb93
--- /dev/null
+++ b/packages/extension/src/ui/action/views/settings/views/settings-backups/index.vue
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+ Save your current list of accounts across all networks, so you don't
+ need to re-generate them when you import or restore your wallet with
+ Enkrypt. You will still need your recovery phrase.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ generateRandomNameWithSeed(' ', entity.userId) }}
+
Last backup on: {{ formatDate(entity.updatedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ generateRandomNameWithSeed(' ', selectedBackup.userId) }}
+
+
Last backup on: {{ formatDate(selectedBackup.updatedAt) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/extension/src/ui/action/views/settings/views/settings-general/index.vue b/packages/extension/src/ui/action/views/settings/views/settings-general/index.vue
index 330a4c334..819151194 100644
--- a/packages/extension/src/ui/action/views/settings/views/settings-general/index.vue
+++ b/packages/extension/src/ui/action/views/settings/views/settings-general/index.vue
@@ -49,7 +49,13 @@
information is collected.
-
+
+
+
+ Save your current list of accounts across all networks, so you don't
+ need to re-generate them.
+
+