Merge pull request #283 from darcys22/hardware-wallet

Hardware wallet
This commit is contained in:
Sean 2022-07-14 12:07:02 +10:00 committed by GitHub
commit 019b8d3a9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 95 deletions

View File

@ -272,7 +272,12 @@ export class WalletRPC {
break;
case "create_wallet":
this.createWallet(params.name, params.password, params.language);
this.createWallet(
params.name,
params.password,
params.language,
params.hardware_wallet
);
break;
case "restore_wallet":
@ -509,13 +514,20 @@ export class WalletRPC {
});
}
createWallet(filename, password, language) {
isHardwareWallet(filename) {
let hwfile = path.join(this.wallet_dir, filename + ".hwdev.txt");
return fs.existsSync(hwfile);
}
createWallet(filename, password, language, hardware_wallet) {
// Reset the status error
this.sendGateway("reset_wallet_error");
this.sendRPC("create_wallet", {
filename,
password,
language
language,
hardware_wallet: !!hardware_wallet,
device_label: hardware_wallet ? "hardware_wallet" : undefined
}).then(data => {
if (data.hasOwnProperty("error")) {
this.sendGateway("set_wallet_error", { status: data.error });
@ -702,6 +714,14 @@ export class WalletRPC {
errorOnExist: true
});
}
if (fs.existsSync(import_path + ".hwdev.txt")) {
fs.copySync(
import_path + ".hwdev.txt",
destination + ".hwdev.txt",
fs.constants.COPYFILE_EXCL
);
}
} catch (e) {
this.sendGateway("set_wallet_error", {
status: {
@ -794,6 +814,10 @@ export class WalletRPC {
}
}
if (this.isHardwareWallet(filename)) {
wallet.info.hardware_wallet = true;
}
this.saveWallet().then(() => {
let address_txt_path = path.join(
this.wallet_dir,
@ -810,7 +834,11 @@ export class WalletRPC {
this.sendGateway("set_wallet_data", wallet);
this.startHeartbeat();
if (this.isHardwareWallet(filename)) {
this.startHeartbeat(10);
} else {
this.startHeartbeat();
}
});
}
@ -840,6 +868,12 @@ export class WalletRPC {
});
}
const hardware_wallet_file = path.join(
this.wallet_dir,
filename + ".hwdev.txt"
);
const hardware_wallet = fs.existsSync(hardware_wallet_file);
// store hash of the password so we can check against it later when requesting private keys, or for sending txs
this.wallet_state.password_hash = crypto
.pbkdf2Sync(password, this.auth[2], 1000, 64, "sha512")
@ -847,10 +881,20 @@ export class WalletRPC {
this.wallet_state.name = filename;
this.wallet_state.open = true;
this.startHeartbeat();
if (hardware_wallet) {
this.startHeartbeat(10);
} else {
this.startHeartbeat();
}
this.purchasedNames = {};
this.sendGateway("set_wallet_data", {
info: {
hardware_wallet
}
});
// Check if we have a view only wallet by querying the spend key
this.sendRPC("query_key", { key_type: "spend_key" }).then(data => {
if (data.hasOwnProperty("error") || !data.hasOwnProperty("result")) {
@ -867,17 +911,17 @@ export class WalletRPC {
});
}
startHeartbeat() {
startHeartbeat(multiplier = 1) {
clearInterval(this.heartbeat);
this.heartbeat = setInterval(() => {
this.heartbeatAction();
}, 5000);
}, 5000 * multiplier);
this.heartbeatAction(true);
clearInterval(this.onsHeartbeat);
this.onsHeartbeat = setInterval(() => {
this.updateLocalONSRecords();
}, 30 * 1000); // Every 30 seconds
}, 30 * 1000 * multiplier); // Every 30 seconds
this.updateLocalONSRecords();
}
@ -1701,6 +1745,9 @@ export class WalletRPC {
// send address and tx fees before sending
// isSweepAll refers to if it's the sweep from service nodes page
transfer(password, amount, address, priority, isSweepAll) {
console.log(
"TODO sean remove this - wallet: " + JSON.stringify(this.wallet)
);
const cryptoCallback = (err, password_hash) => {
if (err) {
this.sendGateway("set_tx_status", {
@ -2682,7 +2729,7 @@ export class WalletRPC {
return;
}
// Exclude all files without a keys extension
// Exclude all files without keys
if (path.extname(filename) !== ".keys") return;
const wallet_name = path.parse(filename).name;
@ -2691,7 +2738,8 @@ export class WalletRPC {
let wallet_data = {
name: wallet_name,
address: null,
password_protected: null
password_protected: null,
hardware_wallet: false
};
if (
@ -2720,6 +2768,12 @@ export class WalletRPC {
}
}
if (
fs.existsSync(path.join(this.wallet_dir, wallet_name + ".hwdev.txt"))
) {
wallet_data.hardware_wallet = true;
}
wallets.list.push(wallet_data);
} catch (e) {
// Something went wrong

View File

@ -0,0 +1,86 @@
<template>
<q-item @click.native="openWallet(wallet)">
<q-item-section avatar>
<q-icon class="wallet-icon">
<svg
width="48"
viewBox="0 0 17 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-wallet"
>
<defs class="si-glyph-fill"></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(1.000000, 0.000000)" fill="#434343">
<path
d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z"
class="si-glyph-fill"
></path>
<path
d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z"
class="si-glyph-fill"
></path>
<path
d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-label class="wallet-name" caption>{{ wallet.name }}</q-item-label>
<q-item-label class="monospace ellipsis" caption>{{
wallet.address
}}</q-item-label>
</q-item-section>
<ContextMenu
:menu-items="menuItems"
@openWallet="openWallet(wallet)"
@copyAddress="copyAddress(wallet.address)"
/>
</q-item>
</template>
<script>
const { clipboard } = require("electron");
import { mapState } from "vuex";
export default {
name: "WalletListItem",
props: {
wallet: {
type: Object,
required: true
},
openWallet: {
type: Function,
required: true
}
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
info: state => state.gateway.wallet.info
}),
methods: {
copyAddress() {
event.stopPropagation();
for (let i = 0; i < event.path.length; i++) {
if (event.path[i].tagName == "BUTTON") {
event.path[i].blur();
break;
}
}
clipboard.writeText(this.wallet.address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};
</script>
<style lang="scss"></style>

View File

@ -516,6 +516,8 @@ export default {
"Purchase or update an ONS record. If you purchase a name, it may take a minute or two for it to show up in the list.",
onsDescription:
"Here you can find all the ONS names owned by this wallet. Decrypting a record you own will return the name and value of that ONS record.",
hardwareWallet: "Hardware wallet",
hardwareWallets: "Hardware wallets",
loadingSettings: "Loading settings",
oxenBalance: "Balance",
lokinetNameDescription:
@ -545,6 +547,7 @@ export default {
recentIncomingTransactionsToAddress:
"Recent incoming transactions to this address",
recentTransactionsWithAddress: "Recent transactions with this address",
regularWallets: "Regular wallets",
rescanModalDescription:
"Select full rescan or rescan of spent outputs only.",
saveSeedWarning: "Please copy and save these in a secure location!",

View File

@ -50,12 +50,32 @@
/>
</OxenField>
<q-btn
class="submit-button"
color="primary"
:label="$t('buttons.createWallet')"
@click="create"
/>
<q-field class="q-pb-sm">
<q-checkbox
v-model="wallet.hardware_wallet"
:label="$t('strings.hardwareWallet')"
/>
</q-field>
<OxenField
v-if="!wallet.hardware_wallet"
:label="$t('fieldLabels.seedLanguage')"
>
<q-select
v-model="wallet.language"
:options="languageOptions"
:dark="theme == 'dark'"
hide-underline
/>
</OxenField>
<q-field>
<q-btn
color="primary"
:label="$t('buttons.createWallet')"
@click="create"
/>
</q-field>
</div>
</q-page>
</template>
@ -88,7 +108,8 @@ export default {
name: "",
language: languageOptions[0].value,
password: "",
password_confirm: ""
password_confirm: "",
hardware_wallet: false
},
languageOptions
};

View File

@ -1,7 +1,7 @@
<template>
<q-page>
<q-list class="wallet-list" no-border :dark="theme == 'dark'">
<template v-if="wallets.list.length">
<q-list class="wallet-list" link no-border :dark="theme == 'dark'">
<template v-if="wallet_list.length">
<div class="header row justify-between items-center">
<div class="header-title">
{{ $t("titles.yourWallets") }}
@ -30,61 +30,28 @@
</q-btn>
</div>
<div class="hr-separator" />
<q-item
v-for="wallet in wallets.list"
<!-- Hardware wallets -->
<q-list-header v-if="hardware_wallets.length">{{
$t("strings.hardwareWallets")
}}</q-list-header>
<WalletListItem
v-for="wallet in hardware_wallets"
:key="`${wallet.address}-${wallet.name}`"
@click.native="openWallet(wallet)"
>
<q-item-section avatar>
<q-icon class="wallet-icon">
<svg
width="48"
viewBox="0 0 17 16"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class="si-glyph si-glyph-wallet"
>
<defs class="si-glyph-fill"></defs>
<g
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
>
<g transform="translate(1.000000, 0.000000)" fill="#434343">
<path
d="M7.988,10.635 L7.988,8.327 C7.988,7.578 8.561,6.969 9.267,6.969 L13.964,6.969 L13.964,5.531 C13.964,4.849 13.56,4.279 13.007,4.093 L13.007,4.094 L11.356,4.08 L11.336,4.022 L3.925,4.022 L3.784,4.07 L1.17,4.068 L1.165,4.047 C0.529,4.167 0.017,4.743 0.017,5.484 L0.017,13.437 C0.017,14.269 0.665,14.992 1.408,14.992 L12.622,14.992 C13.365,14.992 13.965,14.316 13.965,13.484 L13.965,12.031 L9.268,12.031 C8.562,12.031 7.988,11.384 7.988,10.635 L7.988,10.635 Z"
class="si-glyph-fill"
></path>
<path
d="M14.996,8.061 L14.947,8.061 L9.989,8.061 C9.46,8.061 9.031,8.529 9.031,9.106 L9.031,9.922 C9.031,10.498 9.46,10.966 9.989,10.966 L14.947,10.966 L14.996,10.966 C15.525,10.966 15.955,10.498 15.955,9.922 L15.955,9.106 C15.955,8.528 15.525,8.061 14.996,8.061 L14.996,8.061 Z M12.031,10.016 L9.969,10.016 L9.969,9 L12.031,9 L12.031,10.016 L12.031,10.016 Z"
class="si-glyph-fill"
></path>
<path
d="M3.926,4.022 L10.557,1.753 L11.337,4.022 L12.622,4.022 C12.757,4.022 12.885,4.051 13.008,4.092 L11.619,0.051 L1.049,3.572 L1.166,4.048 C1.245,4.033 1.326,4.023 1.408,4.023 L3.926,4.023 L3.926,4.022 Z"
class="si-glyph-fill"
></path>
</g>
</g>
</svg>
</q-icon>
</q-item-section>
<q-item-section>
<q-item-label class="wallet-name" caption>{{
wallet.name
}}</q-item-label>
<q-item-label class="monospace ellipsis" caption>{{
wallet.address
}}</q-item-label>
</q-item-section>
<ContextMenu
:menu-items="menuItems"
@openWallet="openWallet(wallet)"
@copyAddress="copyAddress(wallet.address)"
/>
</q-item>
<q-separator />
:wallet="wallet"
:open-wallet="openWallet"
/>
<!-- Regular wallets -->
<q-list-header v-if="hardware_wallets.length">{{
$t("strings.regularWallets")
}}</q-list-header>
<WalletListItem
v-for="wallet in regular_wallets"
:key="`${wallet.address}-${wallet.name}`"
:wallet="wallet"
:open-wallet="openWallet"
/>
<q-item-separator />
</template>
<template v-else>
<q-item
@ -102,27 +69,24 @@
</template>
<script>
const { clipboard } = require("electron");
import { mapState } from "vuex";
import ContextMenu from "components/menus/contextmenu";
import WalletListItem from "components/wallet_list_item";
export default {
components: {
ContextMenu
},
data() {
const menuItems = [
{ action: "openWallet", i18n: "menuItems.openWallet" },
{ action: "copyAddress", i18n: "menuItems.copyAddress" }
];
return {
menuItems
};
WalletListItem
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
wallets: state => state.gateway.wallets,
wallet_list: state => state.gateway.wallets.list,
status: state => state.gateway.wallet.status,
hardware_wallets() {
return this.wallet_list.filter(w => w.hardware_wallet);
},
regular_wallets() {
return this.wallet_list.filter(w => !w.hardware_wallet);
},
actions() {
// TODO: Add this in once LOKI has the functionality
// <q-item @click.native="restoreViewWallet()">
@ -245,14 +209,6 @@ export default {
},
importLegacyWallet() {
this.$router.replace({ path: "wallet-select/import-legacy" });
},
copyAddress(address) {
clipboard.writeText(address);
this.$q.notify({
type: "positive",
timeout: 1000,
message: this.$t("notification.positive.addressCopied")
});
}
}
};

View File

@ -33,9 +33,11 @@ export default {
height: 0,
balance: 0,
unlocked_balance: 0,
view_only: false,
hardware_wallet: false,
accrued_balance: 0,
accrued_balance_next_payout: 0,
view_only: false
accrued_balance_next_payout: 0
},
secret: {
mnemonic: "",