This commit is contained in:
Danil Kovtonyuk 2022-04-22 13:05:56 +10:00
commit 44f31f8b9f
No known key found for this signature in database
GPG key ID: E72A919BF08C3746
402 changed files with 47865 additions and 0 deletions

17
modules/account/Page.vue Normal file
View file

@ -0,0 +1,17 @@
<template>
<div class="account">
<Settings />
<NoteAccount />
</div>
</template>
<script>
import { Settings, NoteAccount } from './components'
export default {
components: {
Settings,
NoteAccount
}
}
</script>

View file

@ -0,0 +1,94 @@
<template>
<div class="action">
<Statistic />
<div v-for="action in actions" :key="action.description" class="action-item">
<b-icon :icon="action.icon" size="is-large" />
<div class="desc">
{{ $t(action.description) }}
</div>
<b-button type="is-primary" outlined @mousedown.prevent @click="action.onClick">
{{ $t(action.button) }}
</b-button>
</div>
<div class="action-item has-switch">
<b-icon icon="account-file" size="is-large" />
<div class="desc">
{{ $t('account.control.fileDesc') }}
</div>
<b-switch :value="isEnabledSaveFile" size="is-medium" @input="handleEnabledSaveFile" />
</div>
</div>
</template>
<script>
import { openDecryptModal, openShowRecoverKeyModal, openRemoveAccountModal } from '../../modals'
import { controlComputed, controlMethods } from '../../injectors'
import Statistic from './Statistic'
export default {
components: {
Statistic
},
data() {
return {
actions: [
{
icon: 'account-notes',
onClick: this.getEncryptedNotes,
button: 'account.control.loadAll',
description: 'account.control.loadAllDesc'
},
{
icon: 'account-key',
onClick: this.openRecoverKeyModal,
button: 'account.control.showRecoveryKey',
description: 'account.control.showRecoveryKeyDesc'
},
{
icon: 'account-remove',
button: 'account.control.remove',
onClick: this.handleRemoveAccount,
description: 'account.control.removeDesc'
}
]
}
},
computed: {
...controlComputed
},
methods: {
...controlMethods,
handleEnabledSaveFile() {
this.enabledSaveFile()
},
async getEncryptedNotes() {
const props = await this.decryptNotes()
if (props) {
openDecryptModal({ ...props, parent: this })
}
},
handleRemoveAccount() {
const onConfirm = () => {
this.addNoticeWithInterval({
notice: {
title: 'accountHasBeenDeleted',
type: 'info'
},
interval: 2000
})
this.removeAccount()
}
openRemoveAccountModal({ i18n: this.$i18n, onConfirm })
},
async openRecoverKeyModal() {
const recoveryKey = await this.getRecoveryKey()
if (recoveryKey) {
openShowRecoverKeyModal({ recoveryKey, parent: this })
}
}
}
}
</script>

View file

@ -0,0 +1,23 @@
<template>
<div class="account-box">
<Header />
<Actions />
</div>
</template>
<script>
import Header from './Header'
import Actions from './Actions'
export default {
components: {
Header,
Actions
},
data() {
return {}
},
computed: {},
methods: {}
}
</script>

View file

@ -0,0 +1,27 @@
<template>
<div class="address">
<div class="address-item">
<div class="label">{{ $t('account.account') }}</div>
<div class="value">{{ accounts.encrypt }}</div>
</div>
<div class="address-item">
<div class="label">{{ $t('account.backedUpWith') }}</div>
<div class="value is-small">{{ accounts.backup }}</div>
</div>
</div>
</template>
<script>
import { headerComputed } from '../../injectors'
export default {
components: {},
data() {
return {}
},
computed: {
...headerComputed
},
methods: {}
}
</script>

View file

@ -0,0 +1,48 @@
<template>
<div class="action-item">
<b-icon icon="account-balance" size="is-large" />
<i18n path="account.control.balance" tag="div" class="desc">
<template v-if="hasBalances" v-slot:value>
<p class="balance">
<span v-for="(item, index) in getBalance" :key="item.currency" class="balance-item"
><NumberFormat :value="item.amount" /> {{ getSymbol(item.currency)
}}{{ index !== getBalance.length - 1 ? ',' : '' }}</span
>
</p>
</template>
</i18n>
</div>
</template>
<script>
import { statisticComputed } from '../../injectors'
import { NumberFormat } from '../../dependencies'
export default {
components: {
NumberFormat
},
computed: {
...statisticComputed,
getBalance() {
const balances = this.statistic.reduce((acc, { currency, amount }) => {
if (acc[currency]) {
acc[currency] += Number(amount)
} else {
acc[currency] = Number(amount)
}
return acc
}, {})
return Object.keys(balances).map((k) => {
return {
currency: k,
amount: balances[k]
}
})
},
hasBalances() {
return this.getBalance && this.getBalance.length
}
}
}
</script>

View file

@ -0,0 +1 @@
export { default as Control } from './Control'

View file

@ -0,0 +1,52 @@
<template>
<b-tooltip position="is-bottom" type="is-dark-tooltip" :triggers="[]">
<template v-slot:content>
<template v-if="isSetupAccount">
<p>{{ $t('accountConnected') }}</p>
<a @click="onCopy">{{ shortAddress(accounts.encrypt) }}</a>
<p><NumberFormat :value="noteAccountBalance" /> {{ currency }}</p>
</template>
<template v-else>
<p>{{ $t('notConnected') }}</p>
<b-button type="is-primary-link mb-0" @click="redirectToAccount">{{ $t('connectAccount') }}</b-button>
</template>
</template>
<b-button type="is-nav-icon" icon-left="wallet" :class="{ tornado: isSetupAccount }"></b-button>
</b-tooltip>
</template>
<script>
import { indicatorComputed, indicatorMethods } from '../../injectors'
import { NumberFormat } from '../../dependencies'
import { sliceAddress } from '@/utils'
export default {
components: {
NumberFormat
},
props: {
active: {
type: Boolean
}
},
computed: {
...indicatorComputed
},
methods: {
...indicatorMethods,
shortAddress(address) {
return sliceAddress(address)
},
async onCopy() {
await this.$copyText(this.accounts.encrypt)
this.$store.dispatch('notice/addNoticeWithInterval', {
notice: {
title: 'copied',
type: 'info'
},
interval: 2000
})
}
}
}
</script>

View file

@ -0,0 +1 @@
export { default as Indicator } from './Indicator'

View file

@ -0,0 +1,67 @@
<template>
<div ref="note" class="note-account" :class="{ 'is-active': isActive }">
<h2 class="title">
<!-- <b-icon icon="astronaut" size="is-large" /> -->
{{ $t('account.title') }}
</h2>
<b-notification class="main-notification" type="is-info">
{{ $t('account.description') }}
</b-notification>
<Setup v-if="!isSetupAccount" />
<Control v-else />
</div>
</template>
<script>
import { noteComputed, noteMethods } from '../../injectors'
import { Setup } from '../Setup'
import { Control } from '../Control'
export default {
components: {
Setup,
Control
},
data() {
return {
isActive: false
}
},
computed: {
...noteComputed
},
watch: {
isInitialized(isInitialized) {
if (isInitialized) {
this.checkExistAccount()
}
},
isHighlightedNoteAccount: {
handler(value) {
if (value) {
this.scrollOnHiglight()
}
},
immediate: true
}
},
created() {
this.checkExistAccount()
},
methods: {
...noteMethods,
scrollOnHiglight() {
setTimeout(() => {
this.isActive = true
this.$refs.note.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' })
}, 100)
setTimeout(() => {
this.isActive = false
this.highlightNoteAccount({ isHighlighted: false })
}, 1000)
}
}
}
</script>

View file

@ -0,0 +1 @@
export { default as NoteAccount } from './NoteAccount'

View file

