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)
|
||||
- [Crypto Watch List](#crypto-watch-list)
|
||||
- [Crypto Price History](#crypto-token-price-history)
|
||||
- [Crypto Wallet Balance](#wallet-balance)
|
||||
- [RSS Feed](#rss-feed)
|
||||
- [Code Stats](#code-stats)
|
||||
- [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
|
||||
|
||||
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"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<XkcdComic
|
||||
v-else-if="widgetType === 'xkcd-comic'"
|
||||
<WalletBalance
|
||||
v-else-if="widgetType === 'wallet-balance'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
@ -249,6 +249,13 @@
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<XkcdComic
|
||||
v-else-if="widgetType === 'xkcd-comic'"
|
||||
:options="widgetOptions"
|
||||
@loading="setLoaderState"
|
||||
@error="handleError"
|
||||
:ref="widgetRef"
|
||||
/>
|
||||
<!-- No widget type specified -->
|
||||
<div v-else>{{ handleError('Widget type was not found') }}</div>
|
||||
</div>
|
||||
@ -302,6 +309,7 @@ export default {
|
||||
StockPriceChart: () => import('@/components/Widgets/StockPriceChart.vue'),
|
||||
SystemInfo: () => import('@/components/Widgets/SystemInfo.vue'),
|
||||
TflStatus: () => import('@/components/Widgets/TflStatus.vue'),
|
||||
WalletBalance: () => import('@/components/Widgets/WalletBalance.vue'),
|
||||
Weather: () => import('@/components/Widgets/Weather.vue'),
|
||||
WeatherForecast: () => import('@/components/Widgets/WeatherForecast.vue'),
|
||||
XkcdComic: () => import('@/components/Widgets/XkcdComic.vue'),
|
||||
|
@ -57,8 +57,10 @@ const WidgetMixin = {
|
||||
this.finishLoading();
|
||||
},
|
||||
/* Used as v-tooltip, pass text content in, and will show on hover */
|
||||
tooltip(content) {
|
||||
return { content, trigger: 'hover focus', delay: 250 };
|
||||
tooltip(content, html = false) {
|
||||
return {
|
||||
content, html, trigger: 'hover focus', delay: 250,
|
||||
};
|
||||
},
|
||||
/* Makes data request, returns promise */
|
||||
makeRequest(endpoint, options) {
|
||||
|
@ -228,6 +228,8 @@ module.exports = {
|
||||
sportsScores: 'https://www.thesportsdb.com/api/v1/json',
|
||||
stockPriceChart: 'https://www.alphavantage.co/query',
|
||||
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',
|
||||
weatherForecast: 'https://api.openweathermap.org/data/2.5/forecast/daily',
|
||||
xkcdComic: 'https://xkcd.vercel.app/',
|
||||
|
Loading…
Reference in New Issue
Block a user