Browse Source

bitcoin wallet structuring complete

readme
Mayank 5 years ago
parent
commit
29a7833a9f
  1. 3
      package.json
  2. 162
      src/components/BitcoinWallet.vue
  3. 46
      src/helpers/units.js
  4. 7
      src/store/index.js
  5. 309
      src/store/modules/bitcoin.js
  6. 0
      src/store/modules/lightning.js
  7. 0
      src/store/modules/onboarding.js
  8. 14
      src/views/Dashboard.vue
  9. 5
      yarn.lock

3
package.json

@ -12,6 +12,7 @@
"dependencies": {
"animate.css": "^3.7.2",
"axios": "^0.19.2",
"bignumber.js": "^9.0.0",
"bootstrap-vue": "^2.11.0",
"core-js": "^3.4.4",
"countup.js": "^2.0.4",
@ -59,4 +60,4 @@
"> 1%",
"last 2 versions"
]
}
}

162
src/components/BitcoinWallet.vue

@ -8,7 +8,7 @@
suffix: '',
prefix: ''
}"
:sub-title="walletUnit"
sub-title="Sats"
icon="icon-app-bitcoin.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.txHash"
class="tx flex-column align-items-start px-4"
>
@ -216,13 +216,13 @@
</div>
<div class="d-flex justify-content-between pb-3">
<span class="text-muted">
<b>{{ state.withdraw.fees.fast.total }}</b>
<b>{{ fees.fast.total }}</b>
<small>&nbsp;Sats</small>
<br />
<small>Mining fee</small>
</span>
<span class="text-right text-muted">
<b>{{ walletBalance - state.withdraw.amount - state.withdraw.fees.fast.total }}</b>
<b>{{ walletBalance - state.withdraw.amount - fees.fast.total }}</b>
<small>&nbsp;Sats</small>
<br />
<small>Remaining balance</small>
@ -279,12 +279,12 @@
<div class="generated-qr mb-3">
<!-- Popup umbrel logo in the middle of QR code after the QR is generated -->
<transition name="qr-logo-popup">
<img v-show="state.deposit.address" src="@/assets/umbrel-qr-icon.svg" class="qr-logo" />
<img v-show="depositAddress" src="@/assets/umbrel-qr-icon.svg" class="qr-logo" />
</transition>
<!-- QR Code element -->
<qrcode-vue
:value="state.deposit.address"
:value="depositAddress"
:size="200"
level="H"
renderAs="svg"
@ -293,7 +293,7 @@
</div>
<!-- Copy Address Input Field -->
<input-copy size="sm" :value="state.deposit.address" class="mb-4 mt-2"></input-copy>
<input-copy size="sm" :value="depositAddress" class="mb-4 mt-2"></input-copy>
</div>
</transition>
@ -402,6 +402,7 @@
<script>
import QrcodeVue from "qrcode.vue";
import moment from "moment";
import { mapState, mapGetters } from "vuex";
import API from "@/helpers/api";
@ -413,7 +414,7 @@ export default {
return {
state: {
//balance: 162500, //net user's balance in sats
mode: "balance", //balance (default mode), deposit, withdraw, confirm-withdraw, withdraw
mode: "balance", //balance (default mode), deposit, withdraw, review-withdraw, withdrawn
txs: [
//array of last 3 txs
{
@ -443,32 +444,7 @@ export default {
feesTimeout: null,
isTyping: false, //to disable button when the user changes amount/address
isWithdrawing: false,
txHash: "",
fees: {
fast: {
total: "--",
perByte: "--",
error: false
},
normal: {
total: "--",
perByte: "--",
error: false
},
slow: {
total: "--",
perByte: "--",
error: false
},
cheapest: {
total: "--",
perByte: "--",
error: false
}
}
},
deposit: {
address: ""
txHash: ""
},
loading: false, //overall state of the wallet, used to toggle progress bar on top of the card,
error: "" //used to show any error occured, eg. invalid amount, enter more than 0 sats, invoice expired, etc
@ -477,12 +453,14 @@ export default {
},
props: {},
computed: {
walletBalance() {
return this.$store.state.wallet.balance.onChain;
},
walletUnit() {
return this.$store.getters.getWalletUnit;
},
...mapState({
walletBalance: state => state.bitcoin.balance.total,
depositAddress: state => state.bitcoin.depositAddress,
fees: state => state.bitcoin.fees
}),
...mapGetters({
transactions: "bitcoin/transactions"
}),
isBitcoinPage() {
return this.$router.currentRoute.path === "/bitcoin";
}
@ -499,8 +477,7 @@ export default {
//on deposit mode, get new btc address
if (mode === "deposit") {
const depositAddress = await API.get(`v1/lnd/address`);
this.state.deposit.address = depositAddress.address;
await this.$store.dispatch("bitcoin/getDepositAddress");
}
return (this.state.mode = mode);
@ -508,8 +485,12 @@ export default {
reset() {
//reset to default mode, clear any inputs/generated invoice, pasted invoice, etc - used by "Back" button
//refresh txs
this.fetchRecentTxs();
//to do: refresh balance, txs
//in case going back from review withdrawal to edit withdrwal
if (this.state.mode === "review-withdraw") {
return (this.state.mode = "withdraw");
}
//reset state
this.state.withdraw = {
@ -519,72 +500,13 @@ export default {
feesTimeout: null,
isTyping: false, //to disable button when the user changes amount/address
isWithdrawing: false,
txHash: "",
fees: {
fast: {
total: "--",
perByte: "--",
error: false
},
normal: {
total: "--",
perByte: "--",
error: false
},
slow: {
total: "--",
perByte: "--",
error: false
},
cheapest: {
total: "--",
perByte: "--",
error: false
}
}
};
this.state.deposit = {
address: ""
txHash: ""
};
this.state.loading = false;
this.state.error = "";
this.state.mode = "balance";
},
async fetchRecentTxs() {
//Get List of transactions
const txs = await API.get(`v1/lnd/transaction`);
this.state.txs = txs.slice(0, 3).map(tx => {
const amount = Number(tx.amount);
let type = "incoming";
if (amount < 0) {
type = "outgoing";
}
let description = "Unknown";
if (tx.type === "CHANNEL_OPEN" || tx.type === "PENDING_OPEN") {
description = "Lightning Wallet";
} else if (tx.type === "CHANNEL_CLOSE" || tx.type === "PENDING_CLOSE") {
description = "Lightning Wallet";
} else if (tx.type === "ON_CHAIN_TRANSACTION_SENT") {
description = "Withdrawal";
} else if (tx.type === "ON_CHAIN_TRANSACTION_RECEIVED") {
description = "Deposit";
}
return {
type,
amount: amount < 0 ? amount * -1 : amount, //for formatting +/- in view
timestamp: new Date(Number(tx.timeStamp) * 1000),
description
};
});
},
async fetchWithdrawalFees() {
if (this.state.withdraw.feesTimeout) {
clearTimeout(this.state.withdraw.feesTimeout);
@ -605,30 +527,12 @@ export default {
params.amt = this.state.withdraw.amount;
}
const fees = await API.get(`v1/lnd/transaction/estimateFee`, {
params
});
if (fees) {
for (const [speed, estimate] of Object.entries(fees)) {
// If the API returned an error message
if (estimate.code) {
this.state.withdraw.fees[speed].total = "N/A";
this.state.withdraw.fees[speed].perByte = "N/A";
this.state.withdraw.fees[speed].error = estimate.code;
} else {
this.state.withdraw.fees[speed].total = estimate.feeSat;
this.state.withdraw.fees[speed].perByte =
estimate.feerateSatPerByte;
this.state.withdraw.fees[speed].sweepAmount =
estimate.sweepAmount;
this.state.withdraw.fees[speed].error = false;
}
}
await this.$store.dispatch("bitcoin/getFees", params);
if (this.fees) {
//show error if any
if (fees.fast && fees.fast.code) {
this.state.error = fees.fast.text;
if (this.fees.fast && this.fees.fast.error.code) {
this.state.error = this.fees.fast.error.text;
} else {
this.state.error = "";
}
@ -670,8 +574,10 @@ export default {
}
},
watch: {},
created() {
this.fetchRecentTxs();
async created() {
await this.$store.dispatch("bitcoin/getStatus");
this.$store.dispatch("bitcoin/getBalance");
this.$store.dispatch("bitcoin/getTransactions");
},
components: {
CardWidget,

46
src/helpers/units.js

@ -0,0 +1,46 @@
import { BigNumber } from 'bignumber.js';
// Never display numbers as exponents
BigNumber.config({ EXPONENTIAL_AT: 1e+9 });
export function btcToSats(input) {
const btc = new BigNumber(input);
const sats = btc.multipliedBy(100000000);
if (isNaN(sats)) {
return 0;
}
return Number(sats);
}
export function satsToBtc(input, decimals = 8) {
const sats = new BigNumber(input);
const btc = sats.dividedBy(100000000);
if (isNaN(btc)) {
return 0;
}
return Number(btc.decimalPlaces(decimals));
}
export function formatSats(input) {
const sats = new BigNumber(input);
if (isNaN(sats)) {
return 0;
}
return Number(sats.toFormat(0));
}
export function toPrecision(input, decimals = 8) {
const number = new BigNumber(input);
if (isNaN(number)) {
return 0;
}
return Number(number.decimalPlaces(decimals));
}

7
src/store/index.js

@ -2,6 +2,9 @@ import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
//Modules
import bitcoin from './modules/bitcoin';
Vue.use(Vuex);
@ -156,5 +159,7 @@ export default new Vuex.Store({
mutations,
actions,
getters,
modules: {}
modules: {
bitcoin
}
});

309
src/store/modules/bitcoin.js

@ -0,0 +1,309 @@
import API from '@/helpers/api';
import { toPrecision } from '@/helpers/units';
// Initial state
const state = () => ({
operational: false,
calibrating: false,
ipAddress: '',
onionAddress: '',
currentBlock: 0,
blockHeight: 0,
percent: 0,
depositAddress: '',
peers: {
total: 0,
inbound: 0,
outbound: 0,
},
balance: {
total: 0,
confirmed: 0,
pending: 0,
pendingIn: 0,
pendingOut: 0,
},
transactions: [],
pending: [],
price: 0,
fees: {
fast: {
total: "--",
perByte: "--",
error: {
code: "",
text: ""
}
},
normal: {
total: "--",
perByte: "--",
error: {
code: "",
text: ""
}
},
slow: {
total: "--",
perByte: "--",
error: {
code: "",
text: ""
}
},
cheapest: {
total: "--",
perByte: "--",
error: {
code: "",
text: ""
}
}
}
})
// Functions to update the state directly
const mutations = {
isOperational(state, operational) {
state.operational = operational;
},
ipAddress(state, address) {
state.ipAddress = address;
},
onionAddress(state, address) {
state.onionAddress = address;
},
syncStatus(state, sync) {
state.percent = toPrecision(parseFloat(sync.percent) * 100, 2);
state.currentBlock = sync.currentBlock;
state.blockHeight = sync.headerCount;
if (sync.status === 'calibrating') {
state.calibrating = true;
} else {
state.calibrating = false;
}
},
peers(state, peers) {
state.peers.total = peers.total || 0;
state.peers.inbound = peers.inbound || 0;
state.peers.outbound = peers.outbound || 0;
},
balance(state, balance) {
state.balance.total = parseInt(balance.totalBalance);
state.balance.confirmed = parseInt(balance.confirmedBalance);
state.balance.pending = parseInt(balance.unconfirmedBalance);
},
transactions(state, transactions) {
// Clear previously loaded data
state.transactions = [];
// state.pending = [];
// Loop through transactions and sort them by type
// transactions.forEach((transaction) => {
// // Only display Bitcoin transactions
// if (transaction.type === 'ON_CHAIN_TRANSACTION_SENT' || transaction.type === 'ON_CHAIN_TRANSACTION_RECEIVED') {
// if (transaction.numConfirmations > 0) {
// state.transactions.push(transaction);
// } else {
// state.pending.push(transaction);
// }
// }
// });
state.transactions = transactions;
},
depositAddress(state, address) {
state.depositAddress = address;
},
fees(state, fees) {
for (const [speed, estimate] of Object.entries(fees)) {
// If the API returned an error message
if (estimate.code) {
state.fees[speed].total = "N/A";
state.fees[speed].perByte = "N/A";
state.fees[speed].error = {
code: estimate.code,
text: estimate.text
};
} else {
state.fees[speed].total = estimate.feeSat;
state.fees[speed].perByte =
estimate.feerateSatPerByte;
state.fees[speed].sweepAmount =
estimate.sweepAmount;
state.fees[speed].error = false;
}
}
},
price(state, usd) {
state.price = usd;
},
};
// Functions to get data from the API
const actions = {
async getStatus({ commit, dispatch }) {
const status = await API.get(`v1/bitcoind/info/status`);
if (status) {
commit('isOperational', status.operational);
if (status.operational) {
dispatch('getSync');
}
}
},
async getAddresses({ commit, state }) {
// We can only make this request when bitcoind is operational
if (state.operational) {
const addresses = await API.get(`v1/bitcoind/info/addresses`);
// Default onion address to not found.
commit('onionAddress', 'Could not determine bitcoin onion address');
if (addresses) {
addresses.forEach(address => {
if (address.includes('.onion')) {
commit('onionAddress', address);
} else {
commit('ipAddress', address);
}
});
}
}
},
async getSync({ commit, state }) {
if (state.operational) {
const sync = await API.get(`v1/bitcoind/info/sync`);
if (sync) {
commit('syncStatus', sync);
}
}
},
async getPeers({ commit, state }) {
if (state.operational) {
const peers = await API.get(`v1/bitcoind/info/connections`);
if (peers) {
commit('peers', peers);
}
}
},
async getBalance({ commit, state }) {
if (state.operational) {
const balance = await API.get(`v1/lnd/wallet/btc`);
if (balance) {
commit('balance', balance);
}
}
},
async getTransactions({ commit, state }) {
if (state.operational) {
const transactions = await API.get(`v1/lnd/transaction`);
commit('transactions', transactions);
}
},
async getPrice({ commit }) {
// Todo: Cache this value on the node instead of making a 3rd party request
const price = await API.get('https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD');
if (price) {
commit('price', price.USD);
}
},
async getDepositAddress({ commit, state }) {
if (state.operational) {
const { address } = await API.get(`v1/lnd/address`);
if (address) {
commit('depositAddress', address);
}
}
},
async getFees({ commit, state }, { address, confTarget, amt, sweep }) {
if (state.operational) {
const fees = await API.get(`v1/lnd/transaction/estimateFee`, {
params: { address, confTarget, amt, sweep }
});
if (fees) {
commit('fees', fees);
}
}
}
};
const getters = {
status(state) {
const data = {
class: 'loading',
text: 'Loading...',
};
if (state.operational) {
data.class = 'active';
data.text = 'Operational';
}
return data;
},
transactions(state) {
const txs = state.transactions.map(tx => {
const amount = Number(tx.amount);
let type = "incoming";
if (amount < 0) {
type = "outgoing";
}
let description = "Unknown";
if (tx.type === "CHANNEL_OPEN" || tx.type === "PENDING_OPEN") {
description = "Lightning Wallet";
} else if (tx.type === "CHANNEL_CLOSE" || tx.type === "PENDING_CLOSE") {
description = "Lightning Wallet";
} else if (tx.type === "ON_CHAIN_TRANSACTION_SENT") {
description = "Withdrawal";
} else if (tx.type === "ON_CHAIN_TRANSACTION_RECEIVED") {
description = "Deposit";
}
return {
type,
amount: amount < 0 ? amount * -1 : amount, //for formatting +/- in view
timestamp: new Date(Number(tx.timeStamp) * 1000),
description
};
});
return txs;
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};

0
src/store/modules/lightning.js

0
src/store/modules/onboarding.js

14
src/views/Dashboard.vue

@ -14,10 +14,11 @@
<b-col col cols="12" md="6" xl="4">
<card-widget
header="Bitcoin Core"
:status="{text: 'Running', variant: 'success', blink: false}"
title="100%"
:status="{text: syncPercent !== 100 ? 'Synchronizing' : 'Running', variant: 'success', blink: syncPercent !== 100}"
:title="`${syncPercent}%`"
sub-title="Synchronized"
icon="icon-app-bitcoin.svg"
:loading="syncPercent !== 100"
>
<div class>
<!-- <div class="d-flex w-100 justify-content-between px-4">
@ -71,6 +72,9 @@
<script>
// import Vue from "vue";
import { mapState } from "vuex";
import CardWidget from "@/components/CardWidget";
import Blockchain from "@/components/Blockchain";
import LightningWallet from "@/components/LightningWallet";
@ -81,6 +85,9 @@ export default {
return {};
},
computed: {
...mapState({
syncPercent: state => state.bitcoin.percent
}),
isDarkMode() {
return this.$store.getters.isDarkMode;
}
@ -92,6 +99,9 @@ export default {
this.$store.commit("toggleDarkMode");
}
},
created() {
this.$store.dispatch("bitcoin/getSync");
},
components: {
CardWidget,
Blockchain,

5
yarn.lock

@ -1648,6 +1648,11 @@ big.js@^5.2.2:
resolved "https://registry.npm.taobao.org/big.js/download/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg=
bignumber.js@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.npm.taobao.org/binary-extensions/download/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"

Loading…
Cancel
Save