@ -0,0 +1,46 @@
<template>
<div class="action">
<div class="action-item">
<b-icon icon="account-wallet" size="is-large" />
<div class="desc">
{{ isLoggedIn ? $t('account.wallet.disconnect') : $t('account.wallet.desc') }}
</div>
<b-button v-if="isLoggedIn" type="is-primary" outlined @mousedown.prevent @click="onLogOut">
{{ $t('account.wallet.logout') }}
</b-button>
<connect-button v-else outlined action-text="account.wallet.connectWeb3" />
</div>
<div class="action-item">
<b-icon icon="account-rpc" size="is-large" />
<div class="desc">
{{ $t('account.wallet.rpcDesc') }}
</div>
<b-button type="is-primary" outlined @click="onSettings">{{ $t('account.wallet.changeRpc') }}</b-button>
</div>
</div>
</template>
<script>
import { walletComputed, walletActions } from '../../injectors'
import { openSettingsModal } from '../../modals'
import { ConnectButton } from '@/components/web3Connect'
export default {
components: {
ConnectButton
},
computed: {
...walletComputed
},
methods: {
...walletActions,
onSettings() {
openSettingsModal({
parent: this,
netId: this.netId
})
}
}
}
</script>

View file

@ -0,0 +1,18 @@
<template>
<div class="address">
<div class="address-item">
<div class="label">{{ $t('account.wallet.label') }}</div>
<div class="value">{{ ethAccount || '-' }}</div>
</div>
</div>
</template>
<script>
import { walletComputed } from '../../injectors'
export default {
computed: {
...walletComputed
}
}
</script>

View file

@ -0,0 +1,23 @@
<template>
<div class="wallet-account">
<h2 class="title">
{{ $t('wallet') }}
</h2>
<div class="account-box">
<Header />
<Actions />
</div>
</div>
</template>
<script>
import Header from './Header'
import Actions from './Actions'
export default {
components: {
Header,
Actions
}
}
</script>

View file

@ -0,0 +1 @@
export { default as Settings } from './Settings'

View file

@ -0,0 +1,94 @@
<template>
<div class="action">
<div class="action-item">
<b-icon icon="account-setup" size="is-large" />
<div class="desc">
{{ $t('account.setup.desc') }}
</div>
<b-tooltip :active="isAccountDisabled" :label="$t(setupAccountTooltip)" multilined size="is-large">
<b-button :disabled="isAccountDisabled" outlined type="is-primary" @click="showSetupModal">{{
$t('account.setup.account')
}}</b-button>
</b-tooltip>
</div>
<div class="action-item">
<b-icon icon="account-recover" size="is-large" />
<div class="desc">
{{ $t('account.setup.recoverDesc') }}
</div>
<b-tooltip :active="isRecoverDisabled" :label="$t(recoverAccountTooltip)" multilined size="is-large">
<b-button type="is-primary" outlined :disabled="isRecoverDisabled" @click="handleRecoverAccount">
{{ $t('account.setup.recover') }}
</b-button>
</b-tooltip>
</div>
<div class="action-item">
<b-icon icon="account-raw" size="is-large" />
<div class="desc">
{{ $t('account.setup.enterRawDesc') }}
</div>
<b-button type="is-primary" outlined @click="showRecoverKeyModal">{{
$t('account.setup.enterRaw')
}}</b-button>
</div>
</div>
</template>
<script>
import { setupComputed, setupMethods } from '../../injectors'
import { openDecryptModal, openRecoverAccountModal, openSetupAccountModal } from '../../modals'
export default {
computed: {
...setupComputed,
isAccountDisabled() {
return this.isExistAccount || !this.isLoggedIn
},
isRecoverDisabled() {
return !this.isExistAccount || !this.isLoggedIn || this.isPartialSupport
},
recoverAccountTooltip() {
if (this.isPartialSupport) {
return 'mobileWallet.actions.disabled'
}
return this.isLoggedIn ? 'account.setup.recTooltip' : 'connectYourWalletFirst'
},
setupAccountTooltip() {
return this.isLoggedIn ? 'account.setup.setTooltip' : 'connectYourWalletFirst'
}
},
methods: {
...setupMethods,
async getEncryptedNotes() {
const props = await this.decryptNotes()
if (props) {
openDecryptModal({ ...props, parent: this })
}
},
showRecoverKeyModal() {
openRecoverAccountModal({ parent: this, getNotes: this.getEncryptedNotes })
},
showSetupModal() {
openSetupAccountModal({ parent: this })
},
async handleRecoverAccount() {
try {
this.enable({ message: this.$t('account.setup.decrypt') })
await this.recoverAccountFromChain()
await this.getEncryptedNotes()
} catch {
this.addNoticeWithInterval({
notice: {
title: 'decryptFailed',
type: 'danger'
},
interval: 5000
})
} finally {
this.disable()
}
}
}
}
</script>

View file

@ -0,0 +1,24 @@
<template>
<div class="address">
<div class="address-item">
<div class="label">{{ $t('account.account') }}</div>
<div class="value">{{ accounts.backup }}</div>
</div>
<div class="address-item">
<div class="label">{{ $t('account.backedUpWith') }}</div>
<div class="value">{{ accounts.encrypt }}</div>
</div>
</div>
</template>
<script>
import { headerComputed } from '../../injectors'
export default {
computed: {
...headerComputed
},
watch: {},
methods: {}
}
</script>

View file

@ -0,0 +1,18 @@
<template>
<div class="account-box">
<Header />
<Actions />
</div>
</template>
<script>
import Header from './Header'
import Actions from './Actions'
export default {
components: {
Header,
Actions
}
}
</script>

View file

@ -0,0 +1 @@
export { default as Setup } from './Setup'

View file

@ -0,0 +1,5 @@
export { Setup } from './Setup'
export { Control } from './Control'
export { Settings } from './Settings'
export { Indicator } from './Indicator'
export { NoteAccount } from './NoteAccount'

View file

@ -0,0 +1,4 @@
import Settings from '@/components/Settings'
import NumberFormat from '@/components/NumberFormat'
export { Settings, NumberFormat }

2
modules/account/index.js Normal file
View file

@ -0,0 +1,2 @@
export * from './shared'
export { default as AccountPage } from './Page'

View file

@ -0,0 +1,10 @@
// components
export * from './noteInjectors'
export * from './setupInjectors'
export * from './walletInjectors'
export * from './сontrolInjectors'
export * from './indicatorInjectors'
// modals
export * from './setupAccountInjectors'
export * from './recoverAccountInjectors'
export * from './showRecoveryKeyInjectors'

View file

@ -0,0 +1,10 @@
import { mapGetters, mapActions } from 'vuex'
export const indicatorMethods = {
...mapActions('encryptedNote', ['highlightNoteAccount', 'redirectToAccount'])
}
export const indicatorComputed = {
...mapGetters('metamask', ['currency']),
...mapGetters('encryptedNote', ['accounts', 'isSetupAccount', 'noteAccountBalance'])
}

View file

@ -0,0 +1,11 @@
import { mapActions, mapGetters, mapState } from 'vuex'
export const noteMethods = {
...mapActions('encryptedNote', ['checkExistAccount', 'highlightNoteAccount'])
}
export const noteComputed = {
...mapGetters('encryptedNote', ['isSetupAccount']),
...mapState('metamask', ['isInitialized', 'netId']),
...mapGetters('encryptedNote', ['isHighlightedNoteAccount'])
}

View file

@ -0,0 +1,7 @@
import { mapActions, mapGetters } from 'vuex'
export const recoverAccountMethods = mapActions('encryptedNote', ['clearState', 'recoverAccountFromKey'])
export const recoverAccountComputed = {
...mapGetters('encryptedNote', ['recoverAccountFromKeyRequest'])
}

View file

@ -0,0 +1,10 @@
import { mapActions, mapGetters } from 'vuex'
export const setupAccountMethods = {
...mapActions('notice', ['addNoticeWithInterval']),
...mapActions('encryptedNote', ['clearState', 'setupAccount'])
}
export const setupAccountComputed = {
...mapGetters('encryptedNote', ['setupAccountRequest'])
}

View file

