dev: 1.0.3

This commit is contained in:
Andrei O 2022-10-13 23:48:07 +03:00
parent 1a4b531ae2
commit 6c18b2841d
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
21 changed files with 519 additions and 84 deletions

View File

@ -41,6 +41,12 @@ switch (route?.query?.route ?? "") {
});
break;
}
case "wallet-error": {
router.push({
path: `/wallet-error"/${rid}/${param}`
});
break;
}
default: {
router.push({ path: "/", })
}

View File

@ -1,3 +1,4 @@
import { getSelectedNetwork, numToHexStr } from "@/utils/platform";
const allowedMethods = {
'eth_accounts': true,
@ -19,18 +20,33 @@ window.addEventListener("message", (event) => {
if (event.source != window)
return;
if (event.data.type && (event.data.type == "CLWALLET_CONTENT")) {
if (event.data.type && (event.data.type === "CLWALLET_CONTENT")) {
event.data.data.resId = event.data.resId
if((event?.data?.data?.method ?? 'x') in allowedMethods) {
chrome.runtime.sendMessage(event.data.data, (res) => {
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId };
const data = { type: "CLWALLET_PAGE", data: res, resId: event.data.resId, website: window?.location?.href ?? '' };
console.log('data back', data)
window.postMessage(data, "*");
})
} else {
}
else {
const data = { type: "CLWALLET_PAGE", data: { error: true, message: 'Unknown method requested'}, resId: event.data.resId };
window.postMessage(data, "*");
}
} else if (event.data.type && (event.data.type === "CLWALLET_PING")) {
getSelectedNetwork().then(network => {
const data = { type: "CLWALLET_PAGE_LISTENER", data: {
listener: 'connected',
data: {
chainId: numToHexStr(network.chainId ?? 0)
}
}};
window.postMessage(data, "*");
})
} else if (event.data.type && (event.data.type === "CLWALLET_EXT_LISTNER")) {
const data = { type: "CLWALLET_PAGE_LISTENER", data: event.data.data, };
console.log('data listner', data)
window.postMessage(data, "*");
}
});

View File

@ -37,11 +37,14 @@ const listner = function(event: any) {
window.addEventListener("message",listner)
const sendMessage = (args: RequestArguments) => {
const sendMessage = (args: RequestArguments, ping = false) => {
return new Promise((resolve, reject) => {
const resId = crypto.randomUUID()
promResolvers[resId] = { resolve, reject }
const data = { type: "CLWALLET_CONTENT", data: args, resId};
if (ping) {
data.type = 'CLWALLET_PING'
}
console.log('data in', data)
window.postMessage(data, "*");
})
@ -200,17 +203,22 @@ const injectWallet = (win: any) => {
console.log('Clear wallet injected', (window as any).ethereum, win)
}
injectWallet(this)
sendMessage({
method: 'wallet_ready'
}, true)
// setTimeout(() => {
// console.log('Metamask clone test');
// // (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, '111111111')});
// // (<any>window).ethereum.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x99"}]}).then((res: any) => { console.log(res, '111111111')});
// }, 3500)
setTimeout(() => {
console.log('Metamask clone test');
// (<any>window).ethereum.request({method: 'eth_requestAccounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// (<any>window).ethereum.request({method: 'eth_accounts', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// (<any>window).ethereum.request({method: 'eth_chainId', params: Array(0)}).then((res: any) => { console.log(res, '111111111')});
// (<any>window).ethereum.request({method: 'wallet_requestPermissions', params: [{eth_accounts: {}}]}).then((res: any) => { console.log(res, '111111111')});
// (<any>window).ethereum.request({method: 'net_version', params: []}).then((res: any) => { console.log(res, '111111111')});
// (<any>window).ethereum.request({method: 'wallet_switchEthereumChain', params: [{chainId: "0x99"}]}).then((res: any) => { console.log(res, '111111111')});
(<any>window).ethereum.on('connect', ((a: any, b: any) => console.log('connect', a, b)));
(<any>window).ethereum.on('accountsChanged', ((a: any, b: any) => console.log('accountsChanged', a, b)));
(<any>window).ethereum.on('chainChanged', ((a: any, b: any) => console.log('chainChanged', a, typeof a)));
}, 3500)
// console.log( (window as any).ethereum.request({method: 'eth_chainId'}))

View File

@ -0,0 +1,7 @@
import type { listnerType } from '@/extension/types'
export const triggerListner = ( type: listnerType, listnerData: any ) => {
const data = { type: "CLWALLET_EXT_LISTNER", data: { listner: type, data: listnerData } }
window.postMessage(data, "*")
console.log('trigger', data)
}

View File

@ -1,4 +1,4 @@
import { getAccounts, getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl } from '@/utils/platform';
import { getAccounts, getSelectedAccount, getSelectedNetwork, smallRandomString, getSettings, clearPk, openTab, getUrl, addToHistory } from '@/utils/platform';
import { userApprove, userReject, rIdWin, rIdData } from '@/extension/userRequest'
import { signMsg, getBalance, getBlockNumber, estimateGas, sendTransaction, getGasPrice, getBlockByNumber } from '@/utils/wallet'
import type { RequestArguments } from '@/extension/types'
@ -164,7 +164,22 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
break
}
const [account, network] = await Promise.all([getSelectedAccount(), getSelectedNetwork()])
if(!account || !network) {
if(!account || !('address' in account)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-tx&param=${encodeURIComponent('No account is selected you need to have an account selected before trying to make a transaction')}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
})
return
}
if(!network || !('chainId' in network)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-tx&param=${encodeURIComponent('No network is selected you need to have a network selected before trying to make a transaction')}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
})
return
}
params.from = account.address
@ -197,6 +212,13 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
sendResponse(tx)
const buttons = {} as any
const network = await getSelectedNetwork()
addToHistory({
date: Date.now(),
txHash: tx.hash,
chainId: network.chainId,
...(network.explorer ? {txUrl: `${network.explorer}/tx/${tx.hash}`.replace('//', '/') } : {}),
webiste: (message?.website)
})
const notificationId = crypto.randomUUID()
if(network?.explorer) {
notificationUrl = `${network.explorer}/tx/${tx.hash}`.replace('//', '/')
@ -246,6 +268,18 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
case ('personal_sign' || 'eth_sign'): {
try {
const account = await getSelectedAccount()
if(!account || !('address' in account)) {
await chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=sign-tx&param=${encodeURIComponent('No account is selected you need to have an account selected before trying sign a message')}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
})
return
}
await new Promise((resolve, reject) => {
chrome.windows.create({
height: 450,

View File

@ -27,6 +27,7 @@ export interface RequestArguments {
method: string;
params?: any[];
resId?: string
website?: string
}
export interface ProviderRpcError extends Error {
@ -52,3 +53,13 @@ export interface Settings {
lastLock: number
lockOutBlocked: boolean
}
export type listnerType = 'accountsChanged' | 'connect' | 'disconnect' | 'chainChanged'
export interface HistoryItem {
date: number
txUrl?: string
chainId?: number
webiste?: string
txHash: string
}

View File

@ -23,6 +23,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/contract-error/:rid/:param/:contract',
component: () => import('@/views/ContractError.vue'),
},
{
path: '/wallet-error/:rid/:error',
component: () => import('@/views/WalletError.vue'),
},
{
path: '/tabs/',
component: AppTabs,
@ -59,6 +63,10 @@ const routes: Array<RouteRecordRaw> = [
path: 'add-account',
component: () => import('@/views/AddAccount.vue'),
},
{
path: 'add-account/edit/:address',
component: () => import('@/views/AddAccount.vue'),
},
{
path: 'add-network',
component: () => import('@/views/AddNetwork.vue'),

11
src/utils/misc.ts Normal file
View File

@ -0,0 +1,11 @@
export const exportFile = (fileName: string, content: string, type = 'json') => {
const link = document.createElement('a')
const blob = new Blob([content], { type: `text/${type};charset=utf-8;` })
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', fileName)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}

View File

@ -1,4 +1,4 @@
import type { Network, Account, Prices, Settings, Networks } from '@/extension/types'
import type { Network, Account, Prices, Settings, Networks, HistoryItem } from '@/extension/types'
import type { Ref } from 'vue'
const defaultSettings = {
@ -79,6 +79,21 @@ export const getPrices = async (): Promise<Prices> => {
return (await storageGet('prices'))?.prices ?? {} as unknown as Prices
}
export const getHistory = async (): Promise<HistoryItem[]> => {
return (await storageGet('history'))?.history ?? [] as unknown as Prices
}
export const addToHistory = async (historyItem: HistoryItem): Promise<void> => {
const history = await getHistory()
if (history.length >= 100) {
history.pop()
history.unshift(historyItem)
} else {
history.unshift(historyItem)
}
await storageSave('history', history)
}
export const getSettings = async (): Promise<Settings> => {
return (await storageGet('settings'))?.settings ?? defaultSettings as unknown as Settings
}

View File

@ -38,6 +38,7 @@ export const estimateGas = async ({to = '', from = '', data = '', value = '0x0'
return await provider.estimateGas({to, from, data, value})
}
export const sendTransaction = async ({ data= '', gas='0x0', to='', from='', value='0x0', gasPrice='0x0'}:
{to: string, from: string, data: string, value: string, gas: string, gasPrice: string},
gasEstimate: Promise<BigNumber> | null = null, pGasPrice : Promise<BigNumber> | null) => {

View File

@ -48,31 +48,30 @@ async function getKey(passwordBytes: Uint8Array) {
);
}
export const encrypt = async (password: string, data:string) => {
export const getCryptoParams = async(password: string): Promise<{ key: CryptoKey, iv: any }> => {
const enc = new TextEncoder()
const encKey = enc.encode(password)
return { key: await getKey(encKey), iv:await getIv() }
}
export const encrypt = async (data: string, cryptoParams: { key: CryptoKey, iv: any }) => {
const enc = new TextEncoder()
const encData = enc.encode(data)
const encKey = enc.encode(password)
const key = await getKey(encKey)
const iv = await getIv()
const encResult = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
iv: cryptoParams.iv,
},
key,
cryptoParams.key,
encData,
)
return JSON.stringify(new Uint8Array(encResult))
}
export const decrypt = async (encryptedData: string, password: string) => {
const enc = new TextEncoder()
const encKey = enc.encode(password)
const key = await getKey(encKey)
const iv = await getIv()
const encryptedUint= new Uint8Array(Object.values(JSON.parse(encryptedData)));
export const decrypt = async (encryptedData: string, cryptoParams: { key: CryptoKey, iv: any }) => {
const encryptedUint = new Uint8Array(Object.values(JSON.parse(encryptedData)));
const contentBytes = new Uint8Array(
await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encryptedUint)
await crypto.subtle.decrypt({ name: "AES-GCM", iv:cryptoParams.iv }, cryptoParams.key, encryptedUint)
);
return new TextDecoder().decode(contentBytes)
}

View File

@ -20,6 +20,10 @@
message="Copied to clipboard"
:duration="1500"
></ion-toast>
<ion-item v-if="loading || accounts.length < 1">
<ion-label>No EVM accounts found</ion-label>
<ion-button @click="goToAddAccount">Add Account</ion-button>
</ion-item>
<ion-list v-for="account of accounts" :key="account.address">
<ion-item>
<ion-label>
@ -32,7 +36,7 @@
<ion-item>
<ion-chip>View Pk</ion-chip>
<ion-chip @click="deleteAccount(account.address)">Delete</ion-chip>
<ion-chip @click="editName(account.address)">Edit Name</ion-chip>
<ion-chip @click="editAccount(account.address)">Edit Name</ion-chip>
</ion-item>
</ion-list>
</ion-content>
@ -60,6 +64,7 @@ import {
} from "@ionic/vue";
import { addCircleOutline, copyOutline } from "ionicons/icons";
import router from "@/router";
import type { Account } from '@/extension/types'
export default defineComponent({
@ -102,10 +107,15 @@ export default defineComponent({
await replaceAccounts([...accounts.value])
loading.value = false
}
const editName = async (name: string) => {
// do nothing
const editAccount = (address: string) => {
router.push(`add-account/edit/${address}`)
}
const goToAddAccount = () => {
router.push("/tabs/add-account");
};
onIonViewWillEnter(() => {
loadData()
})
@ -118,7 +128,9 @@ export default defineComponent({
copyAddress,
getToastRef,
deleteAccount,
editName
editAccount,
loading,
goToAddAccount
}
}

View File

@ -2,7 +2,8 @@
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Add Account</ion-title>
<ion-title v-if="!isEdit" >Add Account</ion-title>
<ion-title v-else >Edit Account</ion-title>
</ion-toolbar>
</ion-header>
@ -15,11 +16,12 @@
<ion-label>Get Random Name</ion-label>
<ion-button @click="getRandomName" >Generate</ion-button>
</ion-item>
<ion-item>
<ion-icon style="margin-right: 0.5rem;" @click="paste('pasteRpc')" :icon="clipboardOutline" button /><ion-label>PK</ion-label>
<ion-item v-if="!isEdit">
<ion-icon style="margin-right: 0.5rem;" @click="paste('pastePk')" :icon="clipboardOutline" button/>
<ion-label button>PK</ion-label>
<ion-input id="pastePk" v-model="pk"></ion-input>
</ion-item>
<ion-item>
<ion-item v-if="!isEdit">
<ion-label>Get Random PK</ion-label>
<ion-button @click="generateRandomPk" >Generate</ion-button>
</ion-item>
@ -40,10 +42,12 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonAlert, IonIcon } from "@ionic/vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonAlert, IonIcon, onIonViewWillEnter } from "@ionic/vue";
import { ethers } from "ethers"
import { saveSelectedAccount, getAccounts, saveAccount, getRandomPk, smallRandomString, paste } from "@/utils/platform";
import router from "@/router";
import { useRoute } from 'vue-router'
import type { Account } from '@/extension/types'
import { clipboardOutline } from "ionicons/icons";
@ -54,12 +58,27 @@ export default defineComponent({
const pk = ref('')
const alertOpen = ref(false)
const alertMsg = ref('')
const route = useRoute()
const isEdit = route.path.includes('/edit')
const paramAddress = route.params.address ?? ""
let accountsProm: Promise<Account[] | undefined>
const resetFields = () => {
name.value = ''
pk.value = ''
}
onIonViewWillEnter(async () => {
if(isEdit && paramAddress) {
accountsProm = getAccounts()
const accounts = await accountsProm as Account[]
const acc = accounts.find(account => account.address === paramAddress)
if(acc) {
name.value = acc.name
}
}
})
const onAddAccount = async () => {
let p1 = Promise.resolve()
if(pk.value.length === 64){
@ -73,7 +92,10 @@ export default defineComponent({
const wallet = new ethers.Wallet(pk.value)
const accounts = await getAccounts()
if(!accountsProm) {
accountsProm = getAccounts()
}
const accounts = await accountsProm as Account[]
if((accounts.length ?? 0) < 1 ){
p1 = saveSelectedAccount({
address: wallet.address,
@ -94,7 +116,11 @@ export default defineComponent({
encPk: ''
})
await Promise.all([p1, p2])
router.push('/')
if(isEdit) {
router.push('accounts')
}else {
router.push('/')
}
resetFields()
}
@ -107,7 +133,11 @@ export default defineComponent({
}
const onCancel = () => {
router.push('/')
if(isEdit) {
router.push('accounts')
}else {
router.push('/')
}
}
return {
@ -120,7 +150,8 @@ export default defineComponent({
generateRandomPk,
getRandomName,
clipboardOutline,
paste
paste,
isEdit
}
}

View File

@ -189,7 +189,11 @@ export default defineComponent({
networks[chainId.value] = network
const p2 = replaceNetworks(networks)
await Promise.all([p1, p2])
router.push('/')
if(isEdit) {
router.push('networks')
}else {
router.push('/')
}
resetFields()
}
@ -198,7 +202,11 @@ export default defineComponent({
}
const onCancel = () => {
if(isEdit) {
router.push('networks')
}else {
router.push('/')
}
}
const fillTemplate = (network: typeof mainNets[1] ) =>{

View File

@ -5,16 +5,48 @@
<ion-title>Assets</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Schedule Tab</ion-content>
<ion-content class="ion-padding">
1
</ion-content>
<ion-content class="ion-padding">
2
</ion-content>
<ion-content class="ion-padding">
3
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from "@ionic/vue";
import { defineComponent, Ref, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter } from "@ionic/vue";
import { getSelectedAccount } from "@/utils/platform"
import type { Account } from "@/extension/types"
const yupAssetsApi = 'https://api.yup.io/profile'
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
setup: () => {
const selectedAccount = ref({}) as Ref<Account>
const assets = ref({})
const loading = ref(true)
const isError = ref(false)
const noAssets = ref(false)
onIonViewWillEnter(async () => {
selectedAccount.value = await getSelectedAccount()
const req = await fetch(`${yupAssetsApi}/${selectedAccount.value.address}`)
if(req.ok) {
assets.value = (await req.json()) ?? {}
if(!('poaps' in assets.value) && !('tokens' in assets.value) && !('nfts' in assets.value)) {
noAssets.value = true
}
}else {
isError.value = true
}
loading .value = false
})
}
});
</script>

View File

@ -6,15 +6,29 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">Schedule Tab</ion-content>
<ion-content class="ion-padding">Not implemented</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from "@ionic/vue";
import { defineComponent, Ref, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter } from "@ionic/vue";
import { getHistory } from '@/utils/platform'
import type { HistoryItem } from '@/extension/types'
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
setup: () => {
const history = ref([]) as Ref<HistoryItem[]>;
const loading = ref(true)
onIonViewWillEnter(async () => {
history.value = await getHistory()
loading.value = false
})
return {
history,
loading
}
}
});
</script>

View File

@ -15,7 +15,7 @@
<ion-label>Selected Account: {{ selectedAccount?.name }}</ion-label>
<ion-button @click="accountsModal = true">Select</ion-button>
</ion-item>
<ion-item button @click="copyAddress(selectedAccount.address, getToastRef())">
<ion-item button @click="copyAddress(selectedAccount?.address, getToastRef())">
<p style="font-size: 0.7rem">{{ selectedAccount?.address }}</p>
<ion-icon style="margin-left: 0.5rem" :icon="copyOutline"></ion-icon>
</ion-item>
@ -67,7 +67,7 @@
</ion-header>
<ion-content class="ion-padding">
<ion-list style="margin-bottom: 4rem">
<ion-radio-group :value="selectedAccount.address">
<ion-radio-group :value="selectedAccount?.address ?? ''">
<ion-list-header>
<ion-label>Accounts</ion-label>
</ion-list-header>
@ -168,10 +168,12 @@ import {
replaceNetworks,
getUrl,
saveSelectedNetwork,
numToHexStr
} from "@/utils/platform";
import type { Network, Account, Networks } from "@/extension/types";
import { mainNets } from "@/utils/networks";
import router from "@/router";
import { triggerListner } from '@/extension/listners'
import { copyOutline } from "ionicons/icons";
@ -251,8 +253,9 @@ export default defineComponent({
// console.log(({ [address]: accounts.value[address], ...accounts.value}))
accounts.value.splice(findIndex, 1);
accounts.value.splice(0,0,selectedAccount.value)
await replaceAccounts([...accounts.value])
const newAccounts = [...accounts.value]
await replaceAccounts(newAccounts)
triggerListner('accountsChanged', newAccounts.map(a => a.address))
}
accountsModal.value = false;
loading.value = false;
@ -266,6 +269,7 @@ export default defineComponent({
Object.assign({ [chainId]: networks.value[chainId] }, networks.value)
);
selectedNetwork.value = networks.value[chainId];
triggerListner('chainChanged', numToHexStr(chainId))
}
networksModal.value = false;
loading.value = false;

View File

@ -14,6 +14,11 @@
</ion-header>
<ion-content class="ion-padding">
<ion-item v-if="loading || Object.keys(networks).length < 1">
<ion-label>No EVM Networks found</ion-label>
<ion-button @click="goToAddNetwork">Add Network</ion-button>
</ion-item>
<ion-list v-for="network of networks" :key="network.chainId">
<ion-item>
<ion-avatar v-if="(mainNets as any)[network.chainId]?.icon" style="margin-right: 1rem; width: 1.8rem; height:1.8rem;">
@ -101,6 +106,12 @@ export default defineComponent({
router.push(`add-network/edit/${chainId}`)
}
const goToAddNetwork = () => {
router.push("/tabs/add-network");
};
onIonViewWillEnter(() => {
loadData()
})
@ -115,7 +126,9 @@ export default defineComponent({
getUrl,
mainNets,
deleteNetwork,
editNetwork
editNetwork,
loading,
goToAddNetwork
}
}

View File

@ -13,7 +13,8 @@
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<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>
@ -24,16 +25,16 @@
</ion-list>
<ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label>
<ion-toggle :key="updateKey" slot="end" :checked="settings.s.lockOutEnabled"></ion-toggle>
<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-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-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-item :disabled="!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled">
<ion-button @click="setTime">Set Auto-lock</ion-button>
</ion-item>
</ion-list>
@ -136,9 +137,10 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="mpModal=false">Close</ion-button>
<ion-button @click="modalGetPassword?.reject ? (() => { modalGetPassword.reject(); modalGetPassword = null })() : mpModal=false">Close</ion-button>
</ion-buttons>
<ion-title>Create Encryption Password</ion-title>
<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">
@ -166,13 +168,13 @@
</ion-list>
</div>
<ion-item>
<ion-button @click="confirmModal">Confirm</ion-button>
<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="Error"
:header="alertHeader"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen=false"
@ -184,8 +186,9 @@
<script lang="ts">
import { defineComponent, ref, reactive, Ref } from "vue";
import { storageWipe, getSettings, setSettings, getAccounts, saveSelectedAccount, replaceAccounts } from "@/utils/platform";
import { decrypt, encrypt } from "@/utils/webCrypto"
// import { Account } from '@/extension/type'
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,
@ -244,7 +247,11 @@ export default defineComponent({
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 wipeStorage = async () => {
loading.value = true;
@ -266,6 +273,12 @@ export default defineComponent({
updateKey.value++
}
const changeAutoLock = async () => {
settings.s.lockOutEnabled = !settings.s.lockOutEnabled
updateKey.value++
await saveSettings()
}
const changeEncryption = async () => {
loading.value = true
mpModal.value = true
@ -276,6 +289,7 @@ export default defineComponent({
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)
@ -285,14 +299,16 @@ export default defineComponent({
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(mpPass.value, a.pk)
a.encPk = await encrypt(a.pk, cryptoParams)
a.pk = ''
return a
})
@ -307,12 +323,14 @@ export default defineComponent({
} 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, mpPass.value)
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])
@ -323,8 +341,10 @@ export default defineComponent({
mpPass.value = ''
mpConfirm.value = ''
mpModal.value = false
} catch {
} 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)
@ -348,13 +368,13 @@ export default defineComponent({
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" },{...}]' })
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) || !(e.pk.length !== 66 && e.pk.length !== 64)))
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" },{...}], Also PK must be valid' })
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 })
return resolve({ error: false, json })
}
reader.readAsText(importFile.value?.files?.[0] as File);
@ -367,6 +387,42 @@ export default defineComponent({
})
}
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) {
@ -374,20 +430,71 @@ export default defineComponent({
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( () => {
getSettings().then((storeSettings) =>
onIonViewWillEnter(async () => {
await Promise.all([getSettings().then((storeSettings) =>
{
settings.s = storeSettings
loading.value = false
})
}),
getAccounts().then((accounts) => {
if(accounts.length) {
noAccounts.value = false
}
})])
loading.value = false
})
const setTime = async () => {
@ -428,7 +535,11 @@ export default defineComponent({
toastMsg,
importAcc,
exportAcc,
importFile
importFile,
modalGetPassword,
noAccounts,
alertHeader,
changeAutoLock
};
},
});

View File

@ -69,7 +69,7 @@ import {
replaceAccounts,
saveSelectedAccount
} from "@/utils/platform";
import { decrypt } from "@/utils/webCrypto"
import { decrypt, getCryptoParams } from "@/utils/webCrypto"
export default defineComponent({
props: {
@ -107,8 +107,9 @@ export default defineComponent({
try {
loading.value = true
let accounts = await getAccounts()
const cryptoParams = await getCryptoParams(mpPass.value)
const accProm = accounts.map(async a => {
a.pk = await decrypt(a.encPk, mpPass.value)
a.pk = await decrypt(a.encPk, cryptoParams)
return a
})
accounts = await Promise.all(accProm)

93
src/views/WalletError.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Contract Error</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-label>Transaction was aboreted before being sent</ion-label>
</ion-item>
<ion-item>
<ion-label>Error:</ion-label>
<ion-textarea
style="overflow-y: scroll"
:rows="10"
:cols="20"
:value="error"
readonly
></ion-textarea>
</ion-item>
<ion-item>
<ion-button @click="onCancel">Exit</ion-button>
</ion-item>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonTextarea,
onIonViewWillEnter,
IonLoading,
} from "@ionic/vue";
import { useRoute } from "vue-router";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonTextarea,
IonLoading,
},
setup: () => {
const route = useRoute();
const error = decodeURIComponent((route.params?.param as string) ?? "");
const loading = ref(true);
const contract = (route.params?.contract as string) ?? "";
const onCancel = () => {
window.close();
};
onIonViewWillEnter(async () => {
(window as any)?.resizeTo?.(700, 600);
loading.value = false;
});
return {
onCancel,
contract,
loading,
error,
};
},
});
</script>