oxen-electron-gui-wallet/src/components/service_node/service_node_staking.vue

508 lines
14 KiB
Vue

<template>
<div class="service-node-staking">
<div class="q-px-md q-pt-md">
<p class="tab-desc">
{{ $t("strings.serviceNodeContributionDescription") }}
<span
style="cursor: pointer; text-decoration: underline;"
@click="oxenWebsite"
>Loki {{ $t("strings.website") }}.</span
>
</p>
<OxenField
:label="$t('fieldLabels.serviceNodeKey')"
:error="$v.service_node.key.$error"
>
<q-input
v-model.trim="service_node.key"
:dark="theme == 'dark'"
:placeholder="$t('placeholders.hexCharacters', { count: 64 })"
borderless
dense
@blur="$v.service_node.key.$touch"
/>
</OxenField>
<OxenField
:label="$t('fieldLabels.amount')"
class="q-mt-md"
:error="$v.service_node.amount.$error"
>
<q-input
v-model.trim="service_node.amount"
:dark="theme == 'dark'"
type="number"
min="0"
:max="unlocked_balance / 1e9"
placeholder="0"
borderless
dense
@blur="$v.service_node.amount.$touch"
/>
<q-btn
color="primary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
:label="$t('buttons.min')"
:disable="!areButtonsEnabled()"
@click="service_node.amount = minStake(service_node.key)"
/>
<q-btn
color="primary"
:text-color="theme == 'dark' ? 'white' : 'dark'"
:label="$t('buttons.max')"
:disable="!areButtonsEnabled()"
@click="service_node.amount = maxStake(service_node.key)"
/>
</OxenField>
<div class="submit-button">
<q-btn
:disable="!is_able_to_send"
color="primary"
:label="$t('buttons.stake')"
@click="stake()"
/>
<q-btn
:disable="!is_able_to_send"
color="accent"
:label="$t('buttons.sweepAll')"
@click="sweepAllWarning()"
/>
</div>
</div>
<ServiceNodeContribute
:awaiting-service-nodes="awaiting_service_nodes"
class="contribute"
@contribute="fillStakingFields"
/>
<ConfirmTransactionDialog
:show="confirmSweepAll"
:amount="confirmFields.totalAmount"
:is-blink="confirmFields.isBlink"
:send-to="confirmFields.destination"
:fee="confirmFields.totalFees"
:on-confirm-transaction="onConfirmTransaction"
:on-cancel-transaction="onCancelTransaction"
/>
<q-inner-loading
:showing="stake_status.sending || sweep_all_status.sending"
>
<q-spinner color="primary" size="30" />
</q-inner-loading>
</div>
</template>
<script>
const objectAssignDeep = require("object-assign-deep");
import { mapState } from "vuex";
import { required, decimal } from "vuelidate/lib/validators";
import { service_node_key, greater_than_zero } from "src/validators/common";
import OxenField from "components/oxen_field";
import WalletPassword from "src/mixins/wallet_password";
import ConfirmDialogMixin from "src/mixins/confirm_dialog_mixin";
import ServiceNodeContribute from "./service_node_contribute";
import ServiceNodeMixin from "src/mixins/service_node_mixin";
import ConfirmTransactionDialog from "components/confirm_tx_dialog";
const DO_NOTHING = 10;
export default {
name: "ServiceNodeStaking",
components: {
OxenField,
ServiceNodeContribute,
ConfirmTransactionDialog
},
mixins: [WalletPassword, ConfirmDialogMixin, ServiceNodeMixin],
data() {
return {
service_node: {
key: "",
amount: 0,
// the min and max are for that particular SN,
// start at min/max for the wallet
minStakeAmount: 0,
maxStakeAmount: this.unlocked_balance / 1e9
},
confirmFields: {
isBlink: false,
totalAmount: -1,
destination: "",
totalFees: 0
}
};
},
computed: mapState({
theme: state => state.gateway.app.config.appearance.theme,
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
stake_status: state => state.gateway.service_node_status.stake,
sweep_all_status: state => state.gateway.sweep_all_status,
award_address: state => state.gateway.wallet.info.address,
confirmSweepAll: state => state.gateway.sweep_all_status.code === 1,
is_ready() {
return this.$store.getters["gateway/isReady"];
},
is_able_to_send() {
return this.$store.getters["gateway/isAbleToSend"];
},
address_placeholder(state) {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
},
awaiting_service_nodes(state) {
const nodes = state.gateway.daemon.service_nodes.nodes;
// a reserved node is one on which someone is a "contributor" of amount = 0
const getOurContribution = node =>
node.contributors.find(
c => c.address === this.our_address && c.amount > 0
);
const isAwaitingContribution = node =>
!node.active && !node.funded && node.requested_unlock_height === 0;
const isAwaitingContributionNonReserved = node =>
isAwaitingContribution(node) && !getOurContribution(node);
const isAwaitingContributionReserved = node =>
isAwaitingContribution(node) && getOurContribution(node);
// we want the reserved nodes sorted by fee at the top
const awaitingContributionNodesReserved = nodes
.filter(isAwaitingContributionReserved)
.map(n => {
return {
...n,
awaitingContribution: true
};
});
const awaitingContributionNodesNonReserved = nodes
.filter(isAwaitingContributionNonReserved)
.map(n => {
return {
...n,
awaitingContribution: true
};
});
const compareFee = (n1, n2) =>
this.getFeeDecimal(n1) > this.getFeeDecimal(n2) ? 1 : -1;
awaitingContributionNodesReserved.sort(compareFee);
awaitingContributionNodesNonReserved.sort(compareFee);
const nodesForContribution = [
...awaitingContributionNodesReserved,
...awaitingContributionNodesNonReserved
];
return nodesForContribution;
}
}),
validations: {
service_node: {
key: { required, service_node_key },
amount: {
required,
decimal,
greater_than_zero
}
}
},
watch: {
stake_status: {
handler(val, old) {
if (val.code == old.code) return;
const { code, message } = val;
switch (code) {
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message
});
this.$v.$reset();
this.service_node = {
key: "",
amount: 0
};
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message
});
break;
}
},
deep: true
},
sweep_all_status: {
handler(val, old) {
if (val.code == old.code) return;
const { code, message } = val;
switch (code) {
// the "nothing", so we can update state without doing anything
// in particular
case DO_NOTHING:
break;
case 1:
this.buildDialogFieldsSweepAll(val);
break;
case 0:
this.$q.notify({
type: "positive",
timeout: 1000,
message
});
this.$v.$reset();
this.newTx = {
amount: 0,
address: "",
// blink
priority: 5,
address_book: {
save: false,
name: "",
description: ""
},
note: ""
};
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
message
});
break;
}
},
deep: true
}
},
methods: {
oxenWebsite() {
const url = "https://loki.network/service-nodes/";
this.$gateway.send("core", "open_url", {
url
});
},
fillStakingFields(key, minContribution) {
this.service_node.key = key;
this.service_node.amount = minContribution;
},
minStake() {
const node = this.getNodeWithPubKey();
return this.getMinContribution(node);
},
maxStake() {
const node = this.getNodeWithPubKey();
return this.openForContriubtionOxen(node);
},
getFeeDecimal(node) {
const operatorPortion = node.portions_for_operator;
return (operatorPortion / 18446744073709551612) * 100;
},
getNodeWithPubKey() {
const key = this.service_node.key;
const nodeOfKey = this.awaiting_service_nodes.find(
n => n.service_node_pubkey === key
);
if (!nodeOfKey) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeKey")
});
return;
} else {
return nodeOfKey;
}
},
onConfirmTransaction() {
// put the loading spinner up
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Getting sweep all tx information",
sending: true
});
const isBlink = this.confirmFields.isBlink;
const relayTxData = {
isBlink,
isSweepAll: true
};
// Commit the transaction
this.$gateway.send("wallet", "relay_tx", relayTxData);
},
onCancelTransaction() {
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Cancel the transaction from confirm dialog",
sending: false
});
},
sweepAllWarning() {
this.$q
.dialog({
title: this.$t("dialog.sweepAllWarning.title"),
message: this.$t("dialog.sweepAllWarning.message"),
ok: {
label: this.$t("dialog.sweepAllWarning.ok"),
color: "primary"
},
cancel: {
flat: true,
label: this.$t("dialog.buttons.cancel"),
color: "negative"
}
})
.onOk(() => {
this.sweepAll();
})
.onDismiss(() => {})
.onCancel(() => {});
},
buildDialogFieldsSweepAll(txData) {
this.confirmFields = this.buildDialogFields(txData);
},
areButtonsEnabled() {
// if we can find the service node key in the list of service nodes
const key = this.service_node.key;
return !!this.awaiting_service_nodes.find(
n => n.service_node_pubkey === key
);
},
async sweepAll() {
const { unlocked_balance } = this.info;
const tx = {
amount: unlocked_balance / 1e9,
address: this.award_address,
priority: 0
};
let passwordDialog = await this.showPasswordConfirmation({
title: this.$t("dialog.sweepAll.title"),
noPasswordMessage: this.$t("dialog.sweepAll.message"),
ok: {
label: this.$t("dialog.sweepAll.ok"),
color: "#12C7BA"
}
});
passwordDialog
.onOk(password => {
password = password || "";
this.$store.commit("gateway/set_sweep_all_status", {
code: DO_NOTHING,
message: "Sweeping all",
sending: true
});
const newTx = objectAssignDeep.noMutate(tx, {
password,
isSweepAll: true
});
this.$gateway.send("wallet", "transfer", newTx);
})
.onDismiss(() => {})
.onCancel(() => {});
},
async stake() {
this.$v.service_node.$touch();
if (this.$v.service_node.key.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidServiceNodeKey")
});
return;
}
if (this.service_node.amount < 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.negativeAmount")
});
return;
} else if (this.service_node.amount == 0) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.zeroAmount")
});
return;
} else if (this.service_node.amount > this.unlocked_balance / 1e9) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.notEnoughBalance")
});
return;
} else if (this.$v.service_node.amount.$error) {
this.$q.notify({
type: "negative",
timeout: 1000,
message: this.$t("notification.errors.invalidAmount")
});
return;
}
let passwordDialog = await this.showPasswordConfirmation({
title: this.$t("dialog.stake.title"),
noPasswordMessage: this.$t("dialog.stake.message"),
ok: {
label: this.$t("dialog.stake.ok"),
color: "primary"
},
dark: this.theme == "dark",
color: this.theme == "dark" ? "white" : "dark"
});
passwordDialog
.onOk(password => {
password = password || "";
this.$store.commit("gateway/set_snode_status", {
stake: {
code: 1,
message: "Staking...",
sending: true
}
});
const service_node = objectAssignDeep.noMutate(this.service_node, {
password,
destination: this.award_address
});
this.$gateway.send("wallet", "stake", service_node);
})
.onDismiss(() => {})
.onCancel(() => {});
}
}
};
</script>
<style lang="scss">
.service-node-staking {
.submit-button {
.q-btn:not(:first-child) {
margin-left: 8px;
}
}
}
.contribute {
margin-top: 16px;
padding-left: 8px;
}
.service-node-stake-tab {
margin-top: 4px;
user-select: none;
.header {
font-weight: 450;
}
.q-item-sublabel,
.q-list-header {
font-size: 14px;
}
}
</style>