Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Elements-only configuration:

- `IS_ELEMENTS` - set to `1` to indicate this is an Elements-based chain (enables asset issuance and Elements-specific features)
- `SHOW_PEG_DATA` - set to `1` to show dashboard peg data and fetch its API resources (enabled by the Liquid mainnet and regtest flavors; custom pegged chains must opt in)
- `SHOW_HIGH_VALUE_ASSETS` - set to `1` to show circulating values for selected assets on the Liquid mainnet dashboard (enabled by the Liquid mainnet flavor)
- `NATIVE_ASSET_ID` - the ID of the native asset used to pay fees (defaults to `6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d`, the asset id for BTC)
- `BLIND_PREFIX` - the base58 address prefix byte used for confidential addresses (defaults to `12`)
- `PARENT_CHAIN_EXPLORER_TXOUT` - URL format for linking to transaction outputs on the parent chain, with `{txid}` and `{vout}` as placeholders. Example: `https://blockstream.info/tx/{txid}#output:{vout}`
Expand Down
79 changes: 75 additions & 4 deletions client/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ import {setAdapt} from '@cycle/run/lib/adapt';

import { getMempoolDepth, getConfEstimate, calcSegwitFeeGains } from './lib/fees'
import { summarizeBlockTemplate } from './lib/block-template'
import { getPriceFeedApiBase } from './lib/high-value-assets'
import { isBitcoinNetwork } from './lib/network'
import getPrivacyAnalysis from './lib/privacy-analysis'
import { nativeAssetId, blockTxsPerPage, blocksPerPage, difficultyPeriod, showPegData, blockGridTransactionSelectEvent } from './const'
import {
highValueAssetDefinitions,
nativeAssetId,
blockTxsPerPage,
blocksPerPage,
difficultyPeriod,
showHighValueAssets,
showPegData,
blockGridTransactionSelectEvent
} from './const'
import {
dbg,
combine,
Expand All @@ -28,14 +38,37 @@ import {
import l10n, { defaultLang } from './l10n'
import * as views from './views'

const apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '')
const highValueAssetCategory = assetId => `dashboard-high-value-asset-${assetId}`
, highValueAssetPriceCategory = assetId => `dashboard-high-value-asset-price-${assetId}`
, apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '')
, bitcoinMarketChartUrl = process.env.BITCOIN_MARKET_CHART_URL || 'https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=1&interval=hourly'
, blockTemplatePollIntervalMs = 30000
// Wait one electrs cache window after a new tip before requesting the next
// template. If a refresh is still in progress, electrs holds the request
// until fresh data is ready, so no additional client-side jitter is needed.
, blockTemplatePollAfterNewBlockMs = 15000
, priceFeedApiBase = getPriceFeedApiBase(
apiBase,
process.browser ? window.location.origin : process.env.CANONICAL_URL
)
, setBase = ({ path, ...r }) => ({ ...r, url: path.includes('://') || path.startsWith('./') ? path : apiBase + path })
, highValueAssetRequests = !showHighValueAssets ? [] : highValueAssetDefinitions.reduce((requests, asset) => [
...requests,
{
category: highValueAssetCategory(asset.asset_id),
method: 'GET',
path: `/asset/${asset.asset_id}`,
assetId: asset.asset_id,
bg: true
},
...(priceFeedApiBase ? [{
category: highValueAssetPriceCategory(asset.asset_id),
method: 'GET',
path: `${priceFeedApiBase}/api/v1/assets/${asset.asset_id}/price`,
assetId: asset.asset_id,
bg: true
}] : [])
], [])

const reservedPaths = [ 'mempool', 'assets', 'search' ]
, NEW_TABLE_ENTRY_MS = 2000
Expand Down Expand Up @@ -375,8 +408,40 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
: null
, error: asset.error || chainTxs.error || mempoolTxs.error
}))
, dashboardState$ = O.combineLatest(blocks$, mempoolRecent$, dashboardPegState$, (blks, txs, peg) =>
({ dashblocks: blks.slice(0, 5), dashTxs: txs.slice(0, 11), peg }))
, dashboardHighValueAssets$ = !showHighValueAssets
? O.of({})
: O.merge(...highValueAssetDefinitions.reduce((replies, asset) => [
...replies,
reply(highValueAssetCategory(asset.asset_id), true)
.map(r => state => ({
...state,
[r.request.assetId]: {
...state[r.request.assetId],
asset: r.body
}
})),
reply(highValueAssetPriceCategory(asset.asset_id), true)
.map(r => state => ({
...state,
[r.request.assetId]: {
...state[r.request.assetId],
price: r.body && r.body.data
}
}))
], []))
.scan((state, update) => update(state), {})
.startWith({})
, dashboardState$ = O.combineLatest(
blocks$,
mempoolRecent$,
dashboardPegState$,
dashboardHighValueAssets$,
(blks, txs, peg, highValueAssets) => ({
dashblocks: blks.slice(0, 5),
dashTxs: txs.slice(0, 11),
peg,
highValueAssets
}))

