mirror of
https://github.com/Lissy93/dashy.git
synced 2024-11-24 05:56:49 +03:00
✨ Adds crypto wallet balance widget
This commit is contained in:
parent
2ee01f603c
commit
710b3ea7ad
@ -13,6 +13,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a
|
|||||||
- [Weather Forecast](#weather-forecast)
|
- [Weather Forecast](#weather-forecast)
|
||||||
- [Crypto Watch List](#crypto-watch-list)
|
- [Crypto Watch List](#crypto-watch-list)
|
||||||
- [Crypto Price History](#crypto-token-price-history)
|
- [Crypto Price History](#crypto-token-price-history)
|
||||||
|
- [Crypto Wallet Balance](#wallet-balance)
|
||||||
- [RSS Feed](#rss-feed)
|
- [RSS Feed](#rss-feed)
|
||||||
- [Code Stats](#code-stats)
|
- [Code Stats](#code-stats)
|
||||||
- [Vulnerability Feed](#vulnerability-feed)
|
- [Vulnerability Feed](#vulnerability-feed)
|
||||||
@ -238,6 +239,38 @@ Shows recent price history for a given crypto asset, using price data fetched fr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Wallet Balance
|
||||||
|
|
||||||
|
Keep track of your crypto balances and see recent transactions. Data is fetched from [BlockCypher](https://www.blockcypher.com/dev/)
|
||||||
|
|
||||||
|
<p align="center"><img width="600" src="https://i.ibb.co/27HG4nj/wallet-balances.png" /></p>
|
||||||
|
|
||||||
|
##### Options
|
||||||
|
|
||||||
|
**Field** | **Type** | **Required** | **Description**
|
||||||
|
--- | --- | --- | ---
|
||||||
|
**`coin`** | `string` | Required | Symbol of coin or asset, e.g. `btc`, `eth` or `doge`
|
||||||
|
**`address`** | `string` | Required | Address to monitor. This is your wallet's **public** / receiving address
|
||||||
|
**`network`** | `string` | _Optional_ | To use a different network, other than mainnet. Defaults to `main`
|
||||||
|
**`limit`** | `number` | _Optional_ | Limit the number of transactions to display. Defaults to `10`, set to large number to show all
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: wallet-balance
|
||||||
|
options:
|
||||||
|
coin: btc
|
||||||
|
address: 3853bSxupMjvxEYfwGDGAaLZhTKxB2vEVC
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Info
|
||||||
|
- **CORS**: 🟢 Enabled
|
||||||
|
- **Auth**: 🟢 Not Required
|
||||||
|
- **Price**: 🟢 Free
|
||||||
|
- **Privacy**: _See [BlockCypher Privacy Policy](https://www.blockcypher.com/privacy.html)_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### RSS Feed
|
### RSS Feed
|
||||||
|
|
||||||
Display news and updates from any RSS-enabled service.
|
Display news and updates from any RSS-enabled service.
|
||||||
|
228
src/components/Widgets/WalletBalance.vue
Normal file
228
src/components/Widgets/WalletBalance.vue
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wallet-balance-wrapper">
|
||||||
|
<p class="wallet-title">{{ getCoinNameFromSymbol(coin) }} Wallet</p>
|
||||||
|
<a v-if="metaInfo" :href="metaInfo.explorer" class="wallet-address">{{ address }}</a>
|
||||||
|
<div class="balance-inner">
|
||||||
|
<img v-if="metaInfo" :src="metaInfo.qrCode" alt="QR Code" class="wallet-qr" />
|
||||||
|
<div v-if="balances" class="balances-section">
|
||||||
|
<p class="main-balance" v-tooltip="makeBalanceTooltip(balances)">{{ balances.current }}</p>
|
||||||
|
<div class="balance-info">
|
||||||
|
<div class="balance-info-row">
|
||||||
|
<span class="label">Total In</span>
|
||||||
|
<span class="amount">+ {{ balances.totalReceived }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance-info-row">
|
||||||
|
<span class="label">Total Out:</span>
|
||||||
|
<span class="amount">- {{ balances.totalSent }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="balance-info-row">
|
||||||
|
<span class="label">Last Activity:</span>
|
||||||
|
<span class="amount">{{ balances.lastTransaction }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="transactions" v-if="transactions">
|
||||||
|
<p class="transactions-title">Recent Transactions</p>
|
||||||
|
<a class="transaction-row"
|
||||||
|
v-for="transaction in transactions"
|
||||||
|
:key="transaction.hash"
|
||||||
|
:href="transaction.url"
|
||||||
|
v-tooltip="makeTransactionTooltip(transaction)"
|
||||||
|
>
|
||||||
|
<span class="date">{{ transaction.date }}</span>
|
||||||
|
<span :class="`amount ${transaction.incoming ? 'in' : 'out'}`">
|
||||||
|
{{ transaction.incoming ? '+' : '-'}}{{ transaction.amount }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import WidgetMixin from '@/mixins/WidgetMixin';
|
||||||
|
import { widgetApiEndpoints } from '@/utils/defaults';
|
||||||
|
import { timestampToDate, timestampToTime, getTimeAgo } from '@/utils/MiscHelpers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [WidgetMixin],
|
||||||
|
computed: {
|
||||||
|
coin() {
|
||||||
|
if (!this.options.coin) this.error('You must specify a coin, e.g. \'BTC\'');
|
||||||
|
return this.options.coin.toLowerCase();
|
||||||
|
},
|
||||||
|
address() {
|
||||||
|
if (!this.options.address) this.error('You must specify a public address');
|
||||||
|
return this.options.address;
|
||||||
|
},
|
||||||
|
network() {
|
||||||
|
return this.options.network || 'main';
|
||||||
|
},
|
||||||
|
limit() {
|
||||||
|
return this.options.limit || 10;
|
||||||
|
},
|
||||||
|
endpoint() {
|
||||||
|
return `${widgetApiEndpoints.walletBalance}/`
|
||||||
|
+ `${this.coin}/${this.network}/addrs/${this.address}`;
|
||||||
|
},
|
||||||
|
divisionFactor() {
|
||||||
|
switch (this.coin) {
|
||||||
|
case ('btc'): return 100000000;
|
||||||
|
case ('eth'): return 1000000000000000000;
|
||||||
|
default: return 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
balances: null,
|
||||||
|
metaInfo: null,
|
||||||
|
transactions: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
this.makeRequest(this.endpoint).then(this.processData);
|
||||||
|
},
|
||||||
|
processData(data) {
|
||||||
|
const formatAmount = (amount) => {
|
||||||
|
const symbol = this.coin.toUpperCase();
|
||||||
|
if (!amount) return `0 ${symbol}`;
|
||||||
|
return `${(amount / this.divisionFactor).toFixed(6)} ${symbol}`;
|
||||||
|
};
|
||||||
|
this.balances = {
|
||||||
|
current: formatAmount(data.balance),
|
||||||
|
unconfirmed: formatAmount(data.unconfirmed_balance),
|
||||||
|
final: formatAmount(data.final_balance),
|
||||||
|
totalSent: formatAmount(data.total_sent),
|
||||||
|
totalReceived: formatAmount(data.total_received),
|
||||||
|
lastTransaction: data.txrefs ? getTimeAgo(data.txrefs[0].confirmed) : 'Never',
|
||||||
|
};
|
||||||
|
const transactions = [];
|
||||||
|
data.txrefs.forEach((transaction) => {
|
||||||
|
transactions.push({
|
||||||
|
hash: transaction.tx_hash,
|
||||||
|
amount: formatAmount(transaction.value),
|
||||||
|
date: timestampToDate(transaction.confirmed),
|
||||||
|
time: timestampToTime(transaction.confirmed),
|
||||||
|
confirmations: transaction.confirmations,
|
||||||
|
blockHeight: transaction.block_height,
|
||||||
|
balance: formatAmount(transaction.ref_balance),
|
||||||
|
incoming: transaction.tx_input_n === -1,
|
||||||
|
url: `https://live.blockcypher.com/${this.coin}/tx/${transaction.tx_hash}/`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.transactions = transactions.slice(0, this.limit);
|
||||||
|
},
|
||||||
|
getCoinNameFromSymbol(symbol) {
|
||||||
|
const coins = {
|
||||||
|
btc: 'Bitcoin',
|
||||||
|
dash: 'Dash',
|
||||||
|
doge: 'Doge',
|
||||||
|
ltc: 'Litecoin',
|
||||||
|
eth: 'Ethereum',
|
||||||
|
bhc: 'BitcoinCash',
|
||||||
|
xmr: 'Monero',
|
||||||
|
ada: 'Cardano',
|
||||||
|
bcy: 'BlockCypher',
|
||||||
|
};
|
||||||
|
if (!symbol || !Object.keys(coins).includes(symbol.toLowerCase())) return '';
|
||||||
|
return coins[symbol.toLowerCase()];
|
||||||
|
},
|
||||||
|
makeBalanceTooltip(balances) {
|
||||||
|
return this.tooltip(
|
||||||
|
`<b>Unconfirmed:</b> ${balances.unconfirmed}<br><b>Final:</b> ${balances.final}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
makeTransactionTooltip(transaction) {
|
||||||
|
return this.tooltip(
|
||||||
|
`At ${transaction.time}<br>`
|
||||||
|
+ `<b>BlockHeight:</b> ${transaction.blockHeight}<br>`
|
||||||
|
+ `<b>Confirmations:</b> ${transaction.confirmations}<br>`
|
||||||
|
+ `<b>Balance After:</b> ${transaction.balance}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
makeMetaInfo() {
|
||||||
|
const explorer = `https://live.blockcypher.com/${this.coin}/address/${this.address}/`;
|
||||||
|
const coin = this.getCoinNameFromSymbol(this.coin).toLowerCase();
|
||||||
|
const qrCode = `${widgetApiEndpoints.walletQrCode}/`
|
||||||
|
+ `?style=${coin.toLowerCase()}&color=11&address=${this.address}`;
|
||||||
|
return { explorer, coin, qrCode };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.metaInfo = this.makeMetaInfo();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wallet-balance-wrapper {
|
||||||
|
max-width: 30rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
a.wallet-address {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
opacity: var(--dimming-factor);
|
||||||
|
color: var(--widget-text-color);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
.balance-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
img.wallet-qr {
|
||||||
|
max-width: 7rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
}
|
||||||
|
.balances-section {
|
||||||
|
p {
|
||||||
|
color: var(--widget-text-color);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
cursor: default;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
p.main-balance {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.balance-info .balance-info-row {
|
||||||
|
opacity: var(--dimming-factor);
|
||||||
|
color: var(--widget-text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin: 0.2rem 0.5rem;
|
||||||
|
span.amount {
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.wallet-title, p.transactions-title {
|
||||||
|
color: var(--widget-text-color);
|
||||||
|
margin: 0.5rem 0 0.25rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.transactions .transaction-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
text-decoration: none;
|
||||||
|
span {
|
||||||
|
color: var(--widget-text-color);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
span.amount {
|
||||||
|
&.in { color: var(--success); }
|
||||||
|
&.out { color: var(--danger); }
|
||||||
|
}
|
||||||
|
&:not(:last-child) { border-bottom: 1px dashed var(--widget-text-color); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -228,8 +228,8 @@
|
|||||||
@error="handleError"
|
@error="handleError"
|
||||||
:ref="widgetRef"
|
:ref="widgetRef"
|
||||||
/>
|
/>
|
||||||
<XkcdComic
|
<WalletBalance
|
||||||
v-else-if="widgetType === 'xkcd-comic'"
|
v-else-if="widgetType === 'wallet-balance'"
|
||||||
:options="widgetOptions"
|
:options="widgetOptions"
|
||||||
@loading="setLoaderState"
|
@loading="setLoaderState"
|
||||||
@error="handleError"
|
@error="handleError"
|
||||||
@ -249,6 +249,13 @@
|
|||||||
@error="handleError"
|
@error="handleError"
|
||||||
:ref="widgetRef"
|
:ref="widgetRef"
|
||||||
/>
|
/>
|
||||||
|
<XkcdComic
|
||||||
|
v-else-if="widgetType === 'xkcd-comic'"
|
||||||
|
:options="widgetOptions"
|
||||||
|
@loading="setLoaderState"
|
||||||
|
@error="handleError"
|
||||||
|
:ref="widgetRef"
|
||||||
|
/>
|
||||||
<!-- No widget type specified -->
|
<!-- No widget type specified -->
|
||||||
<div v-else>{{ handleError('Widget type was not found') }}</div>
|
<div v-else>{{ handleError('Widget type was not found') }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -302,6 +309,7 @@ export default {
|
|||||||
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
|
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
|
||||||
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
|
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
|
||||||
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
|
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
|
||||||
|
WalletBalance: () => import('@/components/Widgets/WalletBalance.vue'),
|
||||||
Weather: () => import('@/components/Widgets/Weather.vue'),
|
Weather: () => import('@/components/Widgets/Weather.vue'),
|
||||||
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
|
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
|
||||||
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
|
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
|
||||||
|
@ -57,8 +57,10 @@ const WidgetMixin = {
|
|||||||
this.finishLoading();
|
this.finishLoading();
|
||||||
},
|
},
|
||||||
/* Used as v-tooltip, pass text content in, and will show on hover */
|
/* Used as v-tooltip, pass text content in, and will show on hover */
|
||||||
tooltip(content) {
|
tooltip(content, html = false) {
|
||||||
return { content, trigger: 'hover focus', delay: 250 };
|
return {
|
||||||
|
content, html, trigger: 'hover focus', delay: 250,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
/* Makes data request, returns promise */
|
/* Makes data request, returns promise */
|
||||||
makeRequest(endpoint, options) {
|
makeRequest(endpoint, options) {
|
||||||
|
@ -228,6 +228,8 @@ module.exports = {
|
|||||||
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
|
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
|
||||||
stockPriceChart: 'https://www.alphavantage.co/query',
|
stockPriceChart: 'https://www.alphavantage.co/query',
|
||||||
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
|
tflStatus: 'https://api.tfl.gov.uk/line/mode/tube/status',
|
||||||
|
walletBalance: 'https://api.blockcypher.com/v1',
|
||||||
|
walletQrCode: 'https://www.bitcoinqrcodemaker.com/api',
|
||||||
weather: 'https://api.openweathermap.org/data/2.5/weather',
|
weather: 'https://api.openweathermap.org/data/2.5/weather',
|
||||||
weatherForecast: 'https://api.openweathermap.org/data/2.5/forecast/daily',
|
weatherForecast: 'https://api.openweathermap.org/data/2.5/forecast/daily',
|
||||||
xkcdComic: 'https://xkcd.vercel.app/',
|
xkcdComic: 'https://xkcd.vercel.app/',
|
||||||
|
Loading…
Reference in New Issue
Block a user