Browse Source

lightning wallet restructuring

readme
Mayank 5 years ago
parent
commit
adf0fd3626
  1. 249
      src/components/LightningWallet.vue
  2. 4
      src/store/index.js
  3. 323
      src/store/modules/lightning.js

249
src/components/LightningWallet.vue

@ -8,7 +8,7 @@
suffix: '',
prefix: ''
}"
:sub-title="walletUnit"
sub-title="Sats"
icon="icon-app-lightning.svg"
:loading="state.loading"
>
@ -39,7 +39,7 @@
<b-list-group class="pb-2">
<!-- Transaction -->
<b-list-group-item
v-for="tx in state.txs"
v-for="tx in transactions.slice(0,3)"
:key="tx.description"
class="tx flex-column align-items-start px-4"
>
@ -376,10 +376,11 @@
</template>
<script>
import axios from "axios";
import QrcodeVue from "qrcode.vue";
import moment from "moment";
import { mapState } from "vuex";
import API from "@/helpers/api";
import CardWidget from "@/components/CardWidget";
import InputCopy from "@/components/InputCopy";
@ -433,12 +434,10 @@ export default {
},
props: {},
computed: {
walletBalance() {
return this.$store.state.wallet.balance.offChain;
},
walletUnit() {
return this.$store.getters.getWalletUnit;
},
...mapState({
transactions: state => state.lightning.transactions,
walletBalance: state => state.lightning.balance.confirmed
}),
isLightningPage() {
return this.$router.currentRoute.path === "/lightning";
}
@ -457,8 +456,9 @@ export default {
reset() {
//reset to default mode, clear any inputs/generated invoice, pasted invoice, etc - used by "Back" button
//refresh txs
this.fetchRecentTxs();
//refresh data
this.$store.dispatch("lightning/getTransactions");
this.$store.dispatch("lightning/getBalance");
//reset state
this.state.receive = {
@ -479,7 +479,7 @@ export default {
this.state.error = "";
this.state.mode = "balance";
},
sendSats() {
async sendSats() {
//broadcast tx
if (!this.state.send.isValidInvoice) return; //check if the invoice user pasted is valid
@ -487,35 +487,36 @@ export default {
this.state.send.isSending = true;
this.state.error = "";
axios({
method: "post",
url: "v1/lnd/lightning/payInvoice",
data: {
amt: 0, //because payment request already has amount info
paymentRequest: this.state.send.invoiceText
const payload = {
amt: 0, //because payment request already has amount info
paymentRequest: this.state.send.invoiceText
};
try {
const res = await API.post(`v1/lnd/lightning/payInvoice`, payload);
if (res.data.paymentError) {
return (this.state.error = res.data.paymentError);
}
})
.then(res => {
console.log(res);
if (res.data.paymentError) {
return (this.state.error = res.data.paymentError);
}
this.state.mode = "sent";
})
.catch(error => {
this.state.error = error.response.data;
})
.finally(() => {
this.state.loading = false;
this.state.send.isSending = false;
});
this.state.mode = "sent";
//refresh
this.$store.dispatch("lightning/getTransactions");
this.$store.dispatch("lightning/getBalance");
} catch (error) {
this.state.error = JSON.stringify(error.response)
? error.response.data
: "Error sending payment";
}
this.state.loading = false;
this.state.send.isSending = false;
//slight delay in updating the balance so the checkmark's animation completes first
// window.setTimeout(() => {
// this.$store.commit("updateWalletBalance", this.walletBalance - 1000);
// }, 4000);
},
createInvoice() {
async createInvoice() {
//generate invoice to receive payment
this.state.loading = true;
this.state.receive.isGeneratingInvoice = true;
@ -526,27 +527,28 @@ export default {
this.state.receive.invoiceQR = `${this.state.receive.invoiceQR}2345`;
}, 200);
axios({
method: "post",
url: "v1/lnd/lightning/addInvoice",
data: {
amt: this.state.receive.amount,
memo: this.state.receive.description
}
})
.then(res => {
this.state.receive.invoiceQR = this.state.receive.invoiceText =
res.data.paymentRequest;
})
.catch(error => {
console.log(error);
alert(error);
})
.finally(() => {
this.state.loading = false;
this.state.receive.isGeneratingInvoice = false;
window.clearInterval(QRAnimation);
});
const payload = {
amt: this.state.receive.amount,
memo: this.state.receive.description
};
try {
const res = await API.post(`v1/lnd/lightning/addInvoice`, payload);
this.state.receive.invoiceQR = this.state.receive.invoiceText =
res.data.paymentRequest;
//refresh
this.$store.dispatch("lightning/getTransactions");
} catch (error) {
this.state.mode = "receive";
this.state.error = JSON.stringify(error.response)
? error.response.data
: "Error creating invoice";
}
this.state.loading = false;
this.state.receive.isGeneratingInvoice = false;
window.clearInterval(QRAnimation);
// window.setTimeout(() => {
// this.state.loading = false;
@ -556,7 +558,7 @@ export default {
// window.clearInterval(QRAnimation);
// }, 3000);
},
fetchInvoiceDetails() {
async fetchInvoiceDetails() {
//fetch invoice details as pasted by user in the "Send" mode/screen
//if empty field, reset last fetched invoice
if (!this.state.send.invoiceText) {
@ -569,117 +571,50 @@ export default {
return;
}
this.state.loading = true;
this.state.send.description = "";
this.state.send.isValidInvoice = false;
this.state.send.amount = null;
this.state.send.description = "";
this.state.error = "";
this.state.loading = true;
axios
.get(
`v1/lnd/lightning/invoice?paymentRequest=${this.state.send.invoiceText}`
)
.then(res => {
//check if invoice is expired
const now = Math.floor(new Date().getTime());
const invoiceExpiresOn =
(Number(res.data.timestamp) + Number(res.data.expiry)) * 1000;
if (now > invoiceExpiresOn) {
this.state.send.isValidInvoice = false;
this.state.error = `Invoice expired ${moment(
invoiceExpiresOn
).fromNow()}`;
return;
}
const fetchedInvoice = await API.get(
`v1/lnd/lightning/invoice?paymentRequest=${this.state.send.invoiceText}`
);
this.state.send.amount = Number(res.data.numSatoshis);
this.state.send.description = res.data.description;
this.state.send.isValidInvoice = true;
this.state.error = "";
})
.catch(error => {
this.state.send.isValidInvoice = false;
this.state.error = "Invalid invoice";
console.log(error);
})
.finally(() => {
this.state.loading = false;
});
},
async fetchRecentTxs() {
//Get List of transactions
let transactions = [];
// Get incoming txs
const invoices = await API.get(`v1/lnd/lightning/invoices`);
const latestInvoices = invoices.slice(0, 3).map(tx => {
let type = "incoming";
if (tx.state === "CANCELED") {
type = "expired";
} else if (tx.state === "OPEN") {
type = "pending";
}
return {
type,
amount: Number(tx.value),
timestamp: new Date(Number(tx.creationDate) * 1000),
description: tx.memo || "Direct payment from a node",
expiresOn: new Date(
(Number(tx.creationDate) + Number(tx.expiry)) * 1000
)
};
});
transactions = [...transactions, ...latestInvoices];
// Get outgoing txs
const payments = await API.get(`v1/lnd/lightning/payments`);
const latestPayments = payments.slice(0, 3).map(tx => {
return {
type: "outgoing",
amount: Number(tx.value),
timestamp: new Date(Number(tx.creationDate) * 1000),
description: tx.paymentRequest //temporarily store payment request in the description as we'll replace it by memo
};
});
transactions = [...transactions, ...latestPayments];
//Sort by recent to oldest
transactions.sort(function(tx1, tx2) {
return tx2.timestamp - tx1.timestamp;
});
//trim to top 3
transactions = transactions.slice(0, 3);
// Fetch descriptions of all outgoing payments
for (let tx of transactions) {
if (tx.type !== "outgoing") continue;
if (!tx.description) {
//example in case of a keysend tx
tx.description = "Direct payment to a node";
continue;
}
if (!fetchedInvoice) {
this.state.send.isValidInvoice = false;
this.state.error = "Invalid invoice";
this.state.loading = false;
return;
}
try {
const invoiceDetails = await axios.get(
`v1/lnd/lightning/invoice?paymentRequest=${tx.description}`
);
tx.description = invoiceDetails.data.description;
} catch (error) {
alert(error);
tx.description = "";
}
//check if invoice is expired
const now = Math.floor(new Date().getTime());
const invoiceExpiresOn =
(Number(fetchedInvoice.timestamp) + Number(fetchedInvoice.expiry)) *
1000;
if (now > invoiceExpiresOn) {
this.state.send.isValidInvoice = false;
this.state.error = `Invoice expired ${moment(
invoiceExpiresOn
).fromNow()}`;
} else {
this.state.send.amount = Number(fetchedInvoice.numSatoshis);
this.state.send.description = fetchedInvoice.description;
this.state.send.isValidInvoice = true;
this.state.error = "";
}
this.state.txs = transactions;
this.state.loading = false;
}
},
watch: {},
created() {
this.fetchRecentTxs();
async created() {
await this.$store.dispatch("lightning/getStatus");
this.$store.dispatch("lightning/getTransactions");
this.$store.dispatch("lightning/getBalance");
},
components: {
CardWidget,

4
src/store/index.js

@ -4,6 +4,7 @@ import axios from "axios";
//Modules
import bitcoin from './modules/bitcoin';
import lightning from './modules/lightning';
Vue.use(Vuex);
@ -160,6 +161,7 @@ export default new Vuex.Store({
actions,
getters,
modules: {
bitcoin
bitcoin,
lightning
}
});

323
src/store/modules/lightning.js

@ -0,0 +1,323 @@
import API from '@/helpers/api';
// import Events from '~/helpers/events';
// import { sleep } from '@/helpers/utils';
// Helper function to sort lightning transactions by date
// function sortTransactions(a, b) {
// if (a.creationDate > b.creationDate) {
// return -1;
// }
// if (a.creationDate < b.creationDate) {
// return 1;
// }
// return 0;
// }
// Initial state
const state = () => ({
operational: false,
unlocked: false,
currentBlock: 0,
blockHeight: 0,
balance: {
total: 0,
confirmed: 0,
pending: 0,
},
channels: [],
connectionCode: 'unknown',
maxSend: 0,
maxReceive: 0,
transactions: [],
confirmedTransactions: [],
pendingTransactions: [],
pendingChannelEdit: {},
pubkey: '',
})
// Functions to update the state directly
const mutations = {
isOperational(state, operational) {
state.operational = operational;
},
isUnlocked(state, unlocked) {
state.unlocked = unlocked;
},
setConnectionCode(state, code) {
state.connectionCode = code;
},
setChannels(state, channels) {
state.channels = channels;
},
setChannelFocus(state, channel) {
state.pendingChannelEdit = channel;
},
setBalance(state, balance) {
if (balance.confirmed !== undefined) {
state.balance.confirmed = parseInt(balance.confirmed);
}
if (balance.pending !== undefined) {
state.balance.pending = parseInt(balance.pending);
}
state.balance.total = state.balance.confirmed;
},
setMaxReceive(state, maxReceive) {
state.maxReceive = maxReceive;
},
setMaxSend(state, maxSend) {
state.maxSend = maxSend;
},
setTransactions(state, transactions) {
state.transactions = transactions;
},
setConfirmedTransactions(state, confirmedTransactions) {
state.confirmedTransactions = confirmedTransactions;
},
setPendingTransactions(state, pendingTransactions) {
state.pendingTransactions = pendingTransactions;
},
setPubKey(state, pubkey) {
state.pubkey = pubkey;
},
};
// Functions to get data from the API
const actions = {
async getStatus({ commit }) {
const status = await API.get(`v1/lnd/info/status`);
commit('isOperational', status.operational);
commit('isUnlocked', status.unlocked);
// launch unlock modal after 30 sec
// if (!status.unlocked) {
// await sleep(30000);
// const { unlocked } = await API.get(`v1/lnd/info/status`);
// commit('isUnlocked', unlocked);
// if (!unlocked) {
// Events.$emit('unlock-modal-open');
// }
// }
},
async getLndPageData({ commit }) {
const lightning = await API.get(`v1/pages/lnd`);
if (lightning) {
const lightningInfo = lightning.lightningInfo;
commit('setPubKey', lightningInfo.identityPubkey);
}
},
async getConnectionCode({ commit }) {
const uris = await API.get(`v1/lnd/info/uris`);
if (uris && uris.length > 0) {
commit('setConnectionCode', uris[0]);
} else {
commit('setConnectionCode', 'Could not determine lnd connection code');
}
},
// Deprecated, this endpoint returns balance data minus estimated channel closing fees
// These estimates have caused many customers to be confused by the numbers displayed in the dashboard (leaky sats)
// Instead we can calculate our total balance by getting the sum of each channel's localBalance
async getBalance({ commit, state }) {
if (state.operational && state.unlocked) {
const balance = await API.get(`v1/lnd/wallet/lightning`);
if (balance) {
commit('setBalance', { confirmed: balance.balance });
}
}
},
async getChannels({ commit, state }) {
if (state.operational && state.unlocked) {
const rawChannels = await API.get(`v1/lnd/channel`);
const channels = [];
let confirmedBalance = 0;
let pendingBalance = 0;
let maxReceive = 0;
let maxSend = 0;
if (rawChannels) {
// Loop through channels to determine pending balance, max payment amount, and sort channels by type
rawChannels.forEach((channel) => {
const localBalance = parseInt(channel.localBalance) || 0;
const remoteBalance = parseInt(channel.remoteBalance) || 0;
if (channel.type === 'OPEN') {
if (channel.active) {
channel.status = 'online';
} else {
channel.status = 'offline';
}
if (remoteBalance > maxReceive) {
maxReceive = remoteBalance;
}
if (localBalance > maxSend) {
maxSend = localBalance;
}
confirmedBalance += localBalance;
} else if (channel.type === 'PENDING_OPEN_CHANNEL') {
pendingBalance += localBalance;
channel.status = 'opening';
} else if (['WAITING_CLOSING_CHANNEL', 'FORCE_CLOSING_CHANNEL', 'PENDING_CLOSING_CHANNEL'].indexOf(channel.type) > -1) {
pendingBalance += localBalance;
channel.status = 'closing';
// Lnd doesn't provide initiator or autopilot data via rpc. So, we just display a generic closing message.
channel.name = 'Closing Channel';
channel.purpose = 'A channel that is in the process of closing';
} else {
channel.status = 'unknown';
}
if (channel.name === '' && !channel.initiator) {
channel.name = 'Inbound Channel';
channel.purpose = 'A channel that another node has opened to you';
}
// Set placeholder values if autopilot
if (channel.managed === false && channel.initiator) {
channel.name = 'Autopilot';
channel.purpose = 'Managed by autopilot';
}
channels.push(channel);
});
commit('setChannels', channels);
commit('setBalance', { confirmed: confirmedBalance, pending: pendingBalance });
commit('setMaxReceive', maxReceive);
commit('setMaxSend', maxSend);
}
}
},
async getTransactions({ commit, state }) {
if (state.operational && state.unlocked) {
// Get invoices and payments
const invoices = await API.get(`v1/lnd/lightning/invoices`);
const payments = await API.get(`v1/lnd/lightning/payments`);
let transactions = [];
if (invoices) {
const incomingTransactions = invoices.map(tx => {
let type = "incoming";
if (tx.state === "CANCELED") {
type = "expired";
} else if (tx.state === "OPEN") {
type = "pending";
}
return {
type,
amount: Number(tx.value),
timestamp: new Date(Number(tx.creationDate) * 1000),
description: tx.memo || "Direct payment from a node",
expiresOn: new Date(
(Number(tx.creationDate) + Number(tx.expiry)) * 1000
)
};
});
transactions = [...transactions, ...incomingTransactions];
}
if (payments) {
const outgoingTransactions = payments.slice(0, 3).map(tx => {
return {
type: "outgoing",
amount: Number(tx.value),
timestamp: new Date(Number(tx.creationDate) * 1000),
description: tx.paymentRequest //temporarily store payment request in the description as we'll replace it by memo
};
});
transactions = [...transactions, ...outgoingTransactions];
}
//Sort by recent to oldest
transactions.sort(function (tx1, tx2) {
return tx2.timestamp - tx1.timestamp;
});
// Fetch descriptions of all outgoing payments
for (let tx of transactions) {
if (tx.type !== "outgoing") continue;
if (!tx.description) {
//example - in case of a keysend tx
tx.description = "Direct payment to a node";
continue;
} else {
try {
const invoiceDetails = await API.get(
`v1/lnd/lightning/invoice?paymentRequest=${tx.description}`
);
tx.description = invoiceDetails.description;
} catch (error) {
console.log(error);
tx.description = "";
}
}
}
commit('setTransactions', transactions);
}
},
selectChannel({ commit }, channel) {
commit('setChannelFocus', channel);
}
};
const getters = {
status(state) {
const data = {
class: 'loading',
text: 'Loading...',
};
if (state.operational) {
if (state.unlocked) {
data.class = 'active';
data.text = 'Active';
} else {
data.class = 'inactive';
data.text = 'Locked';
}
}
return data;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
Loading…
Cancel
Save