@ -0,0 +1,20 @@
import { mapActions, mapGetters, mapState } from 'vuex'
export const setupMethods = {
...mapActions('loading', ['enable', 'disable']),
...mapActions('notice', ['addNoticeWithInterval']),
...mapActions('encryptedNote', [
'clearState',
'decryptNotes',
'setupAccount',
'recoverAccountFromKey',
'saveRecoveryKeyOnFile',
'recoverAccountFromChain'
])
}
export const setupComputed = {
...mapState('metamask', ['isInitialized', 'providerName']),
...mapGetters('metamask', ['isLoggedIn', 'isPartialSupport']),
...mapGetters('encryptedNote', ['isExistAccount', 'setupAccountRequest'])
}

View file

@ -0,0 +1,5 @@
import { mapActions } from 'vuex'
export const showRecoveryKeyMethods = {
...mapActions('notice', ['addNoticeWithInterval'])
}

View file

@ -0,0 +1,10 @@
import { mapGetters, mapState, mapActions } from 'vuex'
export const walletComputed = {
...mapState('metamask', ['ethAccount']),
...mapGetters('metamask', ['netId', 'isLoggedIn'])
}
export const walletActions = {
...mapActions('metamask', ['onLogOut'])
}

View file

@ -0,0 +1,19 @@
import { mapActions, mapGetters } from 'vuex'
export const controlMethods = {
...mapActions('notice', ['addNoticeWithInterval']),
...mapActions('encryptedNote', ['decryptNotes', 'removeAccount', 'enabledSaveFile', 'getRecoveryKey'])
}
export const controlComputed = {
...mapGetters('encryptedNote', ['isEnabledSaveFile', 'isSetupAccount'])
}
export const statisticComputed = {
...mapGetters('encryptedNote', ['statistic']),
...mapGetters('token', ['getSymbol'])
}
export const headerComputed = {
...mapGetters('encryptedNote', ['accounts'])
}

View file

@ -0,0 +1,84 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $parent.$t('account.modals.decryptInfo.title') }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<div class="note">{{ $parent.$t('account.modals.decryptInfo.description') }}</div>
<div class="account-decrypt-info">
<div class="item">
{{ $parent.$t('account.modals.decryptInfo.spent') }}
<span class="has-text-weight-bold mr-3">{{ spent }}</span>
</div>
<div class="item">
{{ $parent.$t('account.modals.decryptInfo.unSpent') }}
<span class="has-text-weight-bold mr-3">{{ unSpent }}</span>
</div>
<template v-for="(instances, currency) in getStatistic">
<div v-for="(amount, instance) in instances" :key="`${amount}_${currency}_${instance}`" class="item">
{{ instance }} {{ getSymbol(currency) }}:
<span class="has-text-weight-bold mr-3">{{ amount }}</span>
</div>
</template>
</div>
<div class="buttons buttons__halfwidth mt-3">
<b-button type="is-primary" outlined @click="onClose">
{{ $parent.$t('account.modals.decryptInfo.close') }}
</b-button>
<b-button type="is-primary" @click="handleRedirect">
{{ $parent.$t('account.modals.decryptInfo.redirect') }}
</b-button>
</div>
</div>
</template>
<script>
import { statisticComputed } from '../injectors'
export default {
props: {
all: {
type: Number,
required: true
},
spent: {
type: Number,
required: true
},
unSpent: {
type: Number,
required: true
}
},
data() {
return {}
},
computed: {
...statisticComputed,
getStatistic() {
const balance = this.statistic.reduce((acc, { currency, amount }) => {
if (acc[currency] && acc[currency][amount]) {
acc[currency][amount] += 1
} else {
acc[currency] = {
...acc[currency],
[amount]: 1
}
}
return acc
}, {})
return balance
}
},
methods: {
onClose() {
this.$emit('close')
},
handleRedirect() {
this.$router.push('/')
this.$emit('close')
}
}
}
</script>

View file

@ -0,0 +1,90 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $t('account.modals.recoverAccount.title') }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<div class="note">
{{ $t('account.modals.recoverAccount.description') }}
</div>
<div class="field">
<b-input
v-model="recoveryKey"
type="textarea"
class="is-disabled-resize"
rows="2"
:placeholder="$t('enterRecoveryKey')"
:class="{ 'is-warning': hasAndValidKey }"
@input="onInput"
></b-input>
<p v-show="hasAndValidKey" class="help is-warning">
{{ $t('account.modals.recoverAccount.warning') }}
</p>
</div>
<b-notification
v-if="recoverAccountFromKeyRequest.isError"
class="main-notification"
type="is-warning"
:closable="false"
>
{{ recoverAccountFromKeyRequest.errorMessage }}
</b-notification>
<b-button
type="is-primary is-fullwidth"
:disabled="hasAndValidKey"
:loading="recoverAccountFromKeyRequest.isFetching"
@click="handleRecoverAccount"
>
{{ $t('account.modals.recoverAccount.connect') }}
</b-button>
</div>
</template>
<script>
import { recoverAccountComputed, recoverAccountMethods } from '../injectors'
import { debounce } from '@/utils'
export default {
props: {
getNotes: {
required: true,
type: Function
}
},
data() {
return {
recoveryKey: '',
isValidRecoveryKey: true
}
},
computed: {
...recoverAccountComputed,
hasAndValidKey() {
return this.recoveryKey && !this.isValidRecoveryKey
}
},
beforeDestroy() {
this.clearState({ key: 'recoverAccountFromKey' })
},
methods: {
...recoverAccountMethods,
async handleRecoverAccount() {
await this.recoverAccountFromKey({ recoveryKey: this.recoveryKey })
this.$emit('close')
await this.getNotes()
},
onInput(recoveryKey) {
this.clearState({ key: 'recoverAccountFromKey' })
debounce(this.checkPrivateKey, recoveryKey)
},
checkPrivateKey(recoveryKey) {
try {
this.$provider.web3.eth.accounts.privateKeyToAccount(recoveryKey)
this.isValidRecoveryKey = true
} catch {
this.isValidRecoveryKey = false
}
}
}
}
</script>

View file

@ -0,0 +1,71 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<div class="modal-card-title">{{ $parent.$t('account.modals.checkRecoveryKey.title') }}</div>
</header>
<section class="modal-card-body">
<div class="media">
<div class="media-content">
{{
isShow
? $parent.$t('account.modals.checkRecoveryKey.inactiveDescription')
: $parent.$t('account.modals.checkRecoveryKey.description')
}}
</div>
</div>
</section>
<footer v-if="isShow" class="modal-card-foot">
<b-button type="is-primary" outlined @click="$emit('close')">
{{ $parent.$t('close') }}
</b-button>
</footer>
<footer v-else class="modal-card-foot">
<b-button type="is-primary" outlined @click="_onCancel">
{{ $parent.$t('account.modals.checkRecoveryKey.no') }}
</b-button>
<b-button type="is-primary" @click="_onConfirm">
{{ $parent.$t('account.modals.checkRecoveryKey.yes') }}
</b-button>
</footer>
</div>
</template>
<script>
export default {
props: {
onCancel: {
type: Function,
required: true
},
onConfirm: {
type: Function,
required: true
}
},
data() {
return {
timer: null,
isShow: false
}
},
beforeDestroy() {
clearTimeout(this.timer)
},
mounted() {
this.timer = setTimeout(() => {
this.onCancel()
this.isShow = true
}, 1000 * 60)
},
methods: {
_onCancel() {
this.onCancel()
this.$emit('close')
},
_onConfirm() {
this.onConfirm()
this.$emit('close')
}
}
}
</script>

View file

