- {{ $t("strings.serviceNodeDetails.unlockHeight") }}
+ {{
+ $t("strings.serviceNodeDetails.unlockHeight")
+ }}
{{ node.requested_unlock_height }}
@@ -79,7 +98,9 @@
- {{ $t("strings.serviceNodeDetails.lastUptimeProof") }}
+ {{
+ $t("strings.serviceNodeDetails.lastUptimeProof")
+ }}
{{ formatDate(node.last_uptime_proof * 1000) }}
@@ -89,7 +110,9 @@
- {{ $t("strings.serviceNodeDetails.lastRewardBlockHeight") }}
+ {{
+ $t("strings.serviceNodeDetails.lastRewardBlockHeight")
+ }}
{{ node.last_reward_block_height }}
@@ -98,7 +121,11 @@
- {{ $t("strings.serviceNodeDetails.contributors") }}:
+ {{
+ $t("strings.serviceNodeDetails.contributors")
+ }}:
- {{ $t("strings.me") }}
- {{ contributor.name }}
- {{ contributor.address }}
+ {{ $t("strings.me") }}
+ {{
+ contributor.name
+ }}
+ {{
+ contributor.address
+ }}
- {{ $t("strings.operator") }} •
+ {{ $t("strings.operator") }} •
+
{{ $t("strings.contribution") }}:
-
+
-
+
@@ -151,7 +194,9 @@ export default {
}
},
data() {
- const menuItems = [{ action: "copyAddress", i18n: "menuItems.copyAddress" }];
+ const menuItems = [
+ { action: "copyAddress", i18n: "menuItems.copyAddress" }
+ ];
return {
isVisible: false,
@@ -180,7 +225,9 @@ export default {
for (const contributor of this.node.contributors) {
let values = { ...contributor };
- const address = address_book.find(a => a.address === contributor.address);
+ const address = address_book.find(
+ a => a.address === contributor.address
+ );
if (address) {
const { name, description } = address;
const separator = description === "" ? "" : " - ";
diff --git a/src/components/service_node/service_node_list.vue b/src/components/service_node/service_node_list.vue
index e521d04..b3cf83e 100644
--- a/src/components/service_node/service_node_list.vue
+++ b/src/components/service_node/service_node_list.vue
@@ -8,21 +8,38 @@
>
{{ $t("strings.serviceNodeDetails.snKey") }}: {{ node.service_node_pubkey }}{{ $t("strings.serviceNodeDetails.snKey") }}:
+ {{ node.service_node_pubkey }}
- {{ getRole(node) }} •
-
- • {{ $t("strings.contribution") }}:
+
+ {{ getRole(node) }} •
+
+ {{ $t("strings.contribution") }}:
+
+
+
+
+
+ {{ $t("strings.serviceNodeDetails.reserved") }} •
+
- {{ $t("strings.serviceNodeDetails.minContribution") }}: {{ getMinContribution(node) }} LOKI •
- {{ $t("strings.serviceNodeDetails.maxContribution") }}: {{ openForContributionLoki(node) }} LOKI
+ {{ $t("strings.serviceNodeDetails.minContribution") }}:
+ {{ getMinContribution(node) }} LOKI •
+ {{ $t("strings.serviceNodeDetails.maxContribution") }}:
+ {{ openForContributionLoki(node) }} LOKI
-
- {{ getFee(node) }}
+
+ {{
+ getFee(node)
+ }}
{
+ const primary = state.gateway.wallet.address_list.primary[0];
+ return (primary && primary.address) || null;
+ }
+ }),
methods: {
nodeWithMinContribution(node) {
- const nodeWithMinContribution = { ...node, minContribution: this.getMinContribution(node) };
+ const nodeWithMinContribution = {
+ ...node,
+ minContribution: this.getMinContribution(node)
+ };
return nodeWithMinContribution;
},
- openForContribution(node) {
- const openContributionRemaining =
- node.staking_requirement > node.total_reserved ? node.staking_requirement - node.total_reserved : 0;
- return openContributionRemaining;
- },
- openForContributionLoki(node) {
- return (this.openForContribution(node) / 1e9).toFixed(4);
- },
is_ready() {
return this.$store.getters["gateway/isReady"];
},
getRole(node) {
- // don't show a role if the user is not an operator or contributor
let role = "";
const opAddress = node.operator_address;
- if (node.operator_address === this.our_address) {
+ if (opAddress === this.our_address) {
role = "strings.operator";
} else if (node.ourContributionAmount && opAddress !== this.our_address) {
+ // if we're not the operator and we have a contribution amount
role = "strings.contributor";
}
return this.$t(role);
@@ -122,21 +141,12 @@ export default {
getNumContributors(node) {
return node.contributors.length;
},
- getMinContribution(node) {
- // This is calculated in the same way it is calculated on the LokiBlocks site
- const openContributionRemaining = this.openForContribution(node);
- const minContributionAtomicUnits =
- !node.funded && node.contributors.length < MAX_NUMBER_OF_CONTRIBUTORS
- ? openContributionRemaining / (MAX_NUMBER_OF_CONTRIBUTORS - node.contributors.length)
- : 0;
- const minContributionLoki = minContributionAtomicUnits / 1e9;
- // ceiling to 4 decimal places
- return minContributionLoki.toFixed(4);
- },
getFee(node) {
const operatorPortion = node.portions_for_operator;
const percentageFee = (operatorPortion / 18446744073709551612) * 100;
- return `${percentageFee.toFixed(2)}% ${this.$t("strings.transactions.fee")}`;
+ return `${percentageFee.toFixed(2)}% ${this.$t(
+ "strings.transactions.fee"
+ )}`;
},
copyKey(key) {
clipboard.writeText(key);
diff --git a/src/components/service_node/service_node_staking.vue b/src/components/service_node/service_node_staking.vue
index d943532..3709047 100644
--- a/src/components/service_node/service_node_staking.vue
+++ b/src/components/service_node/service_node_staking.vue
@@ -3,11 +3,16 @@
{{ $t("strings.serviceNodeContributionDescription") }}
- Loki {{ $t("strings.website") }}.
-
+
-
-
+
{{ $t("buttons.all") }}
+ :label="$t('buttons.min')"
+ :disable="!areButtonsEnabled()"
+ @click="service_node.amount = minStake(service_node.key)"
+ />
+
-
+
-
-
+
+
+
@@ -61,20 +98,37 @@ import { required, decimal } from "vuelidate/lib/validators";
import { service_node_key, greater_than_zero } from "src/validators/common";
import LokiField from "components/loki_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: {
LokiField,
- ServiceNodeContribute
+ ServiceNodeContribute,
+ ConfirmTransactionDialog
},
- mixins: [WalletPassword],
+ mixins: [WalletPassword, ConfirmDialogMixin, ServiceNodeMixin],
data() {
return {
service_node: {
key: "",
- amount: 0
+ 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: {
+ metadataList: [],
+ isBlink: false,
+ totalAmount: -1,
+ destination: "",
+ totalFees: 0
}
};
},
@@ -83,8 +137,9 @@ export default {
unlocked_balance: state => state.gateway.wallet.info.unlocked_balance,
info: state => state.gateway.wallet.info,
stake_status: state => state.gateway.service_node_status.stake,
- tx_status: state => state.gateway.tx_status,
+ 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"];
},
@@ -95,6 +150,49 @@ export default {
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: {
@@ -136,22 +234,44 @@ export default {
},
deep: true
},
- tx_status: {
+ sweep_all_status: {
handler(val, old) {
if (val.code == old.code) return;
- switch (this.tx_status.code) {
+ 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.tx_status.message
+ message
});
+ this.$v.$reset();
+ this.newTx = {
+ amount: 0,
+ address: "",
+ payment_id: "",
+ // blink
+ priority: 5,
+ address_book: {
+ save: false,
+ name: "",
+ description: ""
+ },
+ note: ""
+ };
break;
case -1:
this.$q.notify({
type: "negative",
timeout: 3000,
- message: this.tx_status.message
+ message
});
break;
}
@@ -170,6 +290,61 @@ export default {
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.openForContributionLoki(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 metadataList = this.confirmFields.metadataList;
+ const isBlink = this.confirmFields.isBlink;
+
+ const relayTxData = {
+ metadataList,
+ 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({
@@ -192,6 +367,16 @@ export default {
.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;
@@ -214,12 +399,15 @@ export default {
passwordDialog
.onOk(password => {
password = password || "";
- this.$store.commit("gateway/set_tx_status", {
- code: 1,
+ this.$store.commit("gateway/set_sweep_all_status", {
+ code: DO_NOTHING,
message: "Sweeping all",
sending: true
});
- const newTx = objectAssignDeep.noMutate(tx, { password });
+ const newTx = objectAssignDeep.noMutate(tx, {
+ password,
+ isSweepAll: true
+ });
this.$gateway.send("wallet", "transfer", newTx);
})
.onDismiss(() => {})
@@ -272,7 +460,7 @@ export default {
noPasswordMessage: this.$t("dialog.stake.message"),
ok: {
label: this.$t("dialog.stake.ok"),
- color: this.theme == "dark" ? "white" : "dark"
+ color: "primary"
},
dark: this.theme == "dark",
color: this.theme == "dark" ? "white" : "dark"
diff --git a/src/components/service_node/service_node_unlock.vue b/src/components/service_node/service_node_unlock.vue
index b394c21..13beccf 100644
--- a/src/components/service_node/service_node_unlock.vue
+++ b/src/components/service_node/service_node_unlock.vue
@@ -5,7 +5,9 @@
{{ $t("titles.currentlyStakedNodes") }}
-
{{ $t("strings.serviceNodeStartStakingDescription") }}
+
{{
+ $t("strings.serviceNodeStartStakingDescription")
+ }}
-
+
-
+
@@ -57,8 +66,12 @@ export default {
},
// just SNs the user has contributed to
service_nodes(state) {
- const nodes = state.gateway.daemon.service_nodes.nodes;
- const getOurContribution = node => node.contributors.find(c => c.address === this.our_address);
+ let nodes = state.gateway.daemon.service_nodes.nodes;
+ // don't count reserved nodes in my stakes (where they are a contributor of amount 0)
+ const getOurContribution = node =>
+ node.contributors.find(
+ c => c.address === this.our_address && c.amount > 0
+ );
return nodes.filter(getOurContribution).map(n => {
const ourContribution = getOurContribution(n);
return {
@@ -219,7 +232,10 @@ export default {
});
},
getRole(node) {
- const key = node.operator_address === this.our_address ? "strings.operator" : "strings.contributor";
+ const key =
+ node.operator_address === this.our_address
+ ? "strings.operator"
+ : "strings.contributor";
return this.$t(key);
},
getFee(node) {
diff --git a/src/css/app.styl b/src/css/app.styl
index 41e8b79..c859719 100644
--- a/src/css/app.styl
+++ b/src/css/app.styl
@@ -373,6 +373,35 @@ footer,
color: white;
}
}
+.confirm-tx-card {
+ color: "primary";
+ width: 450px;
+ max-width: 450x;
+
+ .confirm-list {
+ .q-item {
+ max-height: 100%;
+ margin-top: 0;
+ margin-bottom: 4px;
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ }
+
+ .label {
+ color: #cecece;
+ padding-right: 6px;
+ }
+ .address-value {
+ word-break: break-word;
+ }
+
+ .confirm-send-btn {
+ color: white;
+ background: $positive;
+ }
+}
+
.header-popover {
background: $primary;
diff --git a/src/gateway/gateway.js b/src/gateway/gateway.js
index f0c2490..3c40ec2 100644
--- a/src/gateway/gateway.js
+++ b/src/gateway/gateway.js
@@ -13,10 +13,14 @@ export class Gateway extends EventEmitter {
this.scee = new SCEE();
// Set the initial language
- let language = LocalStorage.has("language") ? LocalStorage.getItem("language") : "en-us";
+ let language = LocalStorage.has("language")
+ ? LocalStorage.getItem("language")
+ : "en-us";
this.setLanguage(language);
- let theme = LocalStorage.has("theme") ? LocalStorage.getItem("theme") : "dark";
+ let theme = LocalStorage.has("theme")
+ ? LocalStorage.getItem("theme")
+ : "dark";
this.app.store.commit("gateway/set_app_data", {
config: {
appearance: {
@@ -90,7 +94,10 @@ export class Gateway extends EventEmitter {
cancel: {
flat: true,
label: i18n.t("dialog.buttons.cancel"),
- color: this.app.store.state.gateway.app.config.appearance.theme === "dark" ? "white" : "dark"
+ color:
+ this.app.store.state.gateway.app.config.appearance.theme === "dark"
+ ? "white"
+ : "dark"
},
dark: this.app.store.state.gateway.app.config.appearance.theme === "dark"
})
@@ -111,7 +118,10 @@ export class Gateway extends EventEmitter {
method,
data
};
- let encrypted_data = this.scee.encryptString(JSON.stringify(message), this.token);
+ let encrypted_data = this.scee.encryptString(
+ JSON.stringify(message),
+ this.token
+ );
this.ws.send(encrypted_data);
}
@@ -122,7 +132,9 @@ export class Gateway extends EventEmitter {
receive(message) {
// should wrap this in a try catch, and if fail redirect to error screen
// shouldn't happen outside of dev environment
- let decrypted_data = JSON.parse(this.scee.decryptString(message, this.token));
+ let decrypted_data = JSON.parse(
+ this.scee.decryptString(message, this.token)
+ );
if (
typeof decrypted_data !== "object" ||
@@ -173,6 +185,15 @@ export class Gateway extends EventEmitter {
break;
}
+ case "set_sweep_all_status": {
+ const data = { ...decrypted_data.data };
+ if (data.i18n) {
+ data.message = this.geti18n(data.i18n);
+ }
+ this.app.store.commit("gateway/set_sweep_all_status", data);
+ break;
+ }
+
case "set_lns_status": {
const data = { ...decrypted_data.data };
if (data.i18n) {
@@ -217,7 +238,10 @@ export class Gateway extends EventEmitter {
break;
}
case "set_old_gui_import_status":
- this.app.store.commit("gateway/set_old_gui_import_status", decrypted_data.data);
+ this.app.store.commit(
+ "gateway/set_old_gui_import_status",
+ decrypted_data.data
+ );
break;
case "wallet_list":
@@ -260,7 +284,10 @@ export class Gateway extends EventEmitter {
break;
case "set_update_required":
- this.app.store.commit("gateway/set_update_required", decrypted_data.data);
+ this.app.store.commit(
+ "gateway/set_update_required",
+ decrypted_data.data
+ );
break;
}
}
diff --git a/src/i18n/en-us.js b/src/i18n/en-us.js
index 9b3f4b8..f817280 100644
--- a/src/i18n/en-us.js
+++ b/src/i18n/en-us.js
@@ -22,6 +22,8 @@ export default {
import: "IMPORT",
importWallet: "IMPORT WALLET | IMPORT WALLETS",
lns: "LOKI NAME SERVICE",
+ max: "MAX",
+ min: "MIN",
next: "NEXT",
openWallet: "OPEN WALLET",
purchase: "PURCHASE",
@@ -62,12 +64,14 @@ export default {
},
copyAddress: {
title: "Copy address",
- message: "There is a payment id associated with this address.\nBe sure to copy the payment id separately."
+ message:
+ "There is a payment id associated with this address.\nBe sure to copy the payment id separately."
},
copyPrivateKeys: {
// Copy {seedWords/viewKey/spendKey}
title: "Copy {type}",
- message: "Be careful who you send your private keys to as they control your funds.",
+ message:
+ "Be careful who you send your private keys to as they control your funds.",
seedWords: "Seed Words",
viewKey: "View Key",
spendKey: "Spend Key"
@@ -115,7 +119,8 @@ export default {
},
rescan: {
title: "Rescan wallet",
- message: "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.",
+ message:
+ "Warning: Some information about previous transactions\nsuch as the recipient's address will be lost.",
ok: "RESCAN"
},
restart: {
@@ -312,7 +317,8 @@ export default {
},
errors: {
banningPeer: "Error banning peer",
- cannotAccessRemoteNode: "Could not access remote node, please try another remote node",
+ cannotAccessRemoteNode:
+ "Could not access remote node, please try another remote node",
changingPassword: "Error changing password",
copyWalletFail: "Failed to copy wallet",
copyingPrivateKeys: "Error copying private keys",
@@ -335,8 +341,10 @@ export default {
invalidAmount: "Amount not valid",
invalidBackupOwner: "Backup owner address not valid",
invalidNameLength: "Name must be between 1 and 64 characters long",
- invalidNameFormat: "Name may only contain alphanumerics, hyphens and underscore",
- invalidNameHypenNotAllowed: "Name may only begin or end with alphanumerics or an underscore",
+ invalidNameFormat:
+ "Name may only contain alphanumerics, hyphens and underscore",
+ invalidNameHypenNotAllowed:
+ "Name may only begin or end with alphanumerics or an underscore",
invalidOldPassword: "Invalid old password",
invalidOwner: "Owner address not valid",
invalidPassword: "Invalid password",
@@ -346,7 +354,8 @@ export default {
invalidRestoreDate: "Invalid restore date",
invalidRestoreHeight: "Invalid restore height",
invalidSeedLength: "Invalid seed word length",
- invalidServiceNodeCommand: "Please enter the service node registration command",
+ invalidServiceNodeCommand:
+ "Please enter the service node registration command",
invalidServiceNodeKey: "Service node key not valid",
invalidSessionId: "Session ID not valid",
invalidWalletPath: "Invalid wallet path",
@@ -384,7 +393,8 @@ export default {
mnemonicSeed: "25 (or 24) word mnemonic seed",
pasteTransactionId: "Paste transaction ID",
pasteTransactionProof: "Paste transaction proof",
- proveOptionalMessage: "Optional message against which the signature is signed",
+ proveOptionalMessage:
+ "Optional message against which the signature is signed",
recipientWalletAddress: "Recipient's wallet address",
selectAFile: "Please select a file",
sessionId: "The Session ID to link to Loki Name Service",
@@ -442,7 +452,8 @@ export default {
},
remote: {
title: "Remote Daemon Only",
- description: "Less security, wallet will connect to a remote node to make all transactions."
+ description:
+ "Less security, wallet will connect to a remote node to make all transactions."
}
},
destinationUnknown: "Destination Unknown",
@@ -472,9 +483,11 @@ export default {
proveTransactionDescription:
"Generate a proof of your incoming/outgoing payment by supplying the transaction ID, the recipient address and an optional message.\nFor the case of outgoing payments, you can get a 'Spend Proof' that proves the authorship of a transaction. In this case, you don't need to specify the recipient address.",
readingWalletList: "Reading wallet list",
- recentIncomingTransactionsToAddress: "Recent incoming transactions to this address",
+ recentIncomingTransactionsToAddress:
+ "Recent incoming transactions to this address",
recentTransactionsWithAddress: "Recent transactions with this address",
- rescanModalDescription: "Select full rescan or rescan of spent outputs only.",
+ rescanModalDescription:
+ "Select full rescan or rescan of spent outputs only.",
saveSeedWarning: "Please copy and save these in a secure location!",
saveToAddressBook: "Save to address book",
seedWords: "Seed words",
@@ -483,8 +496,10 @@ export default {
"Staking contributes to the safety of the Loki network. For your contribution, you earn LOKI. Once staked, you will have to wait either 15 or 30 days to have your Loki unlocked, depending on if a stake was unlocked by a contributor or the node was deregistered. To learn more about staking, please visit the",
serviceNodeRegistrationDescription:
'Enter the {registerCommand} command produced by the daemon that is registering to become a Service Node using the "{prepareCommand}" command',
- serviceNodeStartStakingDescription: "To start staking, please visit the Staking tab",
- noServiceNodesCurrentlyAvailable: "There are currently no service nodes available for contribution",
+ serviceNodeStartStakingDescription:
+ "To start staking, please visit the Staking tab",
+ noServiceNodesCurrentlyAvailable:
+ "There are currently no service nodes available for contribution",
serviceNodeDetails: {
contributors: "Contributors",
lastRewardBlockHeight: "Last reward block height",
@@ -494,6 +509,7 @@ export default {
operatorFee: "Operator Fee",
registrationHeight: "Registration height",
unlockHeight: "Unlock height",
+ reserved: "Reserved",
serviceNodeKey: "Service Node Key",
snKey: "SN Key",
stakingRequirement: "Staking requirement",
@@ -535,7 +551,8 @@ export default {
userNotUsedAddress: "You have not used this address",
userUsedAddress: "You have used this address",
viewKey: "View key",
- viewOnlyMode: "View only mode. Please load full wallet in order to send coins.",
+ viewOnlyMode:
+ "View only mode. Please load full wallet in order to send coins.",
website: "website"
},
titles: {
diff --git a/src/mixins/confirm_dialog_mixin.js b/src/mixins/confirm_dialog_mixin.js
new file mode 100644
index 0000000..728e3b7
--- /dev/null
+++ b/src/mixins/confirm_dialog_mixin.js
@@ -0,0 +1,21 @@
+export default {
+ methods: {
+ buildDialogFields(val) {
+ const { feeList, amountList, destinations, metadataList, priority, isSweepAll, address } = val.txData;
+ const totalFees = feeList.reduce((a, b) => a + b, 0) / 1e9;
+ const totalAmount = amountList.reduce((a, b) => a + b, 0) / 1e9;
+ // If the tx is a sweep all, we're sending to the wallet's primary address
+ // a tx can be split, but only sent to one address
+ let destination = isSweepAll ? address : destinations[0].address;
+ const isBlink = [0, 2, 3, 4, 5].includes(priority) ? true : false;
+ const confirmFields = {
+ metadataList,
+ isBlink,
+ destination,
+ totalAmount,
+ totalFees
+ };
+ return confirmFields;
+ }
+ }
+};
diff --git a/src/mixins/service_node_mixin.js b/src/mixins/service_node_mixin.js
new file mode 100644
index 0000000..e8a1b96
--- /dev/null
+++ b/src/mixins/service_node_mixin.js
@@ -0,0 +1,27 @@
+export default {
+ methods: {
+ getMinContribution(node) {
+ const MAX_NUMBER_OF_CONTRIBUTORS = 4;
+ // This is calculated in the same way it is calculated on the LokiBlocks site
+ const openContributionRemaining = this.openForContribution(node);
+ const minContributionAtomicUnits =
+ !node.funded && node.contributors.length < MAX_NUMBER_OF_CONTRIBUTORS
+ ? openContributionRemaining /
+ (MAX_NUMBER_OF_CONTRIBUTORS - node.contributors.length)
+ : 0;
+ const minContributionLoki = minContributionAtomicUnits / 1e9;
+ // ceiling to 4 decimal places
+ return minContributionLoki.toFixed(4);
+ },
+ openForContribution(node) {
+ const openContributionRemaining =
+ node.staking_requirement > node.total_reserved
+ ? node.staking_requirement - node.total_reserved
+ : 0;
+ return openContributionRemaining;
+ },
+ openForContributionLoki(node) {
+ return (this.openForContribution(node) / 1e9).toFixed(4);
+ }
+ }
+};
diff --git a/src/pages/wallet-select/create.vue b/src/pages/wallet-select/create.vue
index 22f648d..58ece8d 100644
--- a/src/pages/wallet-select/create.vue
+++ b/src/pages/wallet-select/create.vue
@@ -1,7 +1,10 @@
-
+
-
+
@@ -79,7 +87,7 @@ export default {
return {
wallet: {
name: "",
- language: languageOptions[0],
+ language: languageOptions[0].value,
password: "",
password_confirm: ""
},
diff --git a/src/pages/wallet/send.vue b/src/pages/wallet/send.vue
index b39ec2d..9aa59c8 100644
--- a/src/pages/wallet/send.vue
+++ b/src/pages/wallet/send.vue
@@ -10,7 +10,10 @@
-
+
-
+
-
+
{{ $t("buttons.contacts") }}
@@ -67,7 +77,11 @@
-
+
-
-
-
- {{ $t("dialog.confirmTransaction.title") }}
-
-
-
-
- {{ $t("dialog.confirmTransaction.sendTo") }}:
-
- {{ confirmFields.destination }}
-
-
-
{{ $t("strings.transactions.amount") }}:
- {{ confirmFields.totalAmount }} Loki
-
-
{{ $t("strings.transactions.fee") }}: {{ confirmFields.totalFees }} Loki
-
-
{{ $t("dialog.confirmTransaction.priority") }}:
- {{ confirmFields.translatedBlinkOrSlow }}
-
-
-
-
-
-
-
-
+
@@ -186,6 +175,8 @@ import { required, decimal } from "vuelidate/lib/validators";
import { payment_id, address, greater_than_zero } from "src/validators/common";
import LokiField from "components/loki_field";
import WalletPassword from "src/mixins/wallet_password";
+import ConfirmDialogMixin from "src/mixins/confirm_dialog_mixin";
+import ConfirmTransactionDialog from "components/confirm_tx_dialog";
const objectAssignDeep = require("object-assign-deep");
// the case for doing nothing on a tx_status update
@@ -193,9 +184,10 @@ const DO_NOTHING = 10;
export default {
components: {
- LokiField
+ LokiField,
+ ConfirmTransactionDialog
},
- mixins: [WalletPassword],
+ mixins: [WalletPassword, ConfirmDialogMixin],
data() {
let priorityOptions = [
{ label: this.$t("strings.priorityOptions.blink"), value: 5 }, // Blink
@@ -214,8 +206,13 @@ export default {
}
},
priorityOptions: priorityOptions,
- confirmTransaction: false,
- confirmFields: {}
+ confirmFields: {
+ metadataList: [],
+ isBlink: false,
+ totalAmount: -1,
+ destination: "",
+ totalFees: 0
+ }
};
},
computed: mapState({
@@ -233,7 +230,8 @@ export default {
const wallet = state.gateway.wallet.info;
const prefix = (wallet && wallet.address && wallet.address[0]) || "L";
return `${prefix}..`;
- }
+ },
+ confirmTransaction: state => state.gateway.tx_status.code === 1
}),
validations: {
newTx: {
@@ -268,7 +266,7 @@ export default {
case DO_NOTHING:
break;
case 1:
- this.buildDialogFields(val);
+ this.buildDialogFieldsSend(val);
break;
case 0:
this.$q.notify({
@@ -308,7 +306,10 @@ export default {
}
},
mounted() {
- if (this.$route.path == "/wallet/send" && this.$route.query.hasOwnProperty("address")) {
+ if (
+ this.$route.path == "/wallet/send" &&
+ this.$route.query.hasOwnProperty("address")
+ ) {
this.autoFill(this.$route.query);
}
},
@@ -317,25 +318,9 @@ export default {
this.newTx.address = info.address;
this.newTx.payment_id = info.payment_id;
},
- buildDialogFields(val) {
- this.confirmTransaction = true;
- const { feeList, amountList, destinations, metadataList, priority } = val.txData;
- const totalFees = feeList.reduce((a, b) => a + b, 0) / 1e9;
- const totalAmount = amountList.reduce((a, b) => a + b, 0) / 1e9;
- // a tx can be split, but only sent to one address
- const destination = destinations[0].address;
-
- const isBlink = [0, 2, 3, 4, 5].includes(priority) ? true : false;
- const blinkOrSlow = isBlink ? "strings.priorityOptions.blink" : "strings.priorityOptions.slow";
- const translatedBlinkOrSlow = this.$t(blinkOrSlow);
- this.confirmFields = {
- metadataList,
- isBlink,
- translatedBlinkOrSlow,
- destination,
- totalAmount,
- totalFees
- };
+ buildDialogFieldsSend(txData) {
+ // build using mixin method
+ this.confirmFields = this.buildDialogFields(txData);
},
onConfirmTransaction() {
// put the loading spinner up
@@ -366,9 +351,16 @@ export default {
note
};
+ // Commit the transaction
this.$gateway.send("wallet", "relay_tx", relayTxData);
},
- // helper for constructing a dialog for confirming transactions
+ onCancelTransaction() {
+ this.$store.commit("gateway/set_tx_status", {
+ code: DO_NOTHING,
+ message: "Cancel the transaction from confirm dialog",
+ sending: false
+ });
+ },
async send() {
this.$v.newTx.$touch();
@@ -454,35 +446,6 @@ export default {