769 lines
21 KiB
Vue
769 lines
21 KiB
Vue
<template>
|
|
<div class="wallet-settings">
|
|
<q-btn
|
|
icon-right="more_vert"
|
|
:label="$t('buttons.settings')"
|
|
size="md"
|
|
flat
|
|
>
|
|
<q-menu anchor="bottom right" self="top right">
|
|
<q-list separator class="menu-list">
|
|
<q-item
|
|
v-close-popup
|
|
clickable
|
|
:disabled="!is_ready"
|
|
@click.native="getPrivateKeys()"
|
|
>
|
|
<q-item-label header>{{
|
|
$t("menuItems.showPrivateKeys")
|
|
}}</q-item-label>
|
|
</q-item>
|
|
<q-item
|
|
v-close-popup
|
|
clickable
|
|
:disabled="!is_ready"
|
|
@click.native="showModal('change_password')"
|
|
>
|
|
<q-item-label header>{{
|
|
$t("menuItems.changePassword")
|
|
}}</q-item-label>
|
|
</q-item>
|
|
<q-item
|
|
v-close-popup
|
|
clickable
|
|
:disabled="!is_ready"
|
|
@click.native="showModal('rescan')"
|
|
>
|
|
<q-item-label header>{{
|
|
$t("menuItems.rescanWallet")
|
|
}}</q-item-label>
|
|
</q-item>
|
|
<q-item
|
|
v-close-popup
|
|
clickable
|
|
:disabled="!is_ready"
|
|
@click.native="showModal('key_image')"
|
|
>
|
|
<q-item-label header>{{
|
|
$t("menuItems.manageKeyImages")
|
|
}}</q-item-label>
|
|
</q-item>
|
|
<q-item
|
|
v-close-popup
|
|
clickable
|
|
:disabled="!is_ready"
|
|
@click.native="deleteWallet()"
|
|
>
|
|
<q-item-label header>{{
|
|
$t("menuItems.deleteWallet")
|
|
}}</q-item-label>
|
|
</q-item>
|
|
</q-list>
|
|
</q-menu>
|
|
</q-btn>
|
|
|
|
<!-- Modals -->
|
|
<!-- PRIVATE KEY MODAL -->
|
|
<q-dialog
|
|
v-model="modals.private_keys.visible"
|
|
minimized
|
|
@hide="closePrivateKeys()"
|
|
>
|
|
<div class="modal private-key-modal">
|
|
<div class="modal-header">{{ $t("titles.privateKeys") }}</div>
|
|
<div class="q-ma-md">
|
|
<template v-if="secret.mnemonic">
|
|
<h6 class="q-mb-xs q-mt-lg">
|
|
{{ $t("strings.seedWords") }}
|
|
</h6>
|
|
<div class="row">
|
|
<div class="col">
|
|
{{ secret.mnemonic }}
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
class="copy-btn"
|
|
color="primary"
|
|
padding="xs"
|
|
size="sm"
|
|
icon="file_copy"
|
|
@click="copyPrivateKey('mnemonic', $event)"
|
|
>
|
|
<q-tooltip
|
|
anchor="center left"
|
|
self="center right"
|
|
:offset="[5, 10]"
|
|
>
|
|
{{ $t("menuItems.copySeedWords") }}
|
|
</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="secret.view_key != secret.spend_key">
|
|
<h6 class="q-mb-xs">{{ $t("strings.viewKey") }}</h6>
|
|
<div class="row">
|
|
<div class="col" style="word-break:break-all;">
|
|
{{ secret.view_key }}
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
class="copy-btn"
|
|
color="primary"
|
|
padding="xs"
|
|
size="sm"
|
|
icon="file_copy"
|
|
@click="copyPrivateKey('view_key', $event)"
|
|
>
|
|
<q-tooltip
|
|
anchor="center left"
|
|
self="center right"
|
|
:offset="[5, 10]"
|
|
>
|
|
{{ $t("menuItems.copyViewKey") }}
|
|
</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="!/^0*$/.test(secret.spend_key)">
|
|
<h6 class="q-mb-xs">{{ $t("strings.spendKey") }}</h6>
|
|
<div class="row">
|
|
<div class="col" style="word-break:break-all;">
|
|
{{ secret.spend_key }}
|
|
</div>
|
|
<div class="col-auto">
|
|
<q-btn
|
|
class="copy-btn"
|
|
color="primary"
|
|
padding="xs"
|
|
size="sm"
|
|
icon="file_copy"
|
|
@click="copyPrivateKey('spend_key', $event)"
|
|
>
|
|
<q-tooltip
|
|
anchor="center left"
|
|
self="center right"
|
|
:offset="[5, 10]"
|
|
>
|
|
{{ $t("menuItems.copySpendKey") }}
|
|
</q-tooltip>
|
|
</q-btn>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="q-mt-lg">
|
|
<q-btn
|
|
color="primary"
|
|
:label="$t('buttons.close')"
|
|
@click="hideModal('private_keys')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-dialog>
|
|
|
|
<!-- RESCAN MODAL -->
|
|
<q-dialog v-model="modals.rescan.visible" minimized>
|
|
<div class="modal rescan-modal">
|
|
<div class="a-ma-lg modal-header">{{ $t("titles.rescanWallet") }}</div>
|
|
<div class="q-ma-md">
|
|
<p>{{ $t("strings.rescanModalDescription") }}</p>
|
|
|
|
<div class="q-mt-lg">
|
|
<q-radio
|
|
v-model="modals.rescan.type"
|
|
val="full"
|
|
:label="$t('fieldLabels.rescanFullBlockchain')"
|
|
/>
|
|
</div>
|
|
<div class="q-mt-sm">
|
|
<q-radio
|
|
v-model="modals.rescan.type"
|
|
val="spent"
|
|
:label="$t('fieldLabels.rescanSpentOutputs')"
|
|
/>
|
|
</div>
|
|
|
|
<div class="q-mt-xl text-right">
|
|
<q-btn
|
|
flat
|
|
class="q-mr-sm"
|
|
:label="$t('buttons.close')"
|
|
@click="hideModal('rescan')"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
:label="$t('buttons.rescan')"
|
|
@click="rescanWallet()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-dialog>
|
|
|
|
<!-- KEY IMAGE MODAL -->
|
|
<q-dialog
|
|
v-model="modals.key_image.visible"
|
|
class="key-image-modal"
|
|
minimized
|
|
>
|
|
<div class="modal key-image-modal">
|
|
<div class="modal-header">
|
|
<!-- Export/Import key images -->
|
|
{{
|
|
$t("dialog.keyImages.title", {
|
|
type: $t(
|
|
`dialog.keyImages.${modals.key_image.type.toLowerCase()}`
|
|
)
|
|
})
|
|
}}
|
|
</div>
|
|
<div class="q-ma-md">
|
|
<div class="row q-mb-md">
|
|
<div class="q-mr-xl">
|
|
<q-radio
|
|
v-model="modals.key_image.type"
|
|
val="Export"
|
|
:label="$t('dialog.keyImages.export')"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<q-radio
|
|
v-model="modals.key_image.type"
|
|
val="Import"
|
|
:label="$t('dialog.keyImages.import')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="modals.key_image.type == 'Export'">
|
|
<OxenField
|
|
class="q-mt-lg"
|
|
:label="$t('fieldLabels.keyImages.exportDirectory')"
|
|
disable-hover
|
|
>
|
|
<q-input
|
|
v-model="modals.key_image.export_path"
|
|
disable
|
|
borderless
|
|
/>
|
|
<input
|
|
id="keyImageExportPath"
|
|
ref="keyImageExportSelect"
|
|
class="image-path"
|
|
type="file"
|
|
webkitdirectory
|
|
directory
|
|
hidden
|
|
@change="setKeyImageExportPath"
|
|
/>
|
|
<q-btn color="primary" @click="selectKeyImageExportPath">{{
|
|
$t("buttons.browse")
|
|
}}</q-btn>
|
|
</OxenField>
|
|
</template>
|
|
<template v-if="modals.key_image.type == 'Import'">
|
|
<OxenField
|
|
class="q-mt-lg"
|
|
:label="$t('fieldLabels.keyImages.importFile')"
|
|
disable-hover
|
|
>
|
|
<q-input
|
|
v-model="modals.key_image.import_path"
|
|
disable
|
|
borderless
|
|
/>
|
|
<input
|
|
id="keyImageImportPath"
|
|
ref="keyImageImportSelect"
|
|
type="file"
|
|
class="image-path"
|
|
hidden
|
|
@change="setKeyImageImportPath"
|
|
/>
|
|
<q-btn color="primary" @click="selectKeyImageImportPath">{{
|
|
$t("buttons.browse")
|
|
}}</q-btn>
|
|
</OxenField>
|
|
</template>
|
|
|
|
<div class="q-mt-lg text-right">
|
|
<q-btn
|
|
flat
|
|
class="q-mr-sm"
|
|
:label="$t('buttons.close')"
|
|
@click="hideModal('key_image')"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
:label="$t('buttons.' + modals.key_image.type.toLowerCase())"
|
|
@click="doKeyImages()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-dialog>
|
|
|
|
<!-- CHANGE PASSWORD MODAL -->
|
|
<q-dialog
|
|
v-model="modals.change_password.visible"
|
|
minimized
|
|
@hide="clearChangePassword()"
|
|
>
|
|
<div class="modal password-modal">
|
|
<div class="modal-header">{{ $t("titles.changePassword") }}</div>
|
|
<div class="q-ma-md">
|
|
<q-input
|
|
v-model="modals.change_password.old_password"
|
|
type="password"
|
|
:label="$t('fieldLabels.oldPassword')"
|
|
/>
|
|
<q-input
|
|
v-model="modals.change_password.new_password"
|
|
type="password"
|
|
:label="$t('fieldLabels.newPassword')"
|
|
/>
|
|
|
|
<q-input
|
|
v-model="modals.change_password.new_password_confirm"
|
|
type="password"
|
|
:label="$t('fieldLabels.confirmNewPassword')"
|
|
/>
|
|
|
|
<div class="q-mt-xl text-right">
|
|
<q-btn
|
|
flat
|
|
class="q-mr-sm"
|
|
:label="$t('buttons.close')"
|
|
@click="hideModal('change_password')"
|
|
/>
|
|
<q-btn
|
|
color="primary"
|
|
:label="$t('buttons.change')"
|
|
@click="doChangePassword()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</q-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
const { clipboard } = require("electron");
|
|
import { mapState } from "vuex";
|
|
import WalletPassword from "src/mixins/wallet_password";
|
|
import OxenField from "components/oxen_field";
|
|
|
|
export default {
|
|
name: "WalletSettings",
|
|
components: {
|
|
OxenField
|
|
},
|
|
mixins: [WalletPassword],
|
|
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: ""
|
|
}
|
|
}
|
|
};
|
|
},
|
|
computed: mapState({
|
|
theme: state => state.gateway.app.config.appearance.theme,
|
|
info: state => state.gateway.wallet.info,
|
|
secret: state => state.gateway.wallet.secret,
|
|
wallet_data_dir: state => state.gateway.app.config.app.wallet_data_dir,
|
|
is_ready() {
|
|
return this.$store.getters["gateway/isReady"];
|
|
},
|
|
locale() {
|
|
return this.$q.lang.getLocale();
|
|
}
|
|
}),
|
|
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.$t(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
|
|
}
|
|
},
|
|
created() {
|
|
const path = require("upath");
|
|
this.modals.key_image.export_path = path.join(
|
|
this.wallet_data_dir,
|
|
"images",
|
|
this.info.name
|
|
);
|
|
this.modals.key_image.import_path = path.join(
|
|
this.wallet_data_dir,
|
|
"images",
|
|
this.info.name,
|
|
"key_image_export"
|
|
);
|
|
},
|
|
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: this.$t("notification.errors.copyingPrivateKeys")
|
|
});
|
|
return;
|
|
}
|
|
|
|
clipboard.writeText(this.secret[type]);
|
|
|
|
let type_key = "seedWords";
|
|
if (type === "spend_key") {
|
|
type_key = "spendKey";
|
|
} else if (type === "view_key") {
|
|
type_key = "viewKey";
|
|
}
|
|
const type_title = this.$t("dialog.copyPrivateKeys." + type_key);
|
|
|
|
this.$q
|
|
.dialog({
|
|
title: this.$t("dialog.copyPrivateKeys.title", {
|
|
type: type_title
|
|
}),
|
|
message: this.$t("dialog.copyPrivateKeys.message"),
|
|
ok: {
|
|
label: this.$t("dialog.buttons.ok"),
|
|
color: "primary"
|
|
}
|
|
})
|
|
.onDismiss(() => null)
|
|
.onCancel(() => null)
|
|
.onOk(() => {
|
|
this.$q.notify({
|
|
type: "positive",
|
|
timeout: 1000,
|
|
message: this.$t("notification.positive.copied", {
|
|
item: this.$t("strings." + type_key)
|
|
})
|
|
});
|
|
});
|
|
},
|
|
async getPrivateKeys() {
|
|
if (!this.is_ready) return;
|
|
let passwordDialog = await this.showPasswordConfirmation({
|
|
title: this.$t("dialog.showPrivateKeys.title"),
|
|
noPasswordMessage: this.$t("dialog.showPrivateKeys.message"),
|
|
ok: {
|
|
label: this.$t("dialog.showPrivateKeys.ok"),
|
|
color: "primary"
|
|
},
|
|
cancel: {
|
|
color: "tertiary",
|
|
flat: true
|
|
},
|
|
color: "white"
|
|
});
|
|
passwordDialog
|
|
.onOk(password => {
|
|
// if no password set
|
|
password = password || "";
|
|
this.$gateway.send("wallet", "get_private_keys", {
|
|
password
|
|
});
|
|
})
|
|
.onDismiss(() => {})
|
|
.onCancel(() => {});
|
|
},
|
|
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: this.$t("dialog.rescan.title"),
|
|
message: this.$t("dialog.rescan.message"),
|
|
ok: {
|
|
label: this.$t("dialog.rescan.ok"),
|
|
color: "primary"
|
|
},
|
|
cancel: {
|
|
flat: true,
|
|
label: this.$t("dialog.buttons.cancel")
|
|
}
|
|
})
|
|
.onOk(() => {
|
|
this.$gateway.send("wallet", "rescan_blockchain");
|
|
})
|
|
.onDismiss(() => {})
|
|
.onCancel(() => {});
|
|
} 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;
|
|
},
|
|
async doKeyImages() {
|
|
this.hideModal("key_image");
|
|
|
|
const type = this.$t(
|
|
`dialog.keyImages.${this.modals.key_image.type.toLowerCase()}`
|
|
);
|
|
|
|
let passwordDialog = await this.showPasswordConfirmation({
|
|
title: this.$t("dialog.keyImages.title", { type }),
|
|
noPasswordMessage: this.$t("dialog.keyImages.message", {
|
|
type: type.toLocaleLowerCase(this.locale)
|
|
}),
|
|
ok: {
|
|
label: type.toLocaleUpperCase(this.locale),
|
|
color: "primary"
|
|
},
|
|
dark: this.theme == "dark",
|
|
color: this.theme == "dark" ? "white" : "dark"
|
|
});
|
|
passwordDialog
|
|
.onOk(password => {
|
|
// if no password set
|
|
password = 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
|
|
});
|
|
})
|
|
.onCancel(() => {})
|
|
.onDismiss(() => {});
|
|
},
|
|
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: this.$t("notification.errors.newPasswordSame")
|
|
});
|
|
} else if (new_password != new_password_confirm) {
|
|
this.$q.notify({
|
|
type: "negative",
|
|
timeout: 1000,
|
|
message: this.$t("notification.errors.newPasswordNoMatch")
|
|
});
|
|
} 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() {
|
|
if (!this.is_ready) return;
|
|
this.$q
|
|
.dialog({
|
|
title: this.$t("dialog.deleteWallet.title"),
|
|
message: this.$t("dialog.deleteWallet.message"),
|
|
ok: {
|
|
label: this.$t("dialog.deleteWallet.ok"),
|
|
color: "red"
|
|
},
|
|
cancel: {
|
|
flat: true,
|
|
label: this.$t("dialog.buttons.cancel"),
|
|
color: this.theme == "dark" ? "white" : "dark"
|
|
},
|
|
color: "#1F1C47"
|
|
})
|
|
.onOk(async () => {
|
|
const hasPassword = await this.hasPassword();
|
|
if (hasPassword) {
|
|
this.$q
|
|
.dialog({
|
|
title: this.$t("dialog.deleteWallet.title"),
|
|
message: this.$t("dialog.password.message"),
|
|
prompt: {
|
|
model: "",
|
|
type: "password"
|
|
},
|
|
ok: {
|
|
label: this.$t("dialog.deleteWallet.ok"),
|
|
color: "negative"
|
|
},
|
|
cancel: {
|
|
flat: true,
|
|
label: this.$t("dialog.buttons.cancel"),
|
|
color: this.theme == "dark" ? "white" : "dark"
|
|
},
|
|
dark: this.theme == "dark",
|
|
color: this.theme == "dark" ? "white" : "dark"
|
|
})
|
|
.onOk(password => {
|
|
password = password || "";
|
|
this.$gateway.send("wallet", "delete_wallet", { password });
|
|
})
|
|
.onDismiss(() => {})
|
|
.onCancel(() => {});
|
|
} else {
|
|
// no password
|
|
let password = "";
|
|
// if there's no password (password is empty string)
|
|
this.$gateway.send("wallet", "delete_wallet", { password });
|
|
}
|
|
})
|
|
.onCancel(() => {})
|
|
.onDismiss(() => {});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.wallet-settings {
|
|
.q-btn {
|
|
color: white;
|
|
}
|
|
}
|
|
|
|
.password-modal {
|
|
min-width: 400px;
|
|
background: white;
|
|
color: #1f1c47;
|
|
|
|
> * {
|
|
color: #1f1c47;
|
|
}
|
|
}
|
|
|
|
.rescan-modal {
|
|
background: white;
|
|
color: #1f1c47;
|
|
}
|
|
|
|
.image-path {
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.key-image-modal {
|
|
color: #1f1c47;
|
|
background: white;
|
|
|
|
label * {
|
|
color: #1f1c47 !important;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
input {
|
|
overflow: ellipsis;
|
|
}
|
|
}
|
|
|
|
.private-key-modal {
|
|
background: white;
|
|
color: #1f1c47;
|
|
|
|
.copy-btn {
|
|
margin-left: 8px;
|
|
}
|
|
}
|
|
|
|
.key-image-modal {
|
|
min-width: 400px;
|
|
width: 45vw;
|
|
|
|
.oxen-field {
|
|
flex: 1;
|
|
}
|
|
}
|
|
</style>
|