@ -0,0 +1,134 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $t('account.modals.setupAccount.title') }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<div class="note">{{ $t('account.modals.setupAccount.description') }}</div>
<div class="field">
<div class="label-with-buttons">
<div class="label">{{ $t('account.modals.setupAccount.label') }}</div>
<b-button v-clipboard:copy="recoveryKey" v-clipboard:success="onCopy" type="is-primary-text">
{{ $t('copy') }}
</b-button>
</div>
<div class="notice is-recovery-key">
<div class="notice__p">{{ recoveryKey }}</div>
</div>
</div>
<b-notification class="main-notification" type="is-info" :closable="false">
{{ $t('account.modals.setupAccount.isNotSupportedWithHw') }}
</b-notification>
<b-checkbox v-model="isSaveOnChain" :disabled="isPartialSupport">{{
$t('account.modals.setupAccount.saveOnChain')
}}</b-checkbox>
<b-checkbox v-if="!isSaveOnChain" v-model="isBackuped">{{
$t('account.modals.setupAccount.backedUp')
}}</b-checkbox>
<b-notification
v-if="!isSaveOnChain && warningMessage"
class="main-notification"
type="is-warning"
:closable="false"
>
{{ warningMessage }}
</b-notification>
<b-notification
v-if="setupAccountRequest.isError"
class="main-notification"
type="is-warning"
:closable="false"
>
{{ setupAccountRequest.errorMessage }}
</b-notification>
<b-button
v-if="!isBackuped && isSaveOnChain"
type="is-primary is-fullwidth"
:loading="setupAccountRequest.isFetching"
@click="onSetupAccount"
>
{{ $t('account.modals.setupAccount.setupAccount') }}
</b-button>
<b-button v-else type="is-primary is-fullwidth" :disabled="!isBackuped" @click="setAccount">
{{ $t('account.modals.setupAccount.setAccount') }}
</b-button>
</div>
</template>
<script>
import { setupMethods, setupComputed } from '../injectors'
export default {
data() {
return {
timer: null,
recoveryKey: '',
isBackuped: false,
isSaveOnChain: true,
warningMessage: ''
}
},
computed: {
...setupComputed
},
watch: {
isSaveOnChain() {
if (this.isSaveOnChain) {
this.isBackuped = false
}
}
},
beforeUpdate() {
if (this.setupAccountRequest.isSuccess) {
this.$parent.close()
}
},
mounted() {
this.recoveryKey = this.$provider.web3.eth.accounts.create().privateKey.slice(2)
if (this.isPartialSupport) {
this.isSaveOnChain = false
}
this.timer = setTimeout(() => {
this.saveRecoveryKeyOnFile({ recoveryKey: this.recoveryKey })
}, 1500)
},
beforeDestroy() {
clearTimeout(this.timer)
this.clearState({ key: 'setupAccount' })
},
methods: {
...setupMethods,
onCopy() {
this.addNoticeWithInterval({
notice: {
title: 'copied',
type: 'info'
},
interval: 2000
})
},
async setAccount() {
try {
await this.recoverAccountFromKey({ recoveryKey: this.recoveryKey })
this.$emit('close')
} catch (err) {
this.warningMessage = err.message
}
},
async onSetupAccount() {
if (!this.isSaveOnChain) {
this.warningMessage = this.$t('account.modals.setupAccount.yourRecoveryKeyWontBeSaved')
return
}
try {
await this.setupAccount({ privateKey: this.recoveryKey })
} catch (err) {
this.warningMessage = err.message
}
}
}
}
</script>

View file

@ -0,0 +1,55 @@
<template>
<div class="modal-card box box-modal">
<header class="box-modal-header is-spaced">
<div class="box-modal-title">{{ $t('account.modals.showRecoveryKey.title') }}</div>
<button type="button" class="delete" @click="$emit('close')" />
</header>
<div class="note">{{ $t('account.modals.showRecoveryKey.description') }}</div>
<div class="field">
<div class="label-with-buttons">
<div class="label"></div>
<b-button v-clipboard:copy="recoveryKey" v-clipboard:success="onCopy" type="is-primary-text">
{{ $t('copy') }}
</b-button>
</div>
<div class="notice is-recovery-key">
<div class="notice__p">{{ recoveryKey }}</div>
</div>
</div>
<b-button type="is-primary" outlined @click="onClose">
{{ $t('account.modals.showRecoveryKey.close') }}
</b-button>
</div>
</template>
<script>
import { showRecoveryKeyMethods } from '../injectors'
export default {
props: {
recoveryKey: {
type: String,
required: true
}
},
data() {
return {}
},
computed: {},
methods: {
...showRecoveryKeyMethods,
onClose() {
this.$emit('close')
},
onCopy() {
this.addNoticeWithInterval({
notice: {
title: 'copied',
type: 'info'
},
interval: 2000
})
}
}
}
</script>

View file

@ -0,0 +1,66 @@
import { ModalProgrammatic, DialogProgrammatic } from 'buefy'
import { Settings } from '../dependencies'
import DecryptInfo from './DecryptInfo.vue'
import SetupAccount from './SetupAccount.vue'
import SessionUpdate from './SessionUpdate.vue'
import RecoverAccount from './RecoverAccount.vue'
import ShowRecoverKey from './ShowRecoverKey.vue'
const openSettingsModal = ({ parent, ...props }) => {
createModal({ props, parent, component: Settings })
}
const openSetupAccountModal = ({ parent, ...props }) => {
createModal({ props, parent, component: SetupAccount, canCancel: false })
}
const openDecryptModal = ({ parent, ...props }) => {
createModal({ props, parent, component: DecryptInfo })
}
const openRecoverAccountModal = ({ parent, ...props }) => {
createModal({ props, parent, component: RecoverAccount })
}
const openShowRecoverKeyModal = ({ parent, ...props }) => {
createModal({ props, parent, component: ShowRecoverKey })
}
function createModal({ component, props, parent, ...rest }) {
ModalProgrammatic.open({
props,
parent,
component,
width: 440,
hasModalCard: true,
customClass: 'is-pinned',
...rest
})
}
const openRemoveAccountModal = ({ i18n, onConfirm }) => {
DialogProgrammatic.confirm({
onConfirm,
title: i18n.t('account.modals.removeAccount.title'),
type: 'is-primary is-outlined',
message: i18n.t('account.modals.removeAccount.description'),
cancelText: i18n.t('account.modals.removeAccount.cancel'),
confirmText: i18n.t('account.modals.removeAccount.remove')
})
}
const openConfirmModal = ({ parent, ...props }) => {
createModal({ props, parent, component: SessionUpdate, customClass: 'dialog' })
}
export {
openDecryptModal,
openConfirmModal,
openSettingsModal,
openSetupAccountModal,
openRemoveAccountModal,
openShowRecoverKeyModal,
openRecoverAccountModal
}

View file

@ -0,0 +1,2 @@
export { Indicator } from '../components'
export { openConfirmModal } from '../modals'

View file

@ -0,0 +1,77 @@
import { graph } from '@/services'
export async function checkExistAccount({ getters, dispatch, rootState, rootGetters }) {
const { ethAccount, netId } = rootState.metamask
if (!ethAccount) {
return
}
try {
const rpc = rootGetters['settings/currentRpc']
const web3 = this.$provider.getWeb3(rpc.url)
const currentBlockNumber = await web3.eth.getBlockNumber()
const isExist = await getEventsFromBlockPart(
{ getters, dispatch, rootState, rootGetters },
{ netId, currentBlockNumber, address: ethAccount }
)
console.log('isExist', isExist)
dispatch('createMutation', {
type: 'CHECK_ACCOUNT',
payload: { isExist }
})
} catch (err) {
throw new Error(`Method checkExistAccount has error: ${err.message}`)
}
}
async function getEventsFromBlockPart(
{ getters, rootGetters, dispatch },
{ address, currentBlockNumber, netId }
) {
try {
const { events: graphEvents, lastSyncBlock } = await graph.getNoteAccounts({ address, netId })
const blockDifference = Math.ceil(currentBlockNumber - lastSyncBlock)
let blockRange = 1
if (Number(netId) === 56) {
blockRange = 4950
}
let numberParts = blockDifference === 0 ? 1 : Math.ceil(blockDifference / blockRange)
const part = Math.ceil(blockDifference / numberParts)
let events = []
let fromBlock = lastSyncBlock
let toBlock = lastSyncBlock + part
if (toBlock >= currentBlockNumber) {
toBlock = 'latest'
numberParts = 1
}
for (let i = 0; i < numberParts; i++) {
const partOfEvents = await getters.echoContract.getEvents({
fromBlock,
toBlock,
address
})
if (partOfEvents) {
events = events.concat(partOfEvents)
}
fromBlock = toBlock
toBlock += part
}
events = graphEvents.concat(events)
return Boolean(Array.isArray(events) && Boolean(events.length))
} catch (err) {
console.log(`getEventsFromBlock has error: ${err.message}`)
return false
}
}

