508 lines
14 KiB
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>
|