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

4
src/store/index.js

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