View file

@ -0,0 +1,8 @@
export function checkRecoveryKey({ getters, dispatch }) {
const { encrypt: address } = getters.accounts
const recoveryKey = this.$sessionStorage.getItem(address)
if (!recoveryKey && !getters.encryptedPrivateKey) {
dispatch('removeAccount')
}
}

View file

@ -0,0 +1,16 @@
import { decrypt } from 'eth-sig-util'
import { unpackEncryptedMessage } from '@/utils'
export async function decryptNote({ dispatch }, encryptedNote) {
try {
const recoveryKey = await dispatch('getRecoveryKey')
const unpackedMessage = unpackEncryptedMessage(encryptedNote)
const [, note] = decrypt(unpackedMessage, recoveryKey).split('-')
return note
} catch (err) {
console.warn(`Method decryptNote has error: ${err.message}`)
}
}

View file

@ -0,0 +1,6 @@
export function _checkCurrentTx({ rootGetters }, transactions) {
const currentTransactions = rootGetters['txHashKeeper/allTxsHash']
const newTransactions = transactions.filter((event) => !currentTransactions.includes(event.txHash))
return newTransactions
}

View file

@ -0,0 +1,207 @@
import { decrypt } from 'eth-sig-util'
import { isAddress } from 'web3-utils'
import { eventsType } from '@/constants'
import { parseHexNote, getInstanceByAddress, unpackEncryptedMessage } from '@/utils'
export async function _encryptFormatTx({ dispatch, getters, rootGetters }, { events, privateKey }) {
let result = []
const netId = rootGetters['metamask/netId']
const eventsInterface = rootGetters['application/eventsInterface']
dispatch('loading/changeText', { message: this.app.i18n.t('decryptingNotes') }, { root: true })
const encryptedEvents = decryptEvents({ events, privateKey })
dispatch(
'loading/changeText',
{ message: this.app.i18n.t('getAndValidateEvents', { name: this.app.i18n.t('deposit') }) },
{ root: true }
)
const instances = encryptedEvents.reduce((acc, curr) => {
const instance = getInstanceByAddress({ netId, address: curr.address })
if (!instance) {
return acc
}
const name = `${instance.amount}${instance.currency}`
if (!acc[name]) {
const service = eventsInterface.getService({ netId, ...instance })
acc[name] = { ...instance, service }
}
return acc
}, {})
await Promise.all(
[].concat(
Object.values(instances).map((instance) => instance.service.updateEvents(eventsType.DEPOSIT)),
Object.values(instances).map((instance) => instance.service.updateEvents(eventsType.WITHDRAWAL))
)
)
const eventBatches = getBatches(encryptedEvents)
for await (const batch of eventBatches) {
try {
const depositPromises = batch.map((event) => {
const instance = getInstanceByAddress({ netId, address: event.address })
if (!instance) {
return
}
const { service } = instances[`${instance.amount}${instance.currency}`]
return getDeposit({ event, netId, service, instance })
})
const proceedDeposits = await Promise.all(depositPromises)
console.log({ proceedDeposits })
dispatch(
'loading/changeText',
{ message: this.app.i18n.t('getAndValidateEvents', { name: this.app.i18n.t('withdrawal') }) },
{ root: true }
)
const proceedEvents = await Promise.all(
proceedDeposits.map(([event, deposit]) => proceedEvent({ event, getters, deposit, netId, dispatch }))
)
result = result.concat(proceedEvents)
} catch (e) {
console.error('_encryptFormatTx', e)
}
}
return formattingEvents(result)
}
function decryptEvents({ privateKey, events }) {
const encryptEvents = []
for (const event of events) {
try {
const unpackedMessage = unpackEncryptedMessage(event.encryptedNote)
const [address, note] = decrypt(unpackedMessage, privateKey).split('-')
encryptEvents.push({ address, note, ...event })
} catch {
// decryption may fail for foreign notes
continue
}
}
return encryptEvents
}
function formattingEvents(proceedEvents) {
const result = []
const statistic = []
let unSpent = 0
proceedEvents.forEach((transaction) => {
if (transaction) {
if (!transaction.isSpent) {
unSpent += 1
statistic.push({
amount: transaction.amount,
currency: transaction.currency
})
}
result.push(transaction)
}
})
return {
unSpent,
statistic,
transactions: result
}
}
async function getDeposit({ netId, event, service, instance }) {
const { commitmentHex, nullifierHex } = parseHexNote(event.note)
const foundEvent = await service.findEvent({
eventName: 'commitment',
eventToFind: commitmentHex,
type: eventsType.DEPOSIT
})
if (!foundEvent) {
return
}
const isSpent = await service.findEvent({
eventName: 'nullifierHash',
eventToFind: nullifierHex,
type: eventsType.WITHDRAWAL
})
const deposit = {
leafIndex: foundEvent.leafIndex,
timestamp: foundEvent.timestamp,
txHash: foundEvent.transactionHash,
depositBlock: foundEvent.blockNumber
}
return [
event,
{
nullifierHex,
commitmentHex,
amount: instance.amount,
isSpent: Boolean(isSpent),
currency: instance.currency,
prefix: `tornado-${instance.currency}-${instance.amount}-${netId}`,
...deposit
}
]
}
async function proceedEvent({ dispatch, getters, deposit, netId, event: { note, address, ...event } }) {
const { encrypt, backup } = getters.accounts
try {
const { depositBlock, ...rest } = deposit
const transaction = {
...rest,
netId,
status: 2,
type: 'Deposit',
txHash: event.txHash,
owner: isAddress(encrypt) ? encrypt : '',
backupAccount: isAddress(backup) ? backup : '',
index: deposit.leafIndex,
storeType: 'encryptedTxs',
blockNumber: event.blockNumber,
note: event.encryptedNote
}
if (deposit && deposit.isSpent) {
const withdrawEvent = await dispatch(
'application/loadWithdrawalEvent',
{ withdrawNote: `${deposit.prefix}-${note}` },
{ root: true }
)
if (withdrawEvent) {
transaction.txHash = withdrawEvent.txHash
transaction.depositBlock = depositBlock
transaction.blockNumber = withdrawEvent.blockNumber
}
}
return transaction
} catch (err) {
console.log('err', err.message)
}
}
function getBatches(arr, batchSize = 100) {
const batches = []
while (arr.length) {
batches.push(arr.splice(0, batchSize))
}
return batches
}

View file

@ -0,0 +1,36 @@
export async function decryptNotes({ commit, dispatch }) {
try {
dispatch('loading/enable', { message: this.app.i18n.t('startDecryptingNotes') }, { root: true })
const privateKey = await dispatch('getRecoveryKey', false)
if (!privateKey) {
return
}
const events = await dispatch('application/getEncryptedNotes', {}, { root: true })
const { transactions, statistic, unSpent } = await dispatch('_encryptFormatTx', { events, privateKey })
const checkedTxs = await dispatch('_checkCurrentTx', transactions)
checkedTxs.forEach((tx) => {
commit('txHashKeeper/SAVE_TX_HASH', tx, { root: true })
})
dispatch('createMutation', { type: 'SET_STATISTIC', payload: { statistic } })
return {
unSpent,
spent: checkedTxs.length ? checkedTxs.length - unSpent : 0,
all: events.length ? events.length - 1 : 0
}
} catch (err) {
dispatch('createMutation', {
type: 'SET_DOMAIN_FAILED',
payload: { key: 'decryptNotes', errorMessage: err.message }
})
} finally {
dispatch('loading/disable', {}, { root: true })
}
}

View file

