dev: 1.0.4

This commit is contained in:
Andrei O 2022-10-15 23:20:33 +03:00
parent 6c18b2841d
commit b376839770
No known key found for this signature in database
GPG Key ID: B961E5B68389457E
24 changed files with 1017 additions and 255 deletions

4
.gitignore vendored
View File

@ -30,4 +30,6 @@ npm-debug.log*
/plugins
/www
/src/extension/inject.js
readme.md
README.md
PRIVACY_POLICY.md
LICENSE

View File

@ -0,0 +1,10 @@
{
"appName": {
"message": "Clear EVM Wallet (CLW)",
"description": "This is an open-source EVM wallet based on ethers, ionic, vue, that implements the metamask API."
},
"appDesc": {
"message": "Clear EVM Wallet (CLW)",
"description": "This is an open-source EVM wallet based on ethers, ionic, vue, that implements the metamask API."
}
}

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 700 700" width="700" height="700" opacity="0.91"><defs><linearGradient gradientTransform="rotate(201, 0.5, 0.5)" x1="50%" y1="0%" x2="50%" y2="100%" id="ffflux-gradient"><stop stop-color="hsl(272, 100%, 27%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(296, 66%, 61%)" stop-opacity="1" offset="100%"></stop></linearGradient><filter id="ffflux-filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feTurbulence type="fractalNoise" baseFrequency="0.006 0.004" numOctaves="2" seed="83" stitchTiles="stitch" x="0%" y="0%" width="100%" height="100%" result="turbulence"></feTurbulence>
<feGaussianBlur stdDeviation="24 37" x="0%" y="0%" width="100%" height="100%" in="turbulence" edgeMode="duplicate" result="blur"></feGaussianBlur>
<feBlend mode="screen" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" in2="blur" result="blend"></feBlend>
</filter></defs><rect width="700" height="700" fill="url(#ffflux-gradient)" filter="url(#ffflux-filter)"></rect></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -6,8 +6,9 @@
<script lang="ts">
import { IonApp, IonRouterOutlet } from "@ionic/vue";
import { defineComponent } from "vue";
import { useRoute, useRouter} from "vue-router";
import { defineComponent, onBeforeMount } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getSettings } from '@/utils/platform'
export default defineComponent({
name: "App",
@ -21,6 +22,14 @@ const router = useRouter()
const { param, rid } = route.query;
console.log(route?.query,'zzzzzzzzzzzzzzz')
onBeforeMount( () => {
getSettings().then((settings) => {
if(settings.theme !== 'system') {
document.body.classList.remove(settings.theme === 'dark' ? 'light': 'dark')
document.body.classList.add(settings.theme)
}
})
})
switch (route?.query?.route ?? "") {
case "sign-msg": {
@ -43,7 +52,7 @@ switch (route?.query?.route ?? "") {
}
case "wallet-error": {
router.push({
path: `/wallet-error"/${rid}/${param}`
path: `/wallet-error/${rid}/${param}`
});
break;
}

View File

@ -1,4 +1,5 @@
import { getSelectedNetwork, numToHexStr } from "@/utils/platform";
import type { RequestArguments } from '@/extension/types'
const allowedMethods = {
'eth_accounts': true,
@ -22,10 +23,11 @@ window.addEventListener("message", (event) => {
if (event.data.type && (event.data.type === "CLWALLET_CONTENT")) {
event.data.data.resId = event.data.resId
event.data.data.type = "CLWALLET_CONTENT_MSG"
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, website: window?.location?.href ?? '' };
console.log('data back', data)
// console.log('data back', data)
window.postMessage(data, "*");
})
}
@ -36,18 +38,24 @@ window.addEventListener("message", (event) => {
} else if (event.data.type && (event.data.type === "CLWALLET_PING")) {
getSelectedNetwork().then(network => {
const data = { type: "CLWALLET_PAGE_LISTENER", data: {
listener: 'connected',
listner: 'connect',
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)
}
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
chrome.runtime.onMessage.addListener((message: RequestArguments , sender, sendResponse) => {
if(message.type === "CLWALLET_EXT_LISTNER") {
const data = { type: "CLWALLET_PAGE_LISTENER", data: message.data };
// console.log('data listner', data)
window.postMessage(data, "*");
}
return true
});
(function() {

View File

@ -18,7 +18,7 @@ const listner = function(event: any) {
if (event.data.type && (event.data.type === "CLWALLET_PAGE")) {
if(event?.data?.data?.error){
promResolvers[event.data.resId].reject(event.data.data);
console.log('rejected')
// console.log('rejected')
}else {
promResolvers[event.data.resId].resolve(event.data.data);
}
@ -45,7 +45,7 @@ return new Promise((resolve, reject) => {
if (ping) {
data.type = 'CLWALLET_PING'
}
console.log('data in', data)
// console.log('data in', data)
window.postMessage(data, "*");
})
}
@ -149,6 +149,9 @@ const eth = new Proxy({
break
case 'connect':
listners.connect.add(callback)
sendMessage({
method: 'wallet_ready'
}, true)
break;
case 'disconnect':
listners.disconnect.add(callback)
@ -161,6 +164,24 @@ const eth = new Proxy({
return
}
},
removeListener: (eventName: string, callback: () => void) => {
switch (eventName) {
case 'accountsChanged':
listners.accountsChanged.delete(callback)
break
case 'connect':
listners.connect.delete(callback)
break;
case 'disconnect':
listners.disconnect.delete(callback)
break;
case 'chainChanged':
listners.chainChanged.delete(callback)
break;
default:
return
}
},
// Simulate Metamask
_warnOfDeprecation: () => null,
_state: {},
@ -200,25 +221,21 @@ const injectWallet = (win: any) => {
return
}
});
console.log('Clear wallet injected', (window as any).ethereum, win)
// 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')});
(<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)
// 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) => console.log('chainChanged', a, typeof a)));
// }, 3500)
// console.log( (window as any).ethereum.request({method: 'eth_chainId'}))

View File

@ -2,6 +2,11 @@ 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)
chrome.tabs.query({}, (tabs) => tabs.forEach( tab =>
{
if (tab?.id) {
chrome.tabs.sendMessage(tab.id, data)
}
}
));
}

View File

@ -80,7 +80,9 @@ if (!chrome.notifications.onButtonClicked.hasListener(viewTxListner)){
}
chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendResponse) => {
console.log(message);
if(message?.type !== "CLWALLET_CONTENT_MSG") {
return true
}
(async () => {
if (!('method' in message)) {
sendResponse({
@ -147,7 +149,6 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
}
case 'eth_chainId': {
const network = await getSelectedNetwork()
console.log(network, 'network')
const chainId = network?.chainId ?? 0
sendResponse(`0x${chainId.toString(16)}`)
break
@ -168,7 +169,7 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
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 ?? '')}`),
url: chrome.runtime.getURL(`index.html?route=wallet-error&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
@ -177,7 +178,7 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
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 ?? '')}`),
url: chrome.runtime.getURL(`index.html?route=wallet-error&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
@ -242,12 +243,17 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
} as any)
} catch (err) {
console.log(err)
sendResponse({
error: true,
code: rpcError.USER_REJECTED,
message: 'TX Failed'
})
chrome.windows.create({
height: 450,
width: 400,
url: chrome.runtime.getURL(`index.html?route=wallet-error&param=${encodeURIComponent(String(err))}&rid=${String(message?.resId ?? '')}`),
type: 'popup'
})
chrome.notifications.create({
message: 'Transaction Failed',
title: 'Error',
@ -274,7 +280,7 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
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 ?? '')}`),
url: chrome.runtime.getURL(`index.html?route=wallet-error&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
@ -360,7 +366,8 @@ chrome.runtime.onMessage.addListener((message: RequestArguments, sender, sendRes
}
try {
chrome.windows.remove(sender.tab?.windowId ?? 0)
}catch{
}catch (e) {
console.log(e)
// ignore
}
break

View File

@ -24,16 +24,18 @@ export interface Networks {
}
export interface RequestArguments {
method: string;
params?: any[];
method: string
type: string
params?: any[]
resId?: string
website?: string
data?: any
}
export interface ProviderRpcError extends Error {
message: string;
code: number;
data?: unknown;
message: string
code: number
data?: unknown
}
export interface Price {

View File

@ -1,32 +1,32 @@
export const userReject = {} as Record<string, (() => any) | undefined>
export const userApprove = {} as Record<string, ((a: unknown) => any) | undefined>
export const userReject = {} as Record<string, (() => any) | undefined>
export const userApprove = {} as Record<string, ((a: unknown) => any) | undefined>
export const rIdWin = {} as Record<string, string | undefined>
export const rIdData = {} as Record<string, any | undefined>
export const approve = (rId: string) => {
chrome.runtime.sendMessage({ method: 'wallet_approve', resId: rId })
chrome.runtime.sendMessage({ method: 'wallet_approve', resId: rId, type: 'CLWALLET_CONTENT_MSG' })
}
export const walletSendData = (rId: string, data: any) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_send_data', resId: rId, data}, (r) => {
resolve(r)
})
chrome.runtime.sendMessage({ method: 'wallet_send_data', resId: rId, data, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
resolve(r)
})
})
}
export const walletGetData = (rId: string) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_get_data', resId: rId }, (r) => {
resolve(r)
})
chrome.runtime.sendMessage({ method: 'wallet_get_data', resId: rId, type: 'CLWALLET_CONTENT_MSG' }, (r) => {
resolve(r)
})
})
}
export const walletPing = () => {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ method: 'wallet_ping' }, (r) => {
chrome.runtime.sendMessage({ method: 'wallet_ping', type: 'CLWALLET_CONTENT_MSG' }, (r) => {
resolve(r)
})
})
}
})
}

