oxen-electron-gui-wallet/src/pages/wallet/wallet.vue

565 lines
21 KiB
Vue

<template>
<q-page padding>
<AddressHeader :address="info.address" :title="info.name" />
<div class="row">
<div class="infoBoxBalance">
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Balance</span></div>
<div class="value"><span><FormatLoki :amount="info.balance" /></span></div>
</div>
</div>
</div>
<div>
<div class="infoBox">
<div class="infoBoxContent">
<div class="text"><span>Unlocked balance</span></div>
<div class="value"><span><FormatLoki :amount="info.unlocked_balance" /></span></div>
</div>
</div>
</div>
<div class="col text-right q-mr-sm">
<div class="infoBox q-pt-md">
<q-btn icon-right="more_vert" label="Wallet actions" size="md" flat>
<q-popover anchor="bottom right" self="top right">
<q-list separator link>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="getPrivateKeys()">
<q-item-main>
<q-item-tile label>Show Private Keys</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('change_password')">
<q-item-main>
<q-item-tile label>Change Password</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('rescan')">
<q-item-main>
<q-item-tile label>Rescan Wallet</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="showModal('key_image')">
<q-item-main>
<q-item-tile label>Manage Key Images</q-item-tile>
</q-item-main>
</q-item>
<q-item :disabled="!is_ready"
v-close-overlay @click.native="deleteWallet()">
<q-item-main>
<q-item-tile label>Delete Wallet</q-item-tile>
</q-item-main>
</q-item>
</q-list>
</q-popover>
</q-btn>
</div>
</div>
</div>
<h6 class="q-my-none">Recent transactions:</h6>
<div style="margin: 0 -16px;">
<TxList :limit="5" />
</div>
<q-modal minimized v-model="modals.private_keys.visible" @hide="closePrivateKeys()">
<div class="modal-header">Show private keys</div>
<div class="q-ma-lg">
<template v-if="secret.mnemonic">
<h6 class="q-mb-xs q-mt-lg">Seed words</h6>
<div class="row">
<div class="col">
{{ secret.mnemonic }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('mnemonic', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy seed words
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="secret.view_key != secret.spend_key">
<h6 class="q-mb-xs">View key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.view_key }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('view_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy view key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<template v-if="!/^0*$/.test(secret.spend_key)">
<h6 class="q-mb-xs">Spend key</h6>
<div class="row">
<div class="col" style="word-break:break-all;">
{{ secret.spend_key }}
</div>
<div class="col-auto">
<q-btn
color="primary" style="width:25px;"
size="sm" icon="file_copy"
@click="copyPrivateKey('spend_key', $event)">
<q-tooltip anchor="center left" self="center right" :offset="[5, 10]">
Copy spend key
</q-tooltip>
</q-btn>
</div>
</div>
</template>
<div class="q-mt-lg">
<q-btn
color="primary"
@click="hideModal('private_keys')"
label="Close"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.rescan.visible">
<div class="modal-header">Rescan wallet</div>
<div class="q-ma-lg">
<p>Select full rescan or rescan of spent outputs only.</p>
<div class="q-mt-lg">
<q-radio v-model="modals.rescan.type" val="full" label="Rescan full blockchain" />
</div>
<div class="q-mt-sm">
<q-radio v-model="modals.rescan.type" val="spent" label="Rescan spent outputs" />
</div>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('rescan')"
label="Close"
/>
<q-btn
color="primary"
@click="rescanWallet()"
label="Rescan"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.key_image.visible">
<div class="modal-header">{{modals.key_image.type}} key images</div>
<div class="q-ma-lg">
<div class="row q-mb-md">
<div class="q-mr-xl">
<q-radio v-model="modals.key_image.type" val="Export" label="Export" />
</div>
<div>
<q-radio v-model="modals.key_image.type" val="Import" label="Import" />
</div>
</div>
<template v-if="modals.key_image.type == 'Export'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.export_path" stack-label="Key image export directory" disable />
<input type="file" webkitdirectory directory id="keyImageExportPath" v-on:change="setKeyImageExportPath" ref="keyImageExportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageExportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<template v-if="modals.key_image.type == 'Import'">
<q-field style="width:450px">
<div class="row gutter-sm">
<div class="col-9">
<q-input v-model="modals.key_image.import_path" stack-label="Key image import file" disable />
<input type="file" id="keyImageImportPath" v-on:change="setKeyImageImportPath" ref="keyImageImportSelect" hidden />
</div>
<div class="col-3">
<q-btn class="float-right" v-on:click="selectKeyImageImportPath">Browse</q-btn>
</div>
</div>
</q-field>
</template>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('key_image')"
label="Close"
/>
<q-btn
color="primary"
@click="doKeyImages()"
:label="modals.key_image.type"
/>
</div>
</div>
</q-modal>
<q-modal minimized v-model="modals.change_password.visible" @hide="clearChangePassword()">
<div class="modal-header">Change password</div>
<div class="q-ma-lg">
<q-field>
<q-input v-model="modals.change_password.old_password" type="password" float-label="Old Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password" type="password" float-label="New Password" :dark="theme=='dark'" />
</q-field>
<q-field>
<q-input v-model="modals.change_password.new_password_confirm" type="password" float-label="Confirm New Password" :dark="theme=='dark'" />
</q-field>
<div class="q-mt-xl text-right">
<q-btn
flat class="q-mr-sm"
@click="hideModal('change_password')"
label="Close"
/>
<q-btn
color="primary"
@click="doChangePassword()"
label="Change"
/>
</div>
</div>
</q-modal>
</q-page>
</template>
<script>
const { clipboard } = require("electron")
import { mapState } from "vuex"
import AddressHeader from "components/address_header"
import FormatLoki from "components/format_loki"
import TxList from "components/tx_list"
export default {
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info,
secret: state => state.gateway.wallet.secret,
data_dir: state => state.gateway.app.config.app.data_dir,
is_ready (state) {
return this.$store.getters["gateway/isReady"]
}
}),
data () {
return {
modals: {
private_keys: {
visible: false,
},
rescan: {
visible: false,
type: "full",
},
key_image: {
visible: false,
type: "Export",
export_path: "",
import_path: "",
},
change_password: {
visible: false,
old_password: "",
new_password: "",
new_password_confirm: "",
},
}
}
},
mounted() {
const path = require("path")
this.modals.key_image.export_path = path.join(this.data_dir, "gui")
this.modals.key_image.import_path = path.join(this.data_dir, "gui", "key_image_export")
},
watch: {
secret: {
handler(val, old) {
if(val.view_key == old.view_key) return
switch(this.secret.view_key) {
case "":
break
case -1:
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.secret.mnemonic
})
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
break
default:
this.showModal("private_keys")
break
}
},
deep: true
}
},
methods: {
showModal (which) {
if(!this.is_ready) return
this.modals[which].visible = true
},
hideModal (which) {
this.modals[which].visible = false
},
copyPrivateKey (type, event) {
event.stopPropagation()
for(let i = 0; i < event.path.length; i++) {
if(event.path[i].tagName == "BUTTON") {
event.path[i].blur()
break
}
}
if(this.secret[type] == null) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "Error copying private key",
})
return
}
clipboard.writeText(this.secret[type])
let type_human = type.substring(0,1).toUpperCase()+type.substring(1).replace("_"," ")
this.$q.dialog({
title: "Copy "+type_human,
message: "Be careful who you send your private keys to as they control your funds.",
ok: {
label: "OK"
},
}).then(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
}).catch(() => {
this.$q.notify({
type: "positive",
timeout: 1000,
message: type_human+" copied to clipboard"
})
})
},
getPrivateKeys () {
if(!this.is_ready) return
this.$q.dialog({
title: "Show private keys",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "SHOW"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "get_private_keys", {password})
}).catch(() => {
})
},
closePrivateKeys () {
this.hideModal("private_keys")
setTimeout(() => {
this.$store.commit("gateway/set_wallet_data", {
secret: {
mnemonic: "",
spend_key: "",
view_key: ""
}
})
}, 500)
},
rescanWallet () {
this.hideModal("rescan")
if(this.modals.rescan.type == "full") {
this.$q.dialog({
title: "Rescan wallet",
message: "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.",
ok: {
label: "RESCAN"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "rescan_blockchain")
}).catch(() => {
})
} else {
this.$gateway.send("wallet", "rescan_spent")
}
},
selectKeyImageExportPath () {
this.$refs.keyImageExportSelect.click()
},
setKeyImageExportPath (file) {
this.modals.key_image.export_path = file.target.files[0].path
},
selectKeyImageImportPath () {
this.$refs.keyImageImportSelect.click()
},
setKeyImageImportPath (file) {
this.modals.key_image.import_path = file.target.files[0].path
},
doKeyImages () {
this.hideModal("key_image")
this.$q.dialog({
title: this.modals.key_image.type + " key images",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: this.modals.key_image.type
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
if(this.modals.key_image.type == "Export")
this.$gateway.send("wallet", "export_key_images", {password: password, path: this.modals.key_image.export_path})
else if(this.modals.key_image.type == "Import")
this.$gateway.send("wallet", "import_key_images", {password: password, path: this.modals.key_image.import_path})
}).catch(() => {
})
},
doChangePassword () {
let old_password = this.modals.change_password.old_password
let new_password = this.modals.change_password.new_password
let new_password_confirm = this.modals.change_password.new_password_confirm
if(new_password == old_password) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New password must be different"
})
} else if(new_password != new_password_confirm) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: "New passwords do not match"
})
} else {
this.hideModal("change_password")
this.$gateway.send("wallet", "change_wallet_password", {old_password, new_password})
}
},
clearChangePassword () {
this.modals.change_password.old_password = ""
this.modals.change_password.new_password = ""
this.modals.change_password.new_password_confirm = ""
},
deleteWallet () {
this.$q.dialog({
title: "Delete wallet",
message: "Are you absolutely sure you want to delete your wallet?\nMake sure you have your private keys backed up.\nTHIS PROCESS IS NOT REVERSIBLE!",
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(() => {
this.$q.dialog({
title: "Delete wallet",
message: "Enter wallet password to continue.",
prompt: {
model: "",
type: "password"
},
ok: {
label: "DELETE",
color: "red"
},
cancel: {
flat: true,
label: "CANCEL",
color: this.theme=="dark"?"white":"dark"
}
}).then(password => {
this.$gateway.send("wallet", "delete_wallet", {password})
}).catch(() => {
})
}).catch(() => {
})
}
},
components: {
FormatLoki,
AddressHeader,
TxList
},
}
</script>
<style lang="scss">
.infoBoxBalance {
width: 290px;
}
@media screen and (max-width: 750px) {
.infoBoxBalance {
width: 200px;
}
}
</style>