@ -0,0 +1,4 @@
export { decryptNotes } from './getDecryptNotes'
// helpers
export { _checkCurrentTx } from './checkCurrentTx'
export { _encryptFormatTx } from './encryptFormatTx'

View file

@ -0,0 +1,6 @@
export function enabledSaveFile({ dispatch, getters }) {
dispatch('createMutation', {
type: 'ENABLED_SAVE_FILE',
payload: { isEnabled: !getters.isEnabledSaveFile }
})
}

View file

@ -0,0 +1,22 @@
import { encrypt, getEncryptionPublicKey } from 'eth-sig-util'
export function getEncryptedAccount(_, { privateKey, pubKey }) {
try {
const { address } = this.$provider.web3.eth.accounts.privateKeyToAccount(privateKey)
const keyWithOutPrefix = privateKey.slice(0, 2) === '0x' ? privateKey.replace('0x', '') : privateKey
const publicKey = getEncryptionPublicKey(keyWithOutPrefix)
const encryptedData = encrypt(pubKey, { data: keyWithOutPrefix }, 'x25519-xsalsa20-poly1305')
const hexPrivateKey = Buffer.from(JSON.stringify(encryptedData)).toString('hex')
return {
address,
publicKey,
hexPrivateKey,
encryptedData
}
} catch (err) {
throw new Error(`Method getEncryptedAccount has error: ${err.message}`)
}
}

View file

@ -0,0 +1,19 @@
import { encrypt } from 'eth-sig-util'
import { packEncryptedMessage } from '@/utils'
export function getEncryptedNote({ getters }, { data }) {
try {
const encryptedPublicKey = getters.encryptedPublicKey
if (!encryptedPublicKey) {
return
}
const encryptedData = encrypt(encryptedPublicKey, { data }, 'x25519-xsalsa20-poly1305')
return packEncryptedMessage(encryptedData)
} catch (err) {
throw new Error(`Method getEncryptedNote has error: ${err.message}`)
}
}

View file

@ -0,0 +1,71 @@
import { isAddress } from 'web3-utils'
import { sliceAddress } from '@/utils'
export async function getRecoveryKey({ dispatch, getters, rootState }, enableLoader = true) {
try {
const { encrypt: address } = getters.accounts
const recoverKey = this.$sessionStorage.getItem(address)
if (recoverKey) {
return recoverKey.data
}
const hasError = _checkBackupAccount({ rootState, dispatch, getters, i18n: this.app.i18n })
if (hasError) {
return
}
const encryptedPrivateKey = getters.encryptedPrivateKey
dispatch('loading/enable', { message: this.app.i18n.t('decryptNote') }, { root: true })
const privateKey = await dispatch('metamask/ethDecrypt', encryptedPrivateKey, { root: true })
this.$sessionStorage.setItem(address, privateKey)
return privateKey
} catch (err) {
const isRejected = err.message.includes('MetaMask Decryption: User denied message decryption.')
const notice = {
title: 'decryptFailed',
type: 'danger'
}
if (isRejected) {
notice.title = 'rejectedRequest'
notice.description = rootState.metamask.walletName
}
dispatch('notice/addNoticeWithInterval', { notice, interval: 5000 }, { root: true })
} finally {
if (enableLoader) {
dispatch('loading/disable', {}, { root: true })
}
}
}
function _checkBackupAccount(ctx) {
const { ethAccount } = ctx.rootState.metamask
if (!ethAccount) {
const { backup, encrypt } = ctx.getters.accounts
if (isAddress(backup)) {
ctx.dispatch(
'notice/addNoticeWithInterval',
{
notice: {
untranslatedTitle: ctx.i18n.t('noteAccountKey', {
address: sliceAddress(backup),
noteAddress: sliceAddress(encrypt)
}),
type: 'danger'
},
interval: 10000
},
{ root: true }
)
return 'error'
}
}
}

View file

@ -0,0 +1,3 @@
export function highlightNoteAccount({ dispatch }, { isHighlighted }) {
dispatch('createMutation', { type: 'SET_HIGHLIGHT_NOTE_ACCOUNT', payload: { isHighlighted } })
}

View file

@ -0,0 +1,51 @@
import { decryptNote } from './decryptNote'
import { decryptNotes, _encryptFormatTx, _checkCurrentTx } from './decryptNotes'
import { saveAccount } from './saveAccount'
import { removeAccount } from './removeAccount'
import { getRecoveryKey } from './getRecoveryKey'
import { enabledSaveFile } from './enabledSaveFile'
import { checkRecoveryKey } from './checkRecoveryKey'
import { setupAccount, saveEncryptedAccount } from './setupAccount'
import { recoverAccountFromChain, decryptAccount, getAccountFromAddress } from './recoverAccountFromChain'
import { checkExistAccount } from './checkExistAccount'
import { getEncryptedNote } from './getEncryptedNote'
import { getEncryptedAccount } from './getEncryptedAccount'
import { recoverAccountFromKey } from './recoverAccountFromKey'
import { redirectToAccount } from './redirectToAccount'
import { highlightNoteAccount } from './highlightNoteAccount'
import { saveRecoveryKeyOnFile } from './saveRecoveryKeyOnFile'
import { createMutation, clearState } from './utils'
export const actions = {
// utils
clearState,
createMutation,
// actions
saveAccount,
decryptNote,
decryptNotes,
setupAccount,
removeAccount,
decryptAccount,
getRecoveryKey,
enabledSaveFile,
checkRecoveryKey,
getEncryptedNote,
redirectToAccount,
checkExistAccount,
getEncryptedAccount,
highlightNoteAccount,
saveEncryptedAccount,
getAccountFromAddress,
recoverAccountFromKey,
recoverAccountFromChain,
saveRecoveryKeyOnFile,
// private actions
_encryptFormatTx,
_checkCurrentTx
}

View file

@ -0,0 +1,14 @@
import { getEncryptionPublicKey } from 'eth-sig-util'
export async function decryptAccount({ dispatch }, encryptedAccount) {
try {
const privateKey = await dispatch('metamask/ethDecrypt', encryptedAccount, { root: true })
const publicKey = getEncryptionPublicKey(privateKey)
const { address } = await this.$provider.web3.eth.accounts.privateKeyToAccount(privateKey)
return { address, publicKey, privateKey }
} catch (err) {
throw new Error(`Method decryptAccount has error: ${err.message}`)
}
}

View file

@ -0,0 +1,80 @@
import { graph } from '@/services'
import { unpackEncryptedMessage } from '@/utils'
export async function getAccountFromAddress({ getters, rootGetters }, address) {
try {
const netId = rootGetters['metamask/netId']
const rpc = rootGetters['settings/currentRpc']
const web3 = this.$provider.getWeb3(rpc.url)
const currentBlockNumber = await web3.eth.getBlockNumber()
const events = await getEventsFromBlockPart({ getters }, { netId, currentBlockNumber, address })
const [lastEvent] = events.slice(-1)
if (!lastEvent) {
throw new Error(`Please setup account, account doesn't exist for this address`)
}
const data = lastEvent.encryptedAccount ? lastEvent.encryptedAccount : lastEvent.returnValues.data
const backup = lastEvent.address ? lastEvent.address : lastEvent.returnValues.who
const encryptedMessage = unpackEncryptedMessage(data)
const encryptedKey = Buffer.from(JSON.stringify(encryptedMessage)).toString('hex')
return {
backup,
encryptedKey
}
} catch (err) {
throw new Error(`Method getAccountFromAddress has error: ${err.message}`)
}
}
async function getEventsFromBlockPart({ getters }, { address, currentBlockNumber, netId }) {
try {
const { events: graphEvents, lastSyncBlock } = await graph.getNoteAccounts({ address, netId })
const blockDifference = Math.ceil(currentBlockNumber - lastSyncBlock)
let blockRange = 1
if (Number(netId) === 56) {
blockRange = 4950
}
let numberParts = blockDifference === 0 ? 1 : Math.ceil(blockDifference / blockRange)
const part = Math.ceil(blockDifference / numberParts)
let events = []
let fromBlock = lastSyncBlock
let toBlock = lastSyncBlock + part
if (toBlock >= currentBlockNumber) {
toBlock = 'latest'
numberParts = 1
}
for (let i = 0; i < numberParts; i++) {
const partOfEvents = await getters.echoContract.getEvents({
fromBlock,
toBlock,
address
})
if (partOfEvents) {
events = events.concat(partOfEvents)
}
fromBlock = toBlock
toBlock += part
}
events = graphEvents.concat(events)
return events
} catch (err) {
console.log(`getEventsFromBlock has error: ${err.message}`)
return false
}
}

