584 lines
19 KiB
Vue
584 lines
19 KiB
Vue
<template>
|
|
<ion-page>
|
|
<ion-header>
|
|
<ion-toolbar>
|
|
<ion-title>Settings</ion-title>
|
|
</ion-toolbar>
|
|
</ion-header>
|
|
<ion-content class="ion-padding">
|
|
<ion-accordion-group :value="defaultAccordionOpen" v-if="!loading">
|
|
<ion-accordion value="1">
|
|
<ion-item slot="header" color="light">
|
|
<ion-label>Security</ion-label>
|
|
</ion-item>
|
|
<div class="ion-padding" slot="content">
|
|
<ion-list>
|
|
<ion-item v-if="noAccounts">You need at least one account to touch this settings</ion-item>
|
|
<ion-list :disabled="noAccounts">
|
|
<ion-item>
|
|
<ion-label>Enable Storage Encryption</ion-label>
|
|
<ion-toggle :key="updateKey" @ion-change="changeEncryption" slot="end" :checked="settings.s.enableStorageEnctyption"></ion-toggle>
|
|
</ion-item>
|
|
<ion-item>
|
|
This will require to input an encrypto key when storage is locked.
|
|
</ion-item>
|
|
</ion-list>
|
|
<ion-item :disabled="!settings.s.enableStorageEnctyption">
|
|
<ion-label>Enable Auto Lock</ion-label>
|
|
<ion-toggle :key="updateKey" @ion-change="changeAutoLock" slot="end" :checked="settings.s.lockOutEnabled"></ion-toggle>
|
|
</ion-item>
|
|
<ion-list>
|
|
<ion-item :disabled="!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled">
|
|
<ion-label>Auto-lock Period: (2-120) minutes</ion-label>
|
|
</ion-item>
|
|
<ion-item :disabled="!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled">
|
|
<ion-input :key="updateKey" v-model="settings.s.lockOutPeriod" type="number"></ion-input>
|
|
</ion-item>
|
|
<ion-item :disabled="!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled">
|
|
<ion-button @click="setTime">Set Auto-lock</ion-button>
|
|
</ion-item>
|
|
</ion-list>
|
|
<ion-list>
|
|
<ion-item>
|
|
<ion-label>Permanent Lock</ion-label>
|
|
<ion-toggle @ion-change="changePermaLock" :key="updateKey" slot="end" :disabled="!settings.s.enableStorageEnctyption" :checked="settings.s.encryptAfterEveryTx"></ion-toggle>
|
|
</ion-item>
|
|
<ion-item>Will require decrypt pass before any sign or transaction</ion-item>
|
|
</ion-list>
|
|
</ion-list>
|
|
</div>
|
|
</ion-accordion>
|
|
<ion-accordion value="2">
|
|
<ion-item slot="header" color="light">
|
|
<ion-label>Theme</ion-label>
|
|
</ion-item>
|
|
<div class="ion-padding" slot="content">
|
|
<ion-list>
|
|
<ion-radio-group :value="radioTheme">
|
|
<ion-item>
|
|
<ion-radio
|
|
slot="start"
|
|
value="system"
|
|
@click="changeTheme('system')"
|
|
/>
|
|
<ion-label>System Default</ion-label>
|
|
</ion-item>
|
|
<ion-item>
|
|
<ion-radio
|
|
slot="start"
|
|
value="dark"
|
|
@click="changeTheme('dark')"
|
|
/>
|
|
<ion-label>Dark</ion-label>
|
|
</ion-item>
|
|
<ion-item>
|
|
<ion-radio
|
|
slot="start"
|
|
value="light"
|
|
@click="changeTheme('light')"
|
|
/>
|
|
<ion-label>Light</ion-label>
|
|
</ion-item>
|
|
</ion-radio-group>
|
|
</ion-list>
|
|
</div>
|
|
</ion-accordion>
|
|
<ion-accordion value="3">
|
|
<ion-item slot="header" color="light">
|
|
<ion-label>About</ion-label>
|
|
</ion-item>
|
|
<div class="ion-padding" slot="content">
|
|
<p>Clear EVM Wallet (CLW) is a fully open-source wallet built with Vue, Ionic, and Ethers.</p>
|
|
<p>It emulates Metamask Wallet and can be used as a drop-in replacement, right now if you have both extensions, CLW will overwrite Metamask.</p>
|
|
<p>Main philosophy of the wallet is: no trackers, full control, export/import JSONs with accounts, fast generate new accounts, and wipe everything with one click.</p>
|
|
<p>Github Repo: <a href="#" @click="openTab('https://github.com/andrei0x309/clear-wallet')">LINK</a></p>
|
|
<br/>
|
|
<p style="margin-bottom: 0.2rem">Some Web3 Projects I personally appreciate:</p>
|
|
<p>YUP - web3 social platform <a href="#" @click="openTab('https://app.yup.io')">LINK</a></p>
|
|
<p>Crypto-Leftists: web3 left-wing crypto community <a href="#" @click="openTab('https://discord.gg/gzA4bTCdhb')">LINK</a></p>
|
|
<p>Idena: web3 fully private identity provider blockchain <a href="#" @click="openTab('https://www.idena.io/')">LINK</a></p>
|
|
<p>Mirror: web3 publishing platform <a href="#" @click="openTab('https://mirror.xyz')">LINK</a></p>
|
|
</div>
|
|
</ion-accordion>
|
|
<ion-accordion value="4">
|
|
<ion-item slot="header" color="light">
|
|
<ion-label> Import / Export Accounts</ion-label>
|
|
</ion-item>
|
|
<div class="ion-padding" slot="content">
|
|
<ion-item>
|
|
<ion-label>Import Additional Accounts</ion-label>
|
|
<input ref="importFile" type="file" accept=".json" />
|
|
<ion-button color="warning" @click="importAcc">Import</ion-button>
|
|
</ion-item>
|
|
<ion-item>
|
|
<ion-label>Export All Accounts</ion-label>
|
|
<ion-button color="warning" @click="exportAcc">Export</ion-button>
|
|
</ion-item>
|
|
</div>
|
|
</ion-accordion>
|
|
<ion-accordion value="5">
|
|
<ion-item slot="header" color="light">
|
|
<ion-label>Danger</ion-label>
|
|
</ion-item>
|
|
<div class="ion-padding" slot="content">
|
|
<ion-item>
|
|
<ion-label>WIPE All DATA</ion-label>
|
|
<ion-button color="danger" @click="wipeStorage">PERMA WIPE</ion-button>
|
|
</ion-item>
|
|
</div>
|
|
</ion-accordion>
|
|
</ion-accordion-group>
|
|
<ion-toast
|
|
:is-open="toastState"
|
|
@didDismiss="toastState = false"
|
|
:message="toastMsg"
|
|
:duration="1500"
|
|
></ion-toast>
|
|
<ion-loading
|
|
:is-open="loading"
|
|
cssClass="my-custom-class"
|
|
message="Please wait..."
|
|
:duration="4000"
|
|
@didDismiss="loading = false"
|
|
>
|
|
</ion-loading>
|
|
<ion-modal
|
|
:is-open="mpModal"
|
|
@did-dismiss="mpModal=false;modalDismiss()"
|
|
>
|
|
<ion-header>
|
|
<ion-toolbar>
|
|
<ion-buttons slot="start">
|
|
<ion-button @click="modalGetPassword?.reject ? (() => { modalGetPassword.reject(); modalGetPassword = null })() : mpModal=false">Close</ion-button>
|
|
</ion-buttons>
|
|
<ion-title v-if="!settings.s.enableStorageEnctyption">Create Encryption Password</ion-title>
|
|
<ion-title v-else>Enter Encryption Password</ion-title>
|
|
</ion-toolbar>
|
|
</ion-header>
|
|
<ion-content class="ion-padding">
|
|
<ion-list v-if="settings.s.enableStorageEnctyption">
|
|
<ion-item>
|
|
<ion-label>Old Password</ion-label>
|
|
</ion-item> <ion-item>
|
|
<ion-input v-model="mpPass" type="password"></ion-input>
|
|
</ion-item>
|
|
</ion-list>
|
|
<div v-else>
|
|
<ion-list>
|
|
<ion-item>
|
|
<ion-label>New Password</ion-label>
|
|
</ion-item> <ion-item>
|
|
<ion-input v-model="mpPass" type="password"></ion-input>
|
|
</ion-item>
|
|
</ion-list>
|
|
<ion-list>
|
|
<ion-item>
|
|
<ion-label>Confirm</ion-label>
|
|
</ion-item> <ion-item>
|
|
<ion-input v-model="mpConfirm" type="password"></ion-input>
|
|
</ion-item>
|
|
</ion-list>
|
|
</div>
|
|
<ion-item>
|
|
<ion-button @click="modalGetPassword?.resolve ? (() => { modalGetPassword.resolve(); modalGetPassword = null })() : confirmModal()">Confirm</ion-button>
|
|
</ion-item>
|
|
</ion-content>
|
|
</ion-modal>
|
|
<ion-alert
|
|
:is-open="alertOpen"
|
|
:header="alertHeader"
|
|
:message="alertMsg"
|
|
:buttons="['OK']"
|
|
@didDismiss="alertOpen=false"
|
|
></ion-alert>
|
|
</ion-content>
|
|
</ion-page>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent, ref, reactive, Ref } from "vue";
|
|
import { storageWipe, getSettings, setSettings, getAccounts, saveSelectedAccount, replaceAccounts, openTab } from "@/utils/platform";
|
|
import { decrypt, encrypt, getCryptoParams } from "@/utils/webCrypto"
|
|
import { Account } from '@/extension/types'
|
|
import { exportFile } from '@/utils/misc'
|
|
import type { Settings } from "@/extension/types"
|
|
import {
|
|
IonContent,
|
|
IonHeader,
|
|
IonPage,
|
|
IonTitle,
|
|
IonToolbar,
|
|
IonItem,
|
|
IonLabel,
|
|
IonButton,
|
|
IonLoading,
|
|
onIonViewWillEnter,
|
|
IonList,
|
|
IonToggle,
|
|
IonModal,
|
|
IonInput,
|
|
IonAccordion,
|
|
IonAccordionGroup,
|
|
IonRadioGroup,
|
|
IonRadio,
|
|
IonButtons,
|
|
IonAlert,
|
|
IonToast
|
|
} from "@ionic/vue";
|
|
|
|
export default defineComponent({
|
|
components: {
|
|
IonContent,
|
|
IonHeader,
|
|
IonPage,
|
|
IonTitle,
|
|
IonToolbar,
|
|
IonItem,
|
|
IonLabel,
|
|
IonButton,
|
|
IonLoading,
|
|
IonList,
|
|
IonToggle,
|
|
IonModal,
|
|
IonInput,
|
|
IonAccordion,
|
|
IonAccordionGroup,
|
|
IonRadioGroup,
|
|
IonRadio,
|
|
IonButtons,
|
|
IonAlert,
|
|
IonToast
|
|
},
|
|
setup() {
|
|
const loading = ref(true);
|
|
const mpModal = ref(false);
|
|
const mpPass = ref('');
|
|
const mpConfirm = ref('');
|
|
const updateKey = ref(0);
|
|
const alertOpen = ref(false);
|
|
const alertMsg = ref('');
|
|
const toastState = ref(false);
|
|
const toastMsg = ref('');
|
|
const alertHeader = ref('Error')
|
|
const importFile = ref(null) as unknown as Ref<HTMLInputElement>
|
|
type ModalPromisePassword = null | { resolve: ((p?: unknown) => void), reject: ((p?: unknown) => void)}
|
|
const modalGetPassword = ref(null) as Ref<ModalPromisePassword>
|
|
const noAccounts = ref(true)
|
|
const defaultAccordionOpen = ref("0")
|
|
const radioTheme = ref('system') as Ref<'system' | 'light' | 'dark'>
|
|
|
|
const wipeStorage = async () => {
|
|
loading.value = true;
|
|
await storageWipe();
|
|
loading.value = false;
|
|
};
|
|
const settings = reactive({
|
|
s: null as unknown as Settings
|
|
}) as { s: Settings}
|
|
|
|
const saveSettings = async () => {
|
|
loading.value = true
|
|
await setSettings(settings.s)
|
|
loading.value = false
|
|
}
|
|
|
|
const setEncryptToggle = (state: boolean) => {
|
|
settings.s.enableStorageEnctyption = state
|
|
updateKey.value++
|
|
defaultAccordionOpen.value = "1"
|
|
}
|
|
|
|
const changeAutoLock = async () => {
|
|
settings.s.lockOutEnabled = !settings.s.lockOutEnabled
|
|
updateKey.value++
|
|
await saveSettings()
|
|
defaultAccordionOpen.value = "1"
|
|
}
|
|
|
|
const changePermaLock = async () => {
|
|
settings.s.encryptAfterEveryTx = !settings.s.encryptAfterEveryTx
|
|
updateKey.value++
|
|
await saveSettings()
|
|
defaultAccordionOpen.value = "1"
|
|
}
|
|
|
|
const changeTheme = async (theme: 'system' | 'light' | 'dark') => {
|
|
document.body.classList.remove(radioTheme.value)
|
|
document.body.classList.add(theme)
|
|
radioTheme.value = theme
|
|
settings.s.theme = theme
|
|
await saveSettings()
|
|
defaultAccordionOpen.value = "2"
|
|
}
|
|
|
|
const changeEncryption = async () => {
|
|
loading.value = true
|
|
mpModal.value = true
|
|
loading.value = false
|
|
}
|
|
|
|
const confirmModal = async () => {
|
|
loading.value = true
|
|
if(mpPass.value.length < 3) {
|
|
loading.value = false
|
|
alertHeader.value = 'Error'
|
|
alertMsg.value = 'Password is too short. More than 3 characters are required.';
|
|
alertOpen.value = true
|
|
setEncryptToggle(settings.s.enableStorageEnctyption)
|
|
return
|
|
}
|
|
|
|
if (!settings.s.enableStorageEnctyption) {
|
|
if (mpPass.value !== mpConfirm.value) {
|
|
loading.value = false
|
|
alertHeader.value = 'Error'
|
|
alertMsg.value = 'Password and confirm password do not match';
|
|
alertOpen.value = true
|
|
setEncryptToggle(settings.s.enableStorageEnctyption)
|
|
return
|
|
}
|
|
let accounts = await getAccounts()
|
|
const cryptoParams = await getCryptoParams(mpPass.value)
|
|
const accProm = accounts.map(async a => {
|
|
a.encPk = await encrypt(a.pk, cryptoParams)
|
|
a.pk = ''
|
|
return a
|
|
})
|
|
accounts = await Promise.all(accProm)
|
|
await replaceAccounts(accounts)
|
|
await saveSelectedAccount(accounts[0])
|
|
setEncryptToggle(true)
|
|
await setSettings(settings.s)
|
|
mpPass.value = ''
|
|
mpConfirm.value = ''
|
|
mpModal.value = false
|
|
} else {
|
|
try {
|
|
let accounts = await getAccounts()
|
|
const cryptoParams = await getCryptoParams(mpPass.value)
|
|
const accProm = accounts.map(async a => {
|
|
if(a.encPk) {
|
|
a.pk = await decrypt(a.encPk, cryptoParams)
|
|
}
|
|
return a
|
|
})
|
|
accProm.forEach( a => a.catch(e => console.log(e)) )
|
|
accounts = await Promise.all(accProm)
|
|
await replaceAccounts(accounts)
|
|
await saveSelectedAccount(accounts[0])
|
|
setEncryptToggle(false)
|
|
settings.s.lockOutEnabled = false
|
|
settings.s.encryptAfterEveryTx = false
|
|
await setSettings(settings.s)
|
|
mpPass.value = ''
|
|
mpConfirm.value = ''
|
|
mpModal.value = false
|
|
} catch(error) {
|
|
console.log(error)
|
|
loading.value = false
|
|
alertHeader.value = 'Error'
|
|
alertMsg.value = 'Decryption failed, password is not correct.';
|
|
alertOpen.value = true
|
|
setEncryptToggle(settings.s.enableStorageEnctyption)
|
|
return
|
|
}
|
|
}
|
|
|
|
loading.value = false
|
|
}
|
|
|
|
const validateFile = () => {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
if (!importFile.value?.value?.length) {
|
|
return resolve({
|
|
error: 'Import json file is missing'
|
|
})
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const json = JSON.parse(event?.target?.result as string)
|
|
if(!json.length){
|
|
return resolve({ error: 'JSON format is wrong. Corrrect JSON format is: [{ "name": "Account Name", "pk": "Private Key", "address": "0x..." },{...}]' })
|
|
}
|
|
const test = json.some((e:any) => ( !('pk' in e ) || !('name' in e) || !('address' in e) || !(e.pk.length === 66 || e.pk.length === 64)))
|
|
if(test) {
|
|
return resolve({ error: 'JSON format is wrong. Corrrect JSON format is: [{ "name": "Account Name", "pk": "Private Key", "address": "0x..." },{...}], Also PK must be valid (66 || 64 length) !' })
|
|
}
|
|
return resolve({ error: false, json })
|
|
}
|
|
reader.readAsText(importFile.value?.files?.[0] as File);
|
|
|
|
}catch {
|
|
return resolve(
|
|
{
|
|
error: 'Parsing JSON file'
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const getPassword = () => {
|
|
return new Promise( (resolve, reject) => {
|
|
modalGetPassword.value = { resolve, reject }
|
|
mpModal.value = true
|
|
})
|
|
}
|
|
|
|
const promptForPassword = async (accounts: Account[]) => {
|
|
let isCorectPass = false
|
|
do {
|
|
try {
|
|
await getPassword()
|
|
modalGetPassword.value = null
|
|
} catch {
|
|
alertHeader.value = 'Error'
|
|
alertMsg.value = "Password is required!"
|
|
alertOpen.value = true
|
|
mpModal.value = false
|
|
return false
|
|
}
|
|
try {
|
|
const cryptoParams = await getCryptoParams(mpPass.value)
|
|
if(accounts?.[0]?.encPk) {
|
|
await decrypt(accounts[0].encPk, cryptoParams)
|
|
}
|
|
isCorectPass = true
|
|
} catch {
|
|
isCorectPass = false
|
|
alertHeader.value = 'Error'
|
|
alertMsg.value = "Password is wrong!"
|
|
alertOpen.value = true
|
|
}
|
|
} while (!isCorectPass);
|
|
return true
|
|
}
|
|
|
|
const importAcc = async () => {
|
|
const validation = await validateFile() as { error: any }
|
|
if (validation.error) {
|
|
alertMsg.value = validation.error
|
|
alertOpen.value = true
|
|
return
|
|
}
|
|
const accounts = await getAccounts()
|
|
const newAccounts = (validation as unknown as { json: Account[] }).json
|
|
if(settings.s.enableStorageEnctyption) {
|
|
const hasPass = await promptForPassword(accounts)
|
|
if(hasPass) {
|
|
const cryptoParams = await getCryptoParams(mpPass.value)
|
|
const accProm = newAccounts.map(async a => {
|
|
if(a.pk.length === 64) {
|
|
a.pk = `0x${a.pk}`
|
|
}
|
|
a.encPk = await encrypt(a.pk, cryptoParams)
|
|
return a
|
|
})
|
|
const encNewAccounts = await Promise.all(accProm)
|
|
await replaceAccounts([...accounts, ...encNewAccounts])
|
|
alertHeader.value = 'Success'
|
|
alertMsg.value = "Successfully imported new accounts."
|
|
alertOpen.value = true
|
|
noAccounts.value = false
|
|
}
|
|
return false
|
|
} else {
|
|
await replaceAccounts([...accounts, ...newAccounts.map( a => { a.encPk = ''; return a })])
|
|
alertHeader.value = 'Success'
|
|
alertMsg.value = "Successfully imported new accounts."
|
|
alertOpen.value = true
|
|
noAccounts.value = false
|
|
}
|
|
}
|
|
|
|
const exportAcc = async () => {
|
|
const accounts = await getAccounts()
|
|
if(!accounts.length) {
|
|
alertMsg.value = "You need at least one account to export."
|
|
alertOpen.value = true
|
|
}
|
|
if(settings.s.enableStorageEnctyption) {
|
|
const hasPass = await promptForPassword(accounts)
|
|
if(hasPass) {
|
|
const cryptoParams = await getCryptoParams(mpPass.value)
|
|
const accProm = accounts.map(async a => {
|
|
a.pk = await decrypt(a.encPk, cryptoParams)
|
|
return a
|
|
})
|
|
const encNewAccounts = await Promise.all(accProm)
|
|
exportFile('wallet_export.json', JSON.stringify(encNewAccounts, null, 2))
|
|
}
|
|
return false
|
|
} else {
|
|
exportFile('wallet_export.json', JSON.stringify(accounts, null, 2))
|
|
}
|
|
}
|
|
|
|
|
|
onIonViewWillEnter(async () => {
|
|
await Promise.all([getSettings().then((storeSettings) =>
|
|
{
|
|
settings.s = storeSettings
|
|
radioTheme.value = settings.s.theme
|
|
}),
|
|
getAccounts().then((accounts) => {
|
|
if(accounts.length) {
|
|
noAccounts.value = false
|
|
}
|
|
})])
|
|
loading.value = false
|
|
})
|
|
|
|
const setTime = async () => {
|
|
loading.value = true
|
|
if ( settings.s.lockOutPeriod < 2 || settings.s.lockOutPeriod > 120){
|
|
loading.value = false
|
|
alertMsg.value = 'Auto-lock period must be between 2 and 120';
|
|
alertOpen.value = true
|
|
return
|
|
}
|
|
settings.s.lockOutPeriod = Math.trunc(settings.s.lockOutPeriod)
|
|
await saveSettings()
|
|
loading.value = false
|
|
toastMsg.value = 'Auto-lock period was set';
|
|
toastState.value = true
|
|
}
|
|
|
|
const modalDismiss = () => {
|
|
setEncryptToggle(settings.s.enableStorageEnctyption)
|
|
}
|
|
|
|
return {
|
|
wipeStorage,
|
|
loading,
|
|
mpModal,
|
|
settings,
|
|
saveSettings,
|
|
changeEncryption,
|
|
mpPass,
|
|
mpConfirm,
|
|
confirmModal,
|
|
updateKey,
|
|
alertOpen,
|
|
alertMsg,
|
|
modalDismiss,
|
|
setTime,
|
|
toastState,
|
|
toastMsg,
|
|
importAcc,
|
|
exportAcc,
|
|
importFile,
|
|
modalGetPassword,
|
|
noAccounts,
|
|
alertHeader,
|
|
changeAutoLock,
|
|
defaultAccordionOpen,
|
|
changeTheme,
|
|
openTab,
|
|
radioTheme,
|
|
changePermaLock
|
|
};
|
|
},
|
|
});
|
|
</script>
|