, dashboardEpochStartBlock$ = reply('dashboard-epoch-start-block', true)
.map(r => ({ ...r.body, requestedHeight: r.request.height }))
Expand Down Expand Up @@ -632,6 +697,12 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
goHome$.flatMap(_ => [{ category: 'dashboard-peg-asset', method: 'GET', path: `/asset/${nativeAssetId}`, bg: true }
, { category: 'dashboard-peg-chain-txs', method: 'GET', path: `/asset/${nativeAssetId}/txs/chain`, bg: true }
, { category: 'dashboard-peg-mempool-txs', method: 'GET', path: `/asset/${nativeAssetId}/txs/mempool`, bg: true }])

// fetch asset stats and USD prices only while viewing the Liquid dashboard
, !showHighValueAssets ? O.empty() :
O.merge(goHome$, tickWhileViewing(60000, 'dashBoard', view$))
.throttleTime(1000)
.flatMap(_ => highValueAssetRequests)
//
// elements/liquid only
//
Expand Down
72 changes: 72 additions & 0 deletions client/src/components/high-value-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { CurrencyDollarIcon } from "./icons";
Comment thread
Randy808 marked this conversation as resolved.
import { Tooltip } from "./tooltip";
import { highValueAssetDefinitions as assets, staticRoot } from "../const";
import {
calculateCirculatingDollarAmount,
formatDollarAmount,
} from "../lib/high-value-assets";

export const highValueAssets = (t, assetData = {}) => {
const rows = assets
.map((asset, index) => {
const data = assetData[asset.asset_id] || {};

return {
asset,
dollarAmount: calculateCirculatingDollarAmount(data.asset, data.price),
index,
};
})
.sort((left, right) => {
if (left.dollarAmount == null) {
return right.dollarAmount == null ? left.index - right.index : 1;
}
if (right.dollarAmount == null) return -1;

return right.dollarAmount - left.dollarAmount || left.index - right.index;
});

return (
<div className="high-value-assets-table">
<div className="table-header">
<div className="table-header-icon-container">
<CurrencyDollarIcon />
</div>
<h1 className="table-header-title">{t`High-Value Assets`}</h1>
<Tooltip
iconSrc={`${staticRoot}img/icons/tooltip.svg`}
text={t`This panel shows the circulating value of high-value assets on Liquid.`}
/>
</div>

<div className="high-value-assets-body">
{rows.map(({ asset, dollarAmount }) => {
return (
<a
className="high-value-assets-listing"
href={`asset/${asset.asset_id}`}
key={asset.asset_id}
>
<div className="high-value-assets-icon">
<img
src={`${staticRoot}img/icons/${asset.icon || "hva-default.svg"}`}
alt=""
/>
</div>
<p className="high-value-assets-listing-name">{asset.name}</p>
<p className="high-value-assets-circulating-dollar-amount">
{formatDollarAmount(dollarAmount)}
</p>
</a>
);
})}
</div>
<a className="view-more font-link-semibold" href="assets">
<span>{t`See more`}</span>
<div>
<img alt="" src={`${staticRoot}img/icons/arrow-right-blue.svg`} />
</div>
</a>
</div>
);
};
30 changes: 29 additions & 1 deletion client/src/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,45 @@ export const feeEstimateTargets = {
average: 3,
high: 1,
}
export const staticRoot = process.env.STATIC_ROOT || ''

const configuredTargetBlockIntervalSeconds = Number(process.env.TARGET_BLOCK_INTERVAL_SECONDS)
export const targetBlockIntervalSeconds = configuredTargetBlockIntervalSeconds > 0
? configuredTargetBlockIntervalSeconds
: process.env.IS_ELEMENTS ? 60 : 600

export const nativeAssetId = process.env.NATIVE_ASSET_ID || '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'
const liquidNativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'

export const nativeAssetId = process.env.NATIVE_ASSET_ID || liquidNativeAssetId
export const nativeAssetLabel = process.env.NATIVE_ASSET_LABEL || 'BTC'
export const nativeAssetName = process.env.NATIVE_ASSET_NAME || 'Bitcoin'
export const showPegData = !!process.env.IS_ELEMENTS && process.env.SHOW_PEG_DATA == '1'