View file

@ -0,0 +1,3 @@
export { decryptAccount } from './decryptAccount'
export { getAccountFromAddress } from './getAccountFromAddress'
export { recoverAccountFromChain } from './recoverAccountFromChain'

View file

@ -0,0 +1,13 @@
export async function recoverAccountFromChain({ dispatch, rootState }) {
const { ethAccount } = rootState.metamask
try {
const { encryptedKey, backup } = await dispatch('getAccountFromAddress', ethAccount)
const { address, publicKey, privateKey } = await dispatch('decryptAccount', encryptedKey)
this.$sessionStorage.setItem(address, privateKey)
dispatch('saveAccount', { account: { publicKey, privateKey: encryptedKey }, address, backup })
} catch (err) {
throw new Error(`Method recoverAccountFromChain has error: ${err.message}`)
}
}

View file

@ -0,0 +1,23 @@
import { getEncryptionPublicKey } from 'eth-sig-util'
export function recoverAccountFromKey({ dispatch }, { recoveryKey }) {
try {
dispatch('createMutation', { type: 'SET_DOMAIN_REQUEST', payload: { key: 'recoverAccountFromKey' } })
const publicKey = getEncryptionPublicKey(recoveryKey)
const { address } = this.$provider.web3.eth.accounts.privateKeyToAccount(recoveryKey)
const keyWithOutPrefix = recoveryKey.slice(0, 2) === '0x' ? recoveryKey.replace('0x', '') : recoveryKey
this.$sessionStorage.setItem(address, keyWithOutPrefix)
dispatch('saveAccount', { account: { publicKey, privateKey: '' }, address })
dispatch('createMutation', { type: 'SET_DOMAIN_SUCCESS', payload: { key: 'recoverAccountFromKey' } })
} catch (err) {
dispatch('createMutation', {
type: 'SET_DOMAIN_FAILED',
payload: { key: 'recoverAccountFromKey', errorMessage: err.message }
})
}
}

View file

@ -0,0 +1,4 @@
export function redirectToAccount({ dispatch }) {
dispatch('highlightNoteAccount', { isHighlighted: true })
this.$router.push({ path: '/account' })
}

View file

@ -0,0 +1,19 @@
export function removeAccount({ dispatch }) {
try {
dispatch('createMutation', { type: 'SET_DOMAIN_REQUEST', payload: { key: 'removeAccount' } })
dispatch('createMutation', { type: 'REMOVE_ADDRESSES' })
dispatch('createMutation', { type: 'REMOVE_KEY' })
dispatch('createMutation', { type: 'ENABLED_SAVE_FILE', payload: { isEnabled: true } })
dispatch('createMutation', { type: 'REMOVE_STATISTIC' })
this.$sessionStorage.clear()
dispatch('createMutation', { type: 'SET_DOMAIN_SUCCESS', payload: { key: 'removeAccount' } })
} catch (err) {
dispatch('createMutation', {
type: 'SET_DOMAIN_FAILED',
payload: { key: 'removeAccount', errorMessage: err.message }
})
}
}

View file

@ -0,0 +1,19 @@
export function saveAccount({ dispatch, rootState }, { account, address, backup }) {
const { ethAccount } = rootState.metamask
dispatch('createMutation', {
type: 'SET_ENCRYPTED_ACCOUNT',
payload: account
})
dispatch('createMutation', {
type: 'SET_ADDRESSES',
payload: {
addresses: {
encrypt: address,
backup: backup || '-',
connect: ethAccount
}
}
})
}

View file

@ -0,0 +1,13 @@
import { saveAsFile } from '@/utils'
export function saveRecoveryKeyOnFile(_, { recoveryKey }) {
try {
const { address } = this.$provider.web3.eth.accounts.privateKeyToAccount(recoveryKey)
const data = new Blob([`${recoveryKey}`], { type: 'text/plain;charset=utf-8' })
saveAsFile(data, `backup-note-account-key-${address.slice(0, 10)}.txt`)
} catch (err) {
console.error('saveFile', err.message)
}
}

View file

@ -0,0 +1,2 @@
export { setupAccount } from './setupAccount'
export { saveEncryptedAccount } from './saveEncryptedAccount'

View file

@ -0,0 +1,32 @@
import { numberToHex } from 'web3-utils'
import { packEncryptedMessage } from '@/utils'
export async function saveEncryptedAccount({ getters, dispatch }, { from, encryptedData, callback }) {
try {
const contract = getters.echoContract
const data = packEncryptedMessage(encryptedData)
const callData = contract.getCallData(data)
const gas = await contract.estimateGas({ from, data })
const callParams = {
method: 'eth_sendTransaction',
params: {
data: callData,
to: contract.address,
gas: numberToHex(gas + 10000)
},
watcherParams: {
title: 'accountSaving',
successTitle: 'accountSaved',
onSuccess: callback
},
isSaving: false
}
await dispatch('metamask/sendTransaction', callParams, { root: true })
} catch (err) {
throw new Error(err.message)
}
}

View file

@ -0,0 +1,62 @@
export async function setupAccount({ dispatch, commit, getters, rootState }, { privateKey }) {
try {
dispatch('createMutation', { type: 'SET_DOMAIN_REQUEST', payload: { key: 'setupAccount' } })
await dispatch('checkExistAccount')
if (getters.isExistAccount) {
throw new Error(this.app.i18n.t('haveAccountSetupWithWallet'))
}
dispatch('loading/enable', { message: this.app.i18n.t('pleaseConfirmInWallet') }, { root: true })
const { ethAccount } = rootState.metamask
const pubKey = await dispatch('metamask/getEncryptionPublicKey', {}, { root: true })
const account = await dispatch('getEncryptedAccount', { privateKey, pubKey })
const { address, publicKey, hexPrivateKey, encryptedData } = account
const callback = () => {
dispatch('createMutation', {
type: 'CHECK_ACCOUNT',
payload: { isExist: true }
})
dispatch('saveAccount', {
address,
backup: ethAccount,
account: { publicKey, privateKey: hexPrivateKey }
})
dispatch(
'notice/addNoticeWithInterval',
{
notice: {
title: 'account.modals.setupAccount.successfulNotice',
type: 'info'
},
interval: 10000
},
{ root: true }
)
}
await dispatch('saveEncryptedAccount', {
encryptedData,
from: ethAccount,
callback
})
this.$sessionStorage.setItem(address, privateKey)
dispatch('createMutation', { type: 'SET_DOMAIN_SUCCESS', payload: { key: 'setupAccount' } })
} catch (err) {
console.log('createMutation', err)
dispatch('createMutation', {
type: 'SET_DOMAIN_FAILED',
payload: { key: 'setupAccount', errorMessage: err.message }
})
} finally {
dispatch('loading/disable', {}, { root: true })
}
}

View file

@ -0,0 +1,14 @@
function createMutation({ commit, rootState }, { type, payload }) {
const { netId } = rootState.metamask
commit(type, { ...payload, netId })
}
function clearState({ dispatch }, { key }) {
dispatch('createMutation', {
type: 'CLEAR_STATE',
payload: { key }
})
}
export { clearState, createMutation }

View file