View File

@ -24,7 +24,7 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('@/views/ContractError.vue'),
},
{
path: '/wallet-error/:rid/:error',
path: '/wallet-error/:rid/:param',
component: () => import('@/views/WalletError.vue'),
},
{

View File

@ -2,7 +2,167 @@
http://ionicframework.com/docs/theming/ */
/** Ionic CSS Variables **/
:root {
@media (prefers-color-scheme: dark){
/*
* Dark Colors
* -------------------------------------------
*/
body:not(.light) {
--ion-color-primary: #6a64ff;
--ion-color-primary-rgb: 106, 100, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #5d58e0;
--ion-color-primary-tint: #7974ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body:not(.light) {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body:not(.light) {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
}
:root, body.light {
/** primary **/
--ion-color-primary: #5260ff;
--ion-color-primary-rgb: 82, 96, 255;
@ -76,161 +236,155 @@ http://ionicframework.com/docs/theming/ */
--ion-color-light-tint: #f5f6f9;
}
@media (prefers-color-scheme: dark) {
/*
* Dark Colors
* -------------------------------------------
*/
body {
--ion-color-primary: #6a64ff;
--ion-color-primary-rgb: 106, 100, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #5d58e0;
--ion-color-primary-tint: #7974ff;
body.dark {
--ion-color-primary: #6a64ff;
--ion-color-primary-rgb: 106, 100, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #5d58e0;
--ion-color-primary-tint: #7974ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80,200,255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255,255,255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106,100,255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255,255,255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47,223,117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0,0,0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255,213,52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0,0,0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255,73,97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255,255,255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244,245,248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0,0,0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152,154,162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0,0,0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
--ion-color-light: #222428;
--ion-color-light-rgb: 34,36,40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255,255,255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
.ios body.dark {
--ion-background-color: #000000;
--ion-background-color-rgb: 0,0,0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-item-background: #000000;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
--ion-card-background: #1c1c1d;
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
.ios ion-modal {
--ion-background-color: var(--ion-color-step-100);
--ion-toolbar-background: var(--ion-color-step-150);
--ion-toolbar-border-color: var(--ion-color-step-250);
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
.md body.dark {
--ion-background-color: #121212;
--ion-background-color-rgb: 18,18,18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255,255,255;
--ion-border-color: #222222;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}
--ion-card-background: #1e1e1e;
}

View File

@ -94,6 +94,10 @@ export const addToHistory = async (historyItem: HistoryItem): Promise<void> => {
await storageSave('history', history)
}
export const wipeHistory = async (): Promise<void> => {
await storageSave('history', [])
}
export const getSettings = async (): Promise<Settings> => {
return (await storageGet('settings'))?.settings ?? defaultSettings as unknown as Settings
}
@ -140,6 +144,9 @@ export const smallRandomString = () => {
export const clearPk = async (): Promise<void> => {
let accounts = await getAccounts()
const accProm = accounts.map(async a => {
if(a.encPk) {
a.pk = ''
}
return a
})
accounts = await Promise.all(accProm)

View File

@ -10,8 +10,8 @@ export const signMsg = async (msg: string) => {
export const getBalance = async () =>{
const account = await getSelectedAccount()
const network = await getSelectedNetwork()
const wallet = new ethers.Wallet(account.pk, new ethers.providers.JsonRpcProvider(network.rpc))
return await wallet.getBalance()
const provider = new ethers.providers.JsonRpcProvider(network.rpc)
return await provider.getBalance(account.address)
}
export const getGasPrice = async () => {

View File

@ -34,18 +34,42 @@
<p style="font-size:0.7rem">{{ account.address }}</p><ion-icon :icon="copyOutline"></ion-icon>
</ion-item>
<ion-item>
<ion-chip>View Pk</ion-chip>
<ion-chip @click="viewPk(account.address)">View Pk</ion-chip>
<ion-chip @click="deleteAccount(account.address)">Delete</ion-chip>
<ion-chip @click="editAccount(account.address)">Edit Name</ion-chip>
</ion-item>
</ion-list>
<ion-modal
:is-open="pkModal"
@didDismiss="shownPk=''"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button @click="pkModal=false">Close</ion-button>
</ion-buttons>
<ion-title>View PK</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item @click="copyAddress(shownPk, getToastRef())" button>
<ion-icon style="margin-right: 0.5rem;" :icon="copyOutline" />
<ion-label button>PK</ion-label>
<ion-input id="pastePk" v-model="shownPk" readonly></ion-input>
</ion-item>
</ion-content>
</ion-modal>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, Ref } from "vue";
import { getAccounts, copyAddress, replaceAccounts } from "@/utils/platform"
import { getAccounts, copyAddress, replaceAccounts, getSettings, clearPk } from "@/utils/platform"
import {
IonContent,
IonHeader,
@ -60,12 +84,17 @@ import {
IonButtons,
IonButton,
onIonViewWillEnter,
IonToast
IonToast,
modalController,
IonInput,
IonModal
} from "@ionic/vue";
import { addCircleOutline, copyOutline } from "ionicons/icons";
import router from "@/router";
import type { Account } from '@/extension/types'
import UnlockModal from '@/views/UnlockModal.vue'
import type { Account, Settings } from '@/extension/types'
export default defineComponent({
components: {
@ -81,19 +110,26 @@ export default defineComponent({
IonChip,
IonButtons,
IonButton,
IonToast
IonToast,
IonInput,
IonModal
},
setup () {
const accounts = ref({}) as Ref<Account[]>
const accounts = ref([]) as Ref<Account[]>
const loading = ref(true)
const toastState = ref(false)
const shownPk = ref('')
const pkModal = ref(false)
const settings = ref({}) as Ref<Settings>
const getToastRef = () => toastState
const loadData = () => {
const pAccounts = getAccounts()
Promise.all([pAccounts]).then(( res ) => {
const pGetSettings = getSettings()
Promise.all([pAccounts, pGetSettings]).then(( res ) => {
accounts.value = res[0]
settings.value = res[1]
loading.value = false
})
}
@ -120,6 +156,45 @@ export default defineComponent({
loadData()
})
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: 'viewPk'
}
});
modal.present();
const { role } = await modal.onWillDismiss();
if(role === 'confirm') return true
return false
}
const viewPk = async (addr: string) => {
let pk = ''
const account = accounts.value.find(a => a.address === addr)
if(settings.value.enableStorageEnctyption) {
if(account?.encPk) {
const modalR = await openModal()
if(modalR){
const account = (await getAccounts()).find(a => a.address === addr)
pk = account?.pk ?? ''
}
}else {
pk = account?.pk ?? ''
}
}else {
pk = account?.pk ?? ''
}
if(pk) {
shownPk.value = pk
if(settings.value.encryptAfterEveryTx) {
clearPk()
}
pkModal.value = true
}
}
return {
accounts,
addCircleOutline,
@ -130,7 +205,10 @@ export default defineComponent({
deleteAccount,
editAccount,
loading,
goToAddAccount
goToAddAccount,
viewPk,
pkModal,
shownPk
}
}

View File

@ -27,7 +27,7 @@
</ion-item>
<ion-item>
<ion-button @click="onCancel">Cancel</ion-button>
<ion-button @click="onAddAccount">Add Account</ion-button>
<ion-button @click="onAddAccount">{{ isEdit ? 'Edit Account' : 'Add Account' }}</ion-button>
</ion-item>
<ion-alert
:is-open="alertOpen"
@ -42,12 +42,14 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonAlert, IonIcon, onIonViewWillEnter } from "@ionic/vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonInput, IonButton, IonAlert, IonIcon, onIonViewWillEnter, modalController } from "@ionic/vue";
import { ethers } from "ethers"
import { saveSelectedAccount, getAccounts, saveAccount, getRandomPk, smallRandomString, paste } from "@/utils/platform";
import { saveSelectedAccount, getAccounts, saveAccount, getRandomPk, smallRandomString, paste, getSettings } from "@/utils/platform";
import router from "@/router";
import { useRoute } from 'vue-router'
import type { Account } from '@/extension/types'
import type { Account, Settings } from '@/extension/types'
import UnlockModal from '@/views/UnlockModal.vue'
import { encrypt, getCryptoParams } from '@/utils/webCrypto'
import { clipboardOutline } from "ionicons/icons";
@ -62,15 +64,31 @@ export default defineComponent({
const isEdit = route.path.includes('/edit')
const paramAddress = route.params.address ?? ""
let accountsProm: Promise<Account[] | undefined>
let settingsProm: Promise<Settings | undefined>
const resetFields = () => {
name.value = ''
pk.value = ''
}
const openModal = async () => {
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: 'addAccount'
}
});
modal.present();
const { role, data } = await modal.onWillDismiss();
if(role === 'confirm') return data
return false
}
onIonViewWillEnter(async () => {
if(isEdit && paramAddress) {
accountsProm = getAccounts()
settingsProm = getSettings()
const accounts = await accountsProm as Account[]
const acc = accounts.find(account => account.address === paramAddress)
if(acc) {
@ -95,7 +113,40 @@ export default defineComponent({
if(!accountsProm) {
accountsProm = getAccounts()
}
if(!settingsProm) {
settingsProm = getSettings()
}
const accounts = await accountsProm as Account[]
const settings = await settingsProm as Settings
if( settings.enableStorageEnctyption) {
const pass = await openModal()
if(!pass){
alertMsg.value = "Cannot add account with encryption password."
alertOpen.value = true
return
}
const cryptoParams = await getCryptoParams(pass)
if((accounts.length ?? 0) < 1 ){
p1 = saveSelectedAccount({
address: wallet.address,
name: name.value,
pk: pk.value,
encPk: await encrypt(pk.value, cryptoParams)
})
} else {
if(accounts.find(account => account.address === wallet.address)){
alertMsg.value = "Account already exists."
return alertOpen.value = true
}
}
const p2 = saveAccount({
address: wallet.address,
name: name.value,
pk: pk.value,
encPk: await encrypt(pk.value, cryptoParams)
})
await Promise.all([p1, p2])
}else {
if((accounts.length ?? 0) < 1 ){
p1 = saveSelectedAccount({
address: wallet.address,
@ -116,10 +167,11 @@ export default defineComponent({
encPk: ''
})
await Promise.all([p1, p2])
}
if(isEdit) {
router.push('accounts')
router.push('/tabs/accounts')
}else {
router.push('/')
router.push('/tabs/home')
}
resetFields()
}
@ -134,9 +186,9 @@ export default defineComponent({
const onCancel = () => {
if(isEdit) {
router.push('accounts')
router.push('/tabs/accounts')
}else {
router.push('/')
router.push('/tabs/home')
}
}

View File

@ -6,7 +6,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button @click="templateModal=true" expand="block">Add from popular chain list</ion-button>
<ion-button v-if="!isEdit" @click="templateModal=true" expand="block">Add from popular chain list</ion-button>
<ion-item>
<ion-label>Name(*)</ion-label>
<ion-input v-model="name" placeholder="ex: Polygon"></ion-input>
@ -190,9 +190,9 @@ export default defineComponent({
const p2 = replaceNetworks(networks)
await Promise.all([p1, p2])
if(isEdit) {
router.push('networks')
router.push('/tabs/networks')
}else {
router.push('/')
router.push('/tabs/home')
}
resetFields()
}
@ -203,9 +203,9 @@ export default defineComponent({
const onCancel = () => {
if(isEdit) {
router.push('networks')
router.push('/tabs/networks')
}else {
router.push('/')
router.push('/tabs/home')
}
}

View File

@ -6,33 +6,226 @@
</ion-toolbar>
</ion-header>
<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>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
<ion-toast
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
<ion-item>
<ion-label>Assests for Account: {{ selectedAccount?.name }}</ion-label>
</ion-item>
<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>
<template v-if="isError">
Assets info could not be retrieved because of an http error, API down or conectivity issues.
</template>
<template v-else-if="noAssets">
No assets found for this wallet address.
</template>
<template v-else>
<ion-item v-if="assets.yupScore">
<span style="font-size: 0.9rem">YUP Score:</span> <span style="font-size: 1.1rem; margin-left: 0.5rem">{{ assets.yupScore.toFixed(2) }}</span>
</ion-item>
<ion-item>
<p style="font-size: 0.7rem">YUP score is a score of your wallet based on assets and transactions. </p>
</ion-item>
<template v-if="assets.tokens">
<template v-if="ethTokens.length">
<ion-item>Ethereum Tokens</ion-item>
<ion-list>
<ion-item v-for="token of ethTokens" :key="token.address">
<ion-avatar
v-if="token?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="token?.name"
:src="token?.image"
@error="token.image = getUrl('assets/randomGrad.svg')"
/>
</ion-avatar>
<ion-label><b>{{ token?.symbol }}:</b> {{ token?.balance }}</ion-label>
</ion-item>
<ion-item v-if="hasMore.ethTokens">
<ion-button @click="loadMore('ethTokens')">Load More</ion-button>
</ion-item>
</ion-list>
</template>
<template v-if="polyTokens.length">
<ion-item>Polygon Tokens</ion-item>
<ion-list>
<ion-item v-for="token of polyTokens" :key="token.address">
<ion-avatar
v-if="token?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="token?.name"
:src="token?.image"
@error="token.image = getUrl('assets/randomGrad.svg')"
/>
</ion-avatar>
<ion-label><b>{{ token?.symbol }}:</b> {{ token?.balance }}</ion-label>
</ion-item>
<ion-item v-if="hasMore.polyTokens">
<ion-button @click="loadMore('polyTokens')">Load More</ion-button>
</ion-item>
</ion-list>
</template>
</template>
<template v-if="assets.nfts">
<template v-if="ethNfts.length">
<ion-item>Ethereum NFTs</ion-item>
<ion-list>
<ion-item v-for="nft of ethNfts" :key="nft.address">
<ion-avatar
v-if="nft?.imageURI"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="nft?.collectionName"
:src="nft?.imageURI"
@error="nft.imageURI = getUrl('assets/randomGrad.svg')"
/>
</ion-avatar>
<ion-label><b>{{ nft?.collectionName }}</b></ion-label>
</ion-item>
<ion-item v-if="hasMore.ethNfts">
<ion-button @click="loadMore('ethNfts')">Load More</ion-button>
</ion-item>
</ion-list>
</template>
<template v-if="polyNfts.length">
<ion-item>Polygon NFTs</ion-item>
<ion-list>
<ion-item v-for="nft of polyNfts" :key="nft.address">
<ion-avatar
v-if="nft?.imageURI"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="nft?.collectionName"
:src="nft?.imageURI"
@error="nft.imageURI = getUrl('assets/randomGrad.svg')"
/>
</ion-avatar>
<ion-label><b>{{ nft?.collectionName }}</b></ion-label>
</ion-item>
<ion-item v-if="hasMore.polyNfts">
<ion-button @click="loadMore('polyNfts')">Load More</ion-button>
</ion-item>
</ion-list>
</template>
</template>
<template v-if="assets.poaps">
<template v-if="poaps.length">
<ion-item>POAPs</ion-item>
<ion-list>
<ion-item v-for="nft of poaps" :key="nft.eventId">
<ion-avatar
v-if="nft?.image"
style="margin-right: 1rem; width: 1.8rem; height: 1.8rem"
>
<img
:alt="nft?.title"
:src="nft?.image"
@error="nft.image = getUrl('assets/randomGrad.svg')"
/>
</ion-avatar>
<ion-label><b>{{ nft?.title }}</b></ion-label>
</ion-item>
<ion-item v-if="hasMore.poaps">
<ion-button @click="loadMore('poaps')">Load More</ion-button>
</ion-item>
</ion-list>
</template>
</template>
</template>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, Ref, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter } from "@ionic/vue";
import { getSelectedAccount } from "@/utils/platform"
import { defineComponent, Ref, ref, reactive } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter, IonItem, IonLabel, IonAvatar, IonList, IonButton, IonToast, IonLoading, IonIcon } from "@ionic/vue";
import { getSelectedAccount, copyAddress, getUrl } from "@/utils/platform"
import type { Account } from "@/extension/types"
import { copyOutline } from "ionicons/icons";
const yupAssetsApi = 'https://api.yup.io/profile'
interface IProfileToken {
address: string
balance: number
image: string
name: string
symbol: string
}
interface IProfileNFT {
address: string
collectionImageURI: string
collectionName: string
imageURI: string
link: string
tokenId: number
}
interface IProfilePOAP {
description: string
eventId: string
image: string
link: string
title: string
}
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonLabel, IonAvatar, IonList, IonButton, IonToast, IonLoading, IonIcon },
setup: () => {
const selectedAccount = ref({}) as Ref<Account>
const assets = ref({})
const assets = ref({}) as Ref< {
yupScore?: number
tokens?: any[]
nfts?: any[]
poaps?: any[]
}>
const loading = ref(true)
const isError = ref(false)
const noAssets = ref(false)
const toastState = ref(false);
const ethTokens = ref([]) as Ref< IProfileToken[]>
const polyTokens = ref([]) as Ref< IProfileToken[]>
const ethNfts = ref([]) as Ref< IProfileNFT[]>
const polyNfts = ref([]) as Ref< IProfileNFT[]>
const poaps = ref([]) as Ref< IProfilePOAP[]>
const hasMore = reactive({
poaps: true,
ethTokens: true,
polyTokens: true,
ethNfts: true,
polyNfts: true,
})
const getToastRef = () => toastState;
onIonViewWillEnter(async () => {
selectedAccount.value = await getSelectedAccount()
@ -42,11 +235,100 @@ export default defineComponent({
if(!('poaps' in assets.value) && !('tokens' in assets.value) && !('nfts' in assets.value)) {
noAssets.value = true
}
if ('poaps' in assets.value) {
poaps.value = assets.value?.poaps?.slice(0, 10) ?? []
if(poaps.value.length >= (assets.value?.poaps?.length ?? 0)) {
hasMore.poaps = false
}
}
if ('nfts' in assets.value) {
ethNfts.value = assets.value?.nfts?.filter(n => n.chain === 'ethereum').slice(0, 10) ?? []
if(ethNfts.value.length >= (assets.value?.nfts?.filter(n => n.chain === 'ethereum').length ?? 0)) {
hasMore.ethNfts = false
}
polyNfts.value = assets.value?.nfts?.filter(n => n.chain === 'polygon').slice(0, 10) ?? []
if(polyNfts.value.length >= (assets.value?.nfts?.filter(n => n.chain === 'polygon').length ?? 0)) {
hasMore.polyNfts = false
}
}
if ('tokens' in assets.value) {
ethTokens.value = assets.value?.tokens?.filter(n => n.chain === 'ethereum').slice(0, 10) ?? []
if(ethTokens.value.length >= (assets.value?.tokens?.filter(n => n.chain === 'ethereum').length ?? 0)) {
hasMore.ethTokens = false
}
polyTokens.value = assets.value?.tokens?.filter(n => n.chain === 'polygon').slice(0, 10) ?? []
if(polyTokens.value.length >= (assets.value?.tokens?.filter(n => n.chain === 'polygon').length ?? 0)) {
hasMore.polyTokens = false
}
}
}else {
isError.value = true
}
loading .value = false
})
const loadMore = (type: string) => {
switch(type) {
case 'ethTokens': {
ethTokens.value = assets.value?.tokens?.filter(n => n.chain === 'ethereum').slice(0, ethTokens.value.length + 10) ?? []
if(ethTokens.value.length >= (assets.value?.tokens?.filter(n => n.chain === 'ethereum').length ?? 0)) {
hasMore.ethTokens = false
}
break
}
case 'polyTokens': {
polyTokens.value = assets.value?.tokens?.filter(n => n.chain === 'polygon').slice(0, polyTokens.value.length + 10) ?? []
if(polyTokens.value.length >= (assets.value?.tokens?.filter(n => n.chain === 'polygon').length ?? 0)) {
hasMore.polyTokens = false
}
break
}
case 'ethNfts': {
ethNfts.value = assets.value?.nfts?.filter(n => n.chain === 'ethereum').slice(0, ethNfts.value.length + 10) ?? []
if(ethNfts.value.length >= (assets.value?.nfts?.filter(n => n.chain === 'ethereum').length ?? 0)) {
hasMore.ethNfts = false
}
break
}
case 'polyNfts': {
polyNfts.value = assets.value?.nfts?.filter(n => n.chain === 'polygon').slice(0, polyNfts.value.length + 10) ?? []
if(polyNfts.value.length >= (assets.value?.nfts?.filter(n => n.chain === 'polygon').length ?? 0)) {
hasMore.polyNfts = false
}
break
}
case 'poaps': {
poaps.value = assets.value?.poaps?.slice(0, poaps.value.length + 10) ?? []
if(poaps.value.length >= (assets.value?.poaps?.length ?? 0)) {
hasMore.poaps = false
}
break
}
}
}
return {
selectedAccount,
loading,
isError,
noAssets,
assets,
getToastRef,
copyAddress,
copyOutline,
ethTokens,
polyTokens,
ethNfts,
poaps,
hasMore,
polyNfts,
loadMore,
toastState,
getUrl
}
}
});
</script>

View File

@ -5,29 +5,87 @@
<ion-title>History</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item v-if="history.length === 0">
You don't have any transaction history
</ion-item>
<ion-item v-else>
<ion-list>
<ion-item style="margin-bottom: 0.3rem;margin-top: 0.3rem" v-for="item of history" :key="item.txHash">
<ion-list>
<ion-item><b style="margin-right: 0.5rem">Date:</b> {{ new Date(item.date).toDateString() }}</ion-item>
<ion-item button @click="copyAddress(item.txHash, getToastRef())">
<p style="font-size: 0.7rem"><b style="margin-right: 0.5rem"><ion-icon style="margin-right: 0.3rem; display: inline-block;" :icon="copyOutline"></ion-icon>TxHash:</b>{{ item.txHash }}</p>
</ion-item>
<ion-item v-if="item.chainId"><b style="margin-right: 0.5rem">ChainId:</b> {{ item.chainId }}</ion-item>
<ion-item v-if="item.webiste"><b style="margin-right: 0.5rem">Website:</b> {{ item.webiste }}</ion-item>
<ion-item v-if="item.txUrl"><b style="margin-right: 0.5rem">ViewTx:</b> <a :href="item.txUrl">LINK</a></ion-item>
</ion-list>
</ion-item>
<ion-item>
<ion-button style="margin-left: 0.5rem" color="warning" @click="onWipeHistory">WIPE HISTORY</ion-button>
</ion-item>
</ion-list>
</ion-item>
<ion-content class="ion-padding">Not implemented</ion-content>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
<ion-toast
:is-open="toastState"
@didDismiss="toastState = false"
message="Copied to clipboard"
:duration="1500"
></ion-toast>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, Ref, ref } from "vue";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter } from "@ionic/vue";
import { getHistory } from '@/utils/platform'
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, onIonViewWillEnter, IonItem, IonList, IonToast, IonLoading, IonButton, IonIcon } from "@ionic/vue";
import { getHistory, copyAddress, wipeHistory } from '@/utils/platform'
import type { HistoryItem } from '@/extension/types'
import { copyOutline } from "ionicons/icons";
export default defineComponent({
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar },
components: { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonItem, IonList, IonToast, IonLoading, IonButton, IonIcon },
setup: () => {
const history = ref([]) as Ref<HistoryItem[]>;
const loading = ref(true)
onIonViewWillEnter(async () => {
const loading = ref(false)
const toastState = ref(false);
const getToastRef = () => toastState;
const loadData = async () => {
loading.value = true
history.value = await getHistory()
loading.value = false
}
const onWipeHistory = async () => {
await wipeHistory()
loadData();
}
onIonViewWillEnter(async () => {
loadData()
})
return {
history,
loading
loading,
copyAddress,
getToastRef,
toastState,
copyOutline,
onWipeHistory
}
}
});

View File

@ -6,7 +6,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-accordion-group v-if="!loading">
<ion-accordion-group :value="defaultAccordionOpen" v-if="!loading">
<ion-accordion value="1">
<ion-item slot="header" color="light">
<ion-label>Security</ion-label>
@ -28,7 +28,7 @@
<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">
@ -41,7 +41,7 @@
<ion-list>
<ion-item>
<ion-label>Permanent Lock</ion-label>
<ion-toggle :key="updateKey" slot="end" :disabled="!settings.s.enableStorageEnctyption" :checked="settings.s.encryptAfterEveryTx"></ion-toggle>
<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>
@ -54,11 +54,12 @@
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<ion-radio-group :value="settings.s.theme">
<ion-radio-group :value="radioTheme">
<ion-item>
<ion-radio
slot="start"
value="system"
@click="changeTheme('system')"
/>
<ion-label>System Default</ion-label>
</ion-item>
@ -66,6 +67,7 @@
<ion-radio
slot="start"
value="dark"
@click="changeTheme('dark')"
/>
<ion-label>Dark</ion-label>
</ion-item>
@ -73,6 +75,7 @@
<ion-radio
slot="start"
value="light"
@click="changeTheme('light')"
/>
<ion-label>Light</ion-label>
</ion-item>
@ -85,7 +88,16 @@
<ion-label>About</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
About text
<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">
@ -146,7 +158,7 @@
<ion-content class="ion-padding">
<ion-list v-if="settings.s.enableStorageEnctyption">
<ion-item>
<ion-label>Old Passord</ion-label>
<ion-label>Old Password</ion-label>
</ion-item> <ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
@ -185,7 +197,7 @@
<script lang="ts">
import { defineComponent, ref, reactive, Ref } from "vue";
import { storageWipe, getSettings, setSettings, getAccounts, saveSelectedAccount, replaceAccounts } from "@/utils/platform";
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'
@ -252,6 +264,8 @@ export default defineComponent({
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;
@ -271,12 +285,30 @@ export default defineComponent({
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.lockOutEnabled = !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 () => {
@ -352,7 +384,6 @@ export default defineComponent({
}
}
// settings.s.enableStorageEnctyption = true;
loading.value = false
}
@ -488,6 +519,7 @@ export default defineComponent({
await Promise.all([getSettings().then((storeSettings) =>
{
settings.s = storeSettings
radioTheme.value = settings.s.theme
}),
getAccounts().then((accounts) => {
if(accounts.length) {
@ -539,7 +571,12 @@ export default defineComponent({
modalGetPassword,
noAccounts,
alertHeader,
changeAutoLock
changeAutoLock,
defaultAccordionOpen,
changeTheme,
openTab,
radioTheme,
changePermaLock
};
},
});

View File

@ -57,7 +57,7 @@ import {
import { hexTostr } from "@/utils/platform";
import { approve, walletPing } from "@/extension/userRequest";
import { useRoute } from "vue-router";
import { getSelectedAccount, unBlockLockout, blockLockout } from "@/utils/platform";
import { getSelectedAccount, unBlockLockout, blockLockout, clearPk, getSettings } from "@/utils/platform";
import UnlockModal from "@/views/UnlockModal.vue";
export default defineComponent({
@ -83,6 +83,7 @@ export default defineComponent({
const alertMsg = ref("");
const timerReject = ref(140);
let interval: any;
let pSettings = getSettings()
const onCancel = () => {
window.close();
@ -129,6 +130,13 @@ export default defineComponent({
const modalResult = await openModal();
if (modalResult) {
unBlockLockout();
if(!pSettings) {
pSettings = getSettings()
}
const settings = await pSettings
if(settings.encryptAfterEveryTx) {
clearPk()
}
approve(rid);
} else {
onCancel();

View File

@ -157,7 +157,7 @@ import {
import { ethers } from "ethers";
import { approve, walletPing, walletSendData } from "@/extension/userRequest";
import { useRoute } from "vue-router";
import { getSelectedNetwork, getUrl, getPrices, numToHexStr, blockLockout, unBlockLockout, getSelectedAccount, strToHex} from '@/utils/platform'
import { getSelectedNetwork, getUrl, getPrices, numToHexStr, blockLockout, unBlockLockout, getSelectedAccount, strToHex, getSettings, clearPk } from '@/utils/platform'
import { getBalance, getGasPrice, estimateGas } from '@/utils/wallet'
import type { Network } from '@/extension/types'
import { mainNets } from "@/utils/networks";
@ -209,6 +209,7 @@ export default defineComponent({
const gasPriceModal = ref(false)
const inGasPrice = ref(0)
const inGasLimit = ref(0)
let pSettings = getSettings()
let interval = 0
const bars = ref(0)
@ -227,7 +228,7 @@ export default defineComponent({
const modal = await modalController.create({
component: UnlockModal,
componentProps: {
unlockType: 'message'
unlockType: 'transaction'
}
});
@ -240,10 +241,18 @@ export default defineComponent({
const onSign = async () => {
loading.value = true;
const selectedAccount = await getSelectedAccount()
loading.value = false
if ((selectedAccount.pk ?? '').length !== 66) {
const modalResult = await openModal()
if(modalResult) {
unBlockLockout()
if(!pSettings) {
pSettings = getSettings()
}
const settings = await pSettings
if(settings.encryptAfterEveryTx) {
clearPk()
}
approve(rid)
}else {
onCancel()
@ -287,9 +296,9 @@ export default defineComponent({
const pBalance = getBalance()
const pGetPrices = getPrices()
selectedNetwork.value = await getSelectedNetwork()
userBalance.value = Number(ethers.utils.formatEther((await pBalance).toString()))
userBalance.value = Number(ethers.utils.formatEther((await pBalance).toString() ?? '0x0'))
gasPrice.value = parseInt(ethers.utils.formatUnits((await pGasPrice).toString(), "gwei"), 10)
gasPrice.value = parseInt(ethers.utils.formatUnits((await pGasPrice).toString() ?? '0x0', "gwei"), 10)
try {
gasLimit.value = parseInt((await pEstimateGas).toString(), 10)
} catch (err) {

View File

@ -194,10 +194,10 @@ export default defineComponent({
const onSwitchTemplates = async () => {
loading.value = true;
selectedNetwork.value = templateNetworks[Number(networkId.value)]
saveNetwork(templateNetworks[Number(networkId.value)])
saveSelectedNetwork(templateNetworks[Number(networkId.value)])
await saveNetwork(templateNetworks[Number(networkId.value)])
await saveSelectedNetwork(templateNetworks[Number(networkId.value)])
approve(rid);
loading.value = true;
loading.value = false;
};
const onSwitchNotExisting = async () => {

View File

@ -8,20 +8,29 @@
<ion-title>Unlock to Proceed</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-list v-if="unlockType === 'message'">
<ion-item>To continue signing the message, unlock wallet.</ion-item>
<ion-item>Closing will reject sigining the message</ion-item>
<ion-item>Closing will reject sigining the message.</ion-item>
</ion-list>
<ion-list v-else-if="unlockType === 'viewPk'">
<ion-item>To view the PK, unlock wallet.</ion-item>
<ion-item>Closing will not show the PK.</ion-item>
</ion-list>
<ion-list v-else-if="unlockType === 'addAccount'">
<ion-item>Storage Encrypted, Unlock to add account.</ion-item>
<ion-item>Closing will not add the account.</ion-item>
</ion-list>
<ion-list v-else>
<ion-item>To continue sending the transaction, unlock wallet.</ion-item>
<ion-item>Closing will reject sending the tranzaction.</ion-item>
<ion-item>Closing will reject sending the transaction.</ion-item>
</ion-list>
<ion-item>
<ion-label>Unlock Password</ion-label>
</ion-item> <ion-item>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
@ -100,7 +109,7 @@ export default defineComponent({
const alertMsg = ref('');
const close = () => {
return modalController.dismiss(null, 'cancel');
return modalController?.dismiss(null, 'cancel');
}
const unlock = async () => {
@ -109,14 +118,16 @@ export default defineComponent({
let accounts = await getAccounts()
const cryptoParams = await getCryptoParams(mpPass.value)
const accProm = accounts.map(async a => {
a.pk = await decrypt(a.encPk, cryptoParams)
if(a.encPk) {
a.pk = await decrypt(a.encPk, cryptoParams)
}
return a
})
accounts = await Promise.all(accProm)
await replaceAccounts(accounts)
await saveSelectedAccount(accounts[0])
loading.value = false
return modalController.dismiss(null, 'confirm');
return modalController?.dismiss(mpPass.value, 'confirm');
} catch {
loading.value = false
alertMsg.value = 'Decryption failed, password is not correct, try again.';