export const highValueAssetDefinitions = [
{
asset_id: 'aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034',
name: 'BMN2',
icon: 'hva-bmn2.svg'
},
{
asset_id: 'ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2',
name: 'USDT',
icon: 'hva-usdt.svg'
},
{
asset_id: 'e8305bb5c1794b256a858a01e5d8af7a5817d257fbfbc2c9d49620f13ff401a9',
name: 'CMSTR'
},
{
asset_id: '26ac924263ba547b706251635550a8649545ee5c074fe5db8d7140557baaf32e',
name: 'MEXAS'
}
]

export const showHighValueAssets = !!process.env.IS_ELEMENTS
&& process.env.SHOW_HIGH_VALUE_ASSETS == '1'
&& nativeAssetId == liquidNativeAssetId

// Elements only
export const assetTxsPerPage = 25
export const pegTxsPerPage = 25
51 changes: 51 additions & 0 deletions client/src/lib/high-value-assets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const value = n => n == null ? 0 : Number(n)

export const getPriceFeedApiBase = (apiBase, fallbackOrigin) => {
let url

try {
url = new URL(apiBase, fallbackOrigin)
} catch (_) {
return null
}

return `${url.origin}/price`
}

export const calculateCirculatingDollarAmount = (asset, price) => {
if (!asset || !price || price.price_usd == null || !Number.isFinite(Number(price.price_usd))) return null

const { chain_stats = {}, mempool_stats = {} } = asset
, precision = asset.precision == null ? 0 : Number(asset.precision)
, issuedAmount = value(chain_stats.issued_amount) + value(mempool_stats.issued_amount)
, burnedAmount = value(chain_stats.burned_amount) + value(mempool_stats.burned_amount)
, hasBlindedIssuances = chain_stats.has_blinded_issuances || mempool_stats.has_blinded_issuances
, circulatingAmount = issuedAmount - burnedAmount

if (
hasBlindedIssuances ||
!Number.isFinite(circulatingAmount) ||
circulatingAmount < 0 ||
!Number.isInteger(precision) ||
precision < 0
) return null

const dollarAmount = circulatingAmount / Math.pow(10, precision) * Number(price.price_usd)
return Number.isFinite(dollarAmount) ? dollarAmount : null
}

export const formatDollarAmount = amount => {
if (!Number.isFinite(amount)) return 'N/A'

const units = [
[ 1e12, 'T' ],
[ 1e9, 'B' ],
[ 1e6, 'M' ],
[ 1e3, 'K' ]
]
const unit = units.find(([ divisor ]) => Math.abs(amount) >= divisor)

return unit
? `$${(amount / unit[0]).toFixed(1)}${unit[1]}`
: `$${amount.toFixed(1)}`
}
6 changes: 4 additions & 2 deletions client/src/views/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { overview } from "./overview";
import { feeMarket } from "./fee-market";
import difficultyAdjustment from "./difficulty-adjustment";
import { isBitcoinNetwork } from "../lib/network";
import { showPegData } from "../const";
import { showHighValueAssets, showPegData } from "../const";
import { highValueAssets } from "../components/high-value-assets";

const isTouch = process.browser && "ontouchstart" in window;

const homeLayout = (body, { t, activeTab, ...S }) =>
layout(body, { t, isTouch, activeTab, ...S });

export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => {
const { dashblocks, dashTxs, peg = {} } = dashboardState || {};
const { dashblocks, dashTxs, peg = {}, highValueAssets: highValueAssetData } = dashboardState || {};

return homeLayout(
<div className="home-page" key="dashBoard">
Expand All @@ -31,6 +32,7 @@ export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => {
{isBitcoinNetwork
? difficultyAdjustment({ blocks: dashblocks, ...S })
: ""}
{showHighValueAssets ? highValueAssets(t, highValueAssetData) : ""}
</div>,
{ ...S, t, activeTab: "dashBoard" },
);
Expand Down
1 change: 1 addition & 0 deletions flavors/liquid-mainnet/config.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export NATIVE_ASSET_LABEL=LBTC
export NATIVE_ASSET_NAME='Liquid Bitcoin'
export IS_ELEMENTS=1
export SHOW_PEG_DATA=${SHOW_PEG_DATA:-1}
export SHOW_HIGH_VALUE_ASSETS=${SHOW_HIGH_VALUE_ASSETS:-1}

export ASSET_MAP_URL=./_data/assets.minimal.json

Expand Down
Loading