@ -0,0 +1,57 @@
import Web3 from 'web3'
const ABI = [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'who', type: 'address' },
{ indexed: false, internalType: 'bytes', name: 'data', type: 'bytes' }
],
name: 'Echo',
type: 'event'
},
{
inputs: [{ internalType: 'bytes', name: '_data', type: 'bytes' }],
name: 'echo',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
]
export class EchoContract {
constructor({ rpcUrl, address }) {
this.web3 = new Web3(rpcUrl)
this.contract = new this.web3.eth.Contract(ABI, address)
this.address = this.contract._address
}
async getEvents({ address, fromBlock = 0, toBlock = 'latest' }) {
try {
return await this.contract.getPastEvents('Echo', {
toBlock,
fromBlock,
filter: { who: address }
})
} catch (err) {
throw new Error(`Method getEvents has error: ${err.message}`)
}
}
async estimateGas({ from, data }) {
try {
return await this.contract.methods.echo(data).estimateGas({ from })
} catch (err) {
throw new Error(`Method estimateGas has error: ${err.message}`)
}
}
getCallData(data) {
try {
return this.contract.methods.echo(data).encodeABI()
} catch (err) {
throw new Error(`Method getCallData has error: ${err.message}`)
}
}
}

View file

@ -0,0 +1,81 @@
import { EchoContract } from './Contract'
import networkConfig from '@/networkConfig'
export const getters = {
echoContract: (state, getters, rootState, rootGetters) => {
const netId = rootState.metamask.netId
const { url } = rootGetters['settings/currentRpc']
const address = networkConfig[`netId${netId}`].echoContractAccount
return new EchoContract({ rpcUrl: url, address })
},
// selectors
selectUi: (state, getters, rootState) => (key) => {
const { netId } = rootState.metamask
return state.ui[`netId${netId}`][key]
},
selectDomain: (state, getters, rootState) => (key) => {
const { netId } = rootState.metamask
return state.domain[`netId${netId}`][key]
},
// ui store
isExistAccount: (state, getters) => {
return getters.selectUi('isExistAccount')
},
accounts: (state, getters) => {
return getters.selectUi('addresses')
},
statistic: (state, getters) => {
const data = getters.selectUi('statistic')
if (Array.isArray(data)) {
return data
} else {
return []
}
},
noteAccountBalance: (state, getters, rootState, rootGetters) => {
let balance = 0
const nativeCurrency = rootGetters['metamask/nativeCurrency']
getters.statistic.forEach(({ currency, amount }) => {
if (currency === nativeCurrency) {
balance += Number(amount)
}
})
return balance
},
isSetupAccount: (state, getters) => {
return Boolean(getters.selectUi('encryptedPublicKey'))
},
encryptedPublicKey: (state, getters) => {
return getters.selectUi('encryptedPublicKey')
},
encryptedPrivateKey: (state, getters) => {
return getters.selectUi('encryptedPrivateKey')
},
isEnabledSaveFile: (state, getters) => {
return getters.selectUi('isEnabledSaveFile')
},
isHighlightedNoteAccount: (state, getters) => {
return getters.selectUi('isHighlightedNoteAccount')
},
// domain store
setupAccountRequest: (state, getters) => {
return getters.selectDomain('setupAccount')
},
recoverAccountRequest: (state, getters) => {
return getters.selectDomain('recoverAccountFromChain')
},
removeAccountRequest: (state, getters) => {
return getters.selectDomain('removeAccount')
},
decryptNotesRequest: (state, getters) => {
return getters.selectDomain('decryptNotes')
},
recoverAccountFromKeyRequest: (state, getters) => {
return getters.selectDomain('recoverAccountFromKey')
}
}

View file

@ -0,0 +1,6 @@
import { state } from './state'
import { actions } from './actions'
import { getters } from './getters'
import { mutations } from './mutations'
export { actions, mutations, getters, state }

View file

@ -0,0 +1,10 @@
import { initialUiState } from '../state/ui'
export const addresses = {
SET_ADDRESSES(state, { netId, addresses }) {
this._vm.$set(state.ui[`netId${netId}`], 'addresses', addresses)
},
REMOVE_ADDRESSES(state, { netId }) {
this._vm.$set(state.ui[`netId${netId}`], 'addresses', initialUiState.addresses)
}
}

View file

@ -0,0 +1,34 @@
export const domain = {
CLEAR_STATE(state, { netId, key }) {
this._vm.$set(state.domain[`netId${netId}`], key, {
isError: false,
isSuccess: false,
isFetching: false,
errorMessage: ''
})
},
SET_DOMAIN_REQUEST(state, { netId, key }) {
this._vm.$set(state.domain[`netId${netId}`], key, {
isError: false,
isSuccess: false,
isFetching: true,
errorMessage: ''
})
},
SET_DOMAIN_FAILED(state, { netId, key, errorMessage }) {
this._vm.$set(state.domain[`netId${netId}`], key, {
errorMessage,
isError: true,
isSuccess: false,
isFetching: false
})
},
SET_DOMAIN_SUCCESS(state, { netId, key }) {
this._vm.$set(state.domain[`netId${netId}`], key, {
isError: false,
isSuccess: true,
isFetching: false,
errorMessage: ''
})
}
}

View file

@ -0,0 +1,5 @@
export const enabledSaveFile = {
ENABLED_SAVE_FILE(state, { netId, isEnabled }) {
this._vm.$set(state.ui[`netId${netId}`], 'isEnabledSaveFile', isEnabled)
}
}

View file

@ -0,0 +1,16 @@
export const encryptedAccount = {
SET_ENCRYPTED_ACCOUNT(state, { netId, publicKey, privateKey }) {
this._vm.$set(state.ui[`netId${netId}`], 'encryptedPublicKey', publicKey)
this._vm.$set(state.ui[`netId${netId}`], 'encryptedPrivateKey', privateKey)
},
CHECK_ACCOUNT(state, { netId, isExist }) {
this._vm.$set(state.ui[`netId${netId}`], 'isExistAccount', isExist)
},
REMOVE_KEY(state, { netId }) {
this._vm.$set(state.ui[`netId${netId}`], 'encryptedPublicKey', '')
this._vm.$set(state.ui[`netId${netId}`], 'encryptedPrivateKey', '')
},
SET_HIGHLIGHT_NOTE_ACCOUNT(state, { netId, isHighlighted }) {
this._vm.$set(state.ui[`netId${netId}`], 'isHighlightedNoteAccount', isHighlighted)
}
}

View file

@ -0,0 +1,8 @@
export const statistic = {
SET_STATISTIC(state, { netId, statistic }) {
this._vm.$set(state.ui[`netId${netId}`], 'statistic', statistic)
},
REMOVE_STATISTIC(state, { netId }) {
this._vm.$set(state.ui[`netId${netId}`], 'statistic', {})
}
}

View file

@ -0,0 +1,13 @@
import { domain } from './Domain'
import { statistic } from './Statistic'
import { addresses } from './Addresses'
import { enabledSaveFile } from './EnabledSaveFile'
import { encryptedAccount } from './EncryptedAccount'
export const mutations = {
...domain,
...addresses,
...statistic,
...enabledSaveFile,
...encryptedAccount
}

View file

@ -0,0 +1,18 @@
import { createChainIdState } from '@/utils'
const requestState = {
isError: false,
isSuccess: false,
isFetching: false,
errorMessage: ''
}
const initialDomainState = {
setupAccount: Object.assign({}, requestState),
decryptNotes: Object.assign({}, requestState),
removeAccount: Object.assign({}, requestState),
recoverAccountFromKey: Object.assign({}, requestState),
recoverAccountFromChain: Object.assign({}, requestState)
}
export const domain = createChainIdState(initialDomainState)

View file

@ -0,0 +1,11 @@
import { ui } from './ui'
import { domain } from './domain'
export const state = () => {
return {
ui,
domain
}
}
export * from './ui'

View file

@ -0,0 +1,17 @@
import { createChainIdState } from '@/utils'
export const initialUiState = {
addresses: {
backup: '-',
connect: '-',
encrypt: '-'
},
isExistAccount: false,
encryptedPublicKey: '',
encryptedPrivateKey: '',
isEnabledSaveFile: true,
statistic: [],
isHighlightedNoteAccount: false
}
export const ui = createChainIdState(initialUiState)

1
modules/index.js Normal file
View file

@ -0,0 +1 @@
export { AccountPage } from './account'