diff --git a/.gitignore b/.gitignore index 34c02e69..2ecc901b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ terraform/.terraform terraform/.terraform/environment terraform/*.tfstate.backup terraform/*.tfstate +**/.DS_Store *~ diff --git a/client/src/app.js b/client/src/app.js index 9fb336c8..6ee5e45f 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -207,6 +207,10 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search // In elements, we block rendering until the assetMap is loaded. Otherwise, we can start immediately. , isReady$ = process.env.ASSET_MAP_URL ? assetMap$.mapTo(true).startWith(false) : O.of(true) + // Asset Icons Response + , assetIcons$ = !process.env.ASSET_ICONS_URL ? O.of({}) : + reply('asset-icons') + // Currently visible view , view$ = O.merge(page$.mapTo(null) , goHome$.mapTo('recentBlocks') @@ -241,7 +245,7 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search , mempool$, mempoolRecent$, feeEst$ , tx$, txAnalysis$, openTx$ , goAddr$, addr$, addrTxs$, addrQR$ - , assetMap$, goAsset$, asset$, assetTxs$ + , assetMap$, assetIcons$, goAsset$, asset$, assetTxs$ , isReady$, loading$, page$, view$, title$, theme$ }) @@ -334,6 +338,11 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search ? { category: 'asset-txs', method: 'GET', path: `/asset/${d.asset_id}/txs/chain/${last(d.last_txids)}` } : { category: 'asset-txs', method: 'GET', path: `/asset/${d.asset_id}/txs` }]) + // Fetch Asset Icons + , !process.env.ASSET_ICONS_URL ? O.empty() : O.of( + { category: 'asset-icons', method: 'GET', path: process.env.ASSET_ICONS_URL}) + + // fetch more txs for asset page , moreSTxs$.map(d => ({ category: 'asset-txs-more', method: 'GET', path: `/asset/${d.asset_id}/txs/chain/${d.last_txid}` })) ).map(setBase) diff --git a/client/src/views/asset-advanced-details.js b/client/src/views/asset-advanced-details.js new file mode 100644 index 00000000..381f0815 --- /dev/null +++ b/client/src/views/asset-advanced-details.js @@ -0,0 +1,108 @@ +import Snabbdom from "snabbdom-pragma"; +import { formatJson, formatAssetAmount, formatSat, formatNumber } from './util' + +export default (asset, is_native_asset, mempool_stats, chain_stats, t) => { + + return( +
+ { is_native_asset + // Native asset + ? [ + mempool_stats.peg_in_amount > 0 &&
+
{t`Pegged in (unconfirmed)`}
+
{formatSat(mempool_stats.peg_in_amount)}
+
+ + , mempool_stats.peg_out_amount > 0 &&
+
{t`Pegged out (unconfirmed)`}
+
{formatSat(mempool_stats.peg_out_amount)}
+
+ + , mempool_stats.burned_amount > 0 &&
+
{t`Burned (unconfirmed)`}
+
{formatSat(mempool_stats.burned_amount)}
+
+ + ,
+
{t`Peg/burn transaction count`}
+
{formatNumber(chain_stats.tx_count)}
+
+ + , mempool_stats.peg_out_amount > 0 &&
+
{t`Peg/burn transaction count (unconfirmed)`}
+
{formatNumber(mempool_stats.tx_count)}
+
+ ] + + // Issued assets + : [ +
+
{t`Issuance transaction`}
+
{`${asset.issuance_txin.txid}:${asset.issuance_txin.vin}`}
+
+ + ,
+
{t`Included in Block`}
+
{ asset.status.confirmed + ? {asset.status.block_hash} + : t`Unconfirmed` + }
+
+ + , asset.contract_hash &&
+
{t`Contract hash`}
+
{asset.contract_hash}
+
+ + , asset.contract &&
+
{t`Contract JSON`}
+
{formatJson(asset.contract)}
+
+ + ,
+
{t`Decimal places`}
+
{asset.precision || 0}
+
+ + ,
+
{t`Number of issuances`}
+
{chain_stats.issuance_count}
+
+ + , mempool_stats.issuance_count > 0 &&
+
{t`Number of issuances (unconfirmed)`}
+
{mempool_stats.issuance_count}
+
+ + , mempool_stats.issued_amount > 0 &&
+
{t`Issued amount (unconfirmed)`}
+
{formatAssetAmount(mempool_stats.issued_amount, asset.precision, t)}
+
+ + , mempool_stats.burned_amount > 0 &&
+
{t`Burned amount (unconfirmed)`}
+
{formatAssetAmount(mempool_stats.burned_amount, asset.precision, t)}
+
+ + ,
+
{t`Reissuance tokens created`}
+
{chain_stats.reissuance_tokens == null ? t`Confidential` + : chain_stats.reissuance_tokens === 0 ? t`None` + : formatNumber(chain_stats.reissuance_tokens) }
+
+ + , chain_stats.burned_reissuance_tokens > 0 &&
+
{t`Reissuance tokens burned`}
+
{formatNumber(chain_stats.burned_reissuance_tokens)}
+
+ + , mempool_stats.burned_reissuance_tokens > 0 &&
+
{t`Reissuance tokens burned (unconfirmed)`}
+
{formatNumber(mempool_stats.burned_reissuance_tokens)}
+
+ + ] + } +
+ ) +} \ No newline at end of file diff --git a/client/src/views/asset-issuance-history.js b/client/src/views/asset-issuance-history.js new file mode 100644 index 00000000..ed5deb0a --- /dev/null +++ b/client/src/views/asset-issuance-history.js @@ -0,0 +1,70 @@ +import Snabbdom from "snabbdom-pragma"; +import { formatTime, formatAssetAmount } from './util' + +export default (assetTxs, chain_stats, asset, t) => { + + const getCalculatedAssetTx = () => { + // Supply Starts at Zero + let supply = 0 + // Sort AssetTxs by block_time and Calculate Supply Change / Total Supply + assetTxs.sort((a,b) => (a.status.block_time > b.status.block_time) ? 1 : ((b.status.block_time > a.status.block_time) ? -1 : 0)) + let calAssetTx = assetTxs.map(tx => { + let calTxObj = {} + calTxObj.txid = tx.txid + calTxObj.block_height = tx.status.block_height + calTxObj.block_time = formatTime(tx.status.block_time) + + // Find all Issuance and add them to Supply + tx.vin.forEach(vinTx => { + if("issuance" in vinTx && vinTx.issuance.asset_id === asset.asset_id){ + calTxObj.supplyChange = chain_stats.has_blinded_issuances ? t`Confidential` : `+ ${formatAssetAmount((vinTx.issuance.assetamount), asset.precision, t).text}` + calTxObj.totalSupply = chain_stats.has_blinded_issuances ? t`Confidential` : formatAssetAmount((supply + vinTx.issuance.assetamount), asset.precision, t) + supply = (supply + vinTx.issuance.assetamount) + } + }) + + // Find all Burn Transactions and Remove them from Supply + tx.vout.forEach(voutTx => { + if(voutTx.scriptpubkey_type === "op_return" && voutTx.asset === asset.asset_id){ + if(voutTx.value > 0){ + calTxObj.supplyChange = chain_stats.has_blinded_issuances ? t`Confidential` : `- ${formatAssetAmount((voutTx.value), asset.precision, t).text}` + calTxObj.totalSupply = chain_stats.has_blinded_issuances ? t`Confidential` : formatAssetAmount((supply - voutTx.value), asset.precision, t) + supply = (supply - voutTx.value) + } + } + }) + return calTxObj + }) + + // Reverse Calculated AssetTx to Display Newest to Older + return calAssetTx.reverse() + } + + let calculatedAssetTx = assetTxs === null ? "" : getCalculatedAssetTx() + + + return( +
+ { assetTxs === null ?

{t`No Issuance History`}

+ :
+
+
{t`Block`}
+
{t`Time`}
+
{t`Supply Change`}
+
{t`Total Supply`}
+
+ {calculatedAssetTx.map(tx => +
+ +
{tx.block_height}
+
{tx.block_time}
+
{tx.supplyChange}
+
{tx.totalSupply}
+
+
+ )} +
+ } +
+ ) +} \ No newline at end of file diff --git a/client/src/views/asset-list.js b/client/src/views/asset-list.js index 717c0644..06edae6b 100644 --- a/client/src/views/asset-list.js +++ b/client/src/views/asset-list.js @@ -2,7 +2,7 @@ import Snabbdom from 'snabbdom-pragma' import layout from './layout' -export default ({ assetMap, t, ...S }) => { +export default ({ assetMap, assetIcons, t, ...S }) => { const assets = Object.entries(assetMap) .map(([ asset_id, [ domain, ticker, name ] ]) => ({ asset_id, domain, ticker, name })) @@ -28,7 +28,15 @@ export default ({ assetMap, t, ...S }) => { {assets.map(asset =>
-
{asset.name}
+
+
+ {assetIcons === null ? "" : + assetIcons[`${asset.asset_id}`] === undefined ? +
: + } + {asset.name} +
+
{asset.ticker || None}
{asset.domain}
{asset.asset_id}
diff --git a/client/src/views/asset-summary.js b/client/src/views/asset-summary.js new file mode 100644 index 00000000..aeddc65c --- /dev/null +++ b/client/src/views/asset-summary.js @@ -0,0 +1,97 @@ +import Snabbdom from "snabbdom-pragma"; +import { formatAssetAmount, formatSat } from './util' + +export default (is_native_asset, asset, assetIcons, nativeAssetName, + nativeAssetLabel, entity_type, circulating, chain_stats, is_non_reissuable, t) => { + return( +
+ { is_native_asset + // Native asset + ? [ +
+
+
+
+ {assetIcons === null ? "" : + assetIcons[`${asset.asset_id}`] === undefined ? +
: + } +
+
+
{nativeAssetName}
{nativeAssetLabel}
+
+
+
{t(`Asset ID`)}
+
{asset.asset_id} + { process.browser &&
+
+
} +
+
+
+
{t(`Total Active Supply`)}
+
{formatSat(circulating)}
+
+
+
{t(`Total Amount Burned`)}
+
{formatSat(chain_stats.burned_amount)}
+
+
+
{t`Pegged in`}
+
{formatSat(chain_stats.peg_in_amount)}
+
+
+
{t`Pegged out`}
+
{formatSat(chain_stats.peg_out_amount)}
+
+
+ ] + // Issued assets + : [ +
+
+
+
+ {assetIcons === null ? "" : + assetIcons[`${asset.asset_id}`] === undefined ? +
: + } +
+
+
{asset.name}
{asset.ticker}
+
+
+
{t(`Issuer`)}
+
{asset.entity[entity_type]}
+
+
+
{t(`Asset ID`)}
+
{asset.asset_id} + { process.browser &&
+
+
} +
+
+
+
{t(`Total Active Supply`)}
+
{ circulating == null ? t`Confidential` + : formatAssetAmount(circulating, asset.precision, t) }
+
+
+
{t(`Total Amount Burned`)}
+
{formatAssetAmount(chain_stats.burned_amount, asset.precision, t)}
+
+
+
{t(`Reissuable`)}
+
{ is_non_reissuable ? t`No` : t`Yes` }
+
+
+
{t(`Reissuable Token for Asset`)}
+
{asset.reissuance_token}
+
+
+ ] + } +
+ ) +} \ No newline at end of file diff --git a/client/src/views/asset.js b/client/src/views/asset.js index 833011f7..bd63f23a 100644 --- a/client/src/views/asset.js +++ b/client/src/views/asset.js @@ -1,14 +1,16 @@ import Snabbdom from 'snabbdom-pragma' import { last } from '../util' -import { formatNumber, formatJson, formatAssetAmount, formatSat } from './util' +import { formatNumber } from './util' import layout from './layout' -import search from './search' import { txBox } from './tx' import { maxMempoolTxs, assetTxsPerPage as perPage, nativeAssetLabel, nativeAssetName } from '../const' +import assetSummary from "./asset-summary"; +import issuanceHistory from "./asset-issuance-history"; +import advancedDetails from "./asset-advanced-details"; const staticRoot = process.env.STATIC_ROOT || '' -export default ({ t, asset, assetTxs, goAsset, openTx, spends, tipHeight, loading, ...S }) => { +export default ({ t, asset, assetIcons, assetTxs, goAsset, openTx, spends, tipHeight, loading, ...S }) => { if (!asset) return; const { chain_stats = {}, mempool_stats = {} } = asset @@ -45,208 +47,54 @@ export default ({ t, asset, assetTxs, goAsset, openTx, spends, tipHeight, loadin return layout(
-
+
+ {`<`}{`All Assets`} +
- { search({ t, klass: 'page-search-bar' }) } -

{t`Asset`}

-
- {asset.asset_id} - { process.browser &&
-
-
} -
+ {assetSummary(is_native_asset, asset, assetIcons, nativeAssetName, + nativeAssetLabel, entity_type, circulating, chain_stats, is_non_reissuable, t)}
-
-
-
-
-
{t`Asset id`}
-
{asset.asset_id}
-
- - { is_native_asset - // Native asset - ? [ -
-
{t`Name`}
-
{nativeAssetName}
-
- - ,
-
{t`Ticker`}
-
{nativeAssetLabel}
-
- - ,
-
{t`Pegged in`}
-
{formatSat(chain_stats.peg_in_amount)}
-
- - , mempool_stats.peg_in_amount > 0 &&
-
{t`Pegged in (unconfirmed)`}
-
{formatSat(mempool_stats.peg_in_amount)}
-
- - ,
-
{t`Pegged out`}
-
{formatSat(chain_stats.peg_out_amount)}
-
- - , mempool_stats.peg_out_amount > 0 &&
-
{t`Pegged out (unconfirmed)`}
-
{formatSat(mempool_stats.peg_out_amount)}
-
- - ,
-
{t`Burned`}
-
{formatSat(chain_stats.burned_amount)}
-
- , mempool_stats.burned_amount > 0 &&
-
{t`Burned (unconfirmed)`}
-
{formatSat(mempool_stats.burned_amount)}
-
- - ,
-
{t`Circulating amount`}
-
{formatSat(circulating)}
-
- - ,
-
{t`Peg/burn transaction count`}
-
{formatNumber(chain_stats.tx_count)}
-
- - , mempool_stats.peg_out_amount > 0 &&
-
{t`Peg/burn transaction count (unconfirmed)`}
-
{formatNumber(mempool_stats.tx_count)}
-
- ] - - // Issued assets - : [ - asset.name &&
-
{t`Name`}
-
{asset.name}
-
- - ,
-
{t`Precision - decimal places`}
-
{asset.precision || 0}
-
- - , asset.ticker &&
-
{t`Ticker`}
-
{asset.ticker}
-
- - , asset.entity &&
-
{t(`Issuer ${entity_type}`)}
-
{asset.entity[entity_type]}
-
- - ,
-
{t`Issuance transaction`}
- -
- - ,
-
{t`Included in Block`}
-
{ asset.status.confirmed - ? {asset.status.block_hash} - : t`Unconfirmed` - }
-
- - ,
-
{t`Number of issuances`}
-
{chain_stats.issuance_count}
-
- - , mempool_stats.issuance_count > 0 &&
-
{t`Number of issuances (unconfirmed)`}
-
{mempool_stats.issuance_count}
-
- - ,
-
{t`Issued amount`}
-
{chain_stats.has_blinded_issuances ? t`Confidential` - : formatAssetAmount(chain_stats.issued_amount, asset.precision, t) }
-
- - , mempool_stats.issued_amount > 0 &&
-
{t`Issued amount (unconfirmed)`}
-
{formatAssetAmount(mempool_stats.issued_amount, asset.precision, t)}
-
- - , chain_stats.burned_amount > 0 &&
-
{t`Burned amount`}
-
{formatAssetAmount(chain_stats.burned_amount, asset.precision, t)}
-
- - , mempool_stats.burned_amount > 0 &&
-
{t`Burned amount (unconfirmed)`}
-
{formatAssetAmount(mempool_stats.burned_amount, asset.precision, t)}
-
- - ,
-
{t`Reissuance tokens created`}
-
{chain_stats.reissuance_tokens == null ? t`Confidential` - : chain_stats.reissuance_tokens === 0 ? t`None` - : formatNumber(chain_stats.reissuance_tokens) }
-
- - , chain_stats.burned_reissuance_tokens > 0 &&
-
{t`Reissuance tokens burned`}
-
{formatNumber(chain_stats.burned_reissuance_tokens)}
-
- - , mempool_stats.burned_reissuance_tokens > 0 &&
-
{t`Reissuance tokens burned (unconfirmed)`}
-
{formatNumber(mempool_stats.burned_reissuance_tokens)}
-
- - ,
-
{t`Circulating amount`}
-
{ circulating == null ? t`Confidential` - : formatAssetAmount(circulating, asset.precision, t) }
-
- - ,
-
{t`Re-issuable`}
-
{ is_non_reissuable ? t`No` : t`Yes` }
-
+ { is_native_asset + // Hide Tabs if Asset is Native + ? null : [ +
+ + + + +
+
+ {issuanceHistory(assetTxs, chain_stats, asset, t)} +
+
+ {advancedDetails(asset, is_native_asset, mempool_stats, chain_stats, t)} +
+
+
+ ]} - , asset.contract_hash &&
-
{t`Contract hash`}
-
{asset.contract_hash}
+ {is_native_asset + // Show Transaction Box if Asset is Native + ? [ +
+
+
+

{(is_native_asset ? txsShownTextNative : txsShownTextIssued)(total_txs, est_prev_total_seen_count, shown_txs, t)}

+ { assetTxs ? assetTxs.map(tx => txBox(tx, { openTx, tipHeight, t, spends, ...S })) + : }
- , asset.contract &&
-
{t`Contract JSON`}
-
{formatJson(asset.contract)}
+
+
+ { loading ?
{t`Load more`}
+ : pagingNav(asset, last_seen_txid, est_curr_chain_seen_count, prev_paging_txids, next_paging_txids, prev_paging_est_count, t) } +
- ] - } -
- -
-
-

{(is_native_asset ? txsShownTextNative : txsShownTextIssued)(total_txs, est_prev_total_seen_count, shown_txs, t)}

- { assetTxs ? assetTxs.map(tx => txBox(tx, { openTx, tipHeight, t, spends, ...S })) - : } -
- -
-
- { loading ?
{t`Load more`}
- : pagingNav(asset, last_seen_txid, est_curr_chain_seen_count, prev_paging_txids, next_paging_txids, prev_paging_est_count, t) } +
-
- -
-
+ ] : null }
, { t, ...S }) } diff --git a/flavors/liquid-mainnet/config.env b/flavors/liquid-mainnet/config.env index 750bcba9..0b3d8b38 100755 --- a/flavors/liquid-mainnet/config.env +++ b/flavors/liquid-mainnet/config.env @@ -9,6 +9,8 @@ export IS_ELEMENTS=1 export ASSET_MAP_URL=./_data/assets.minimal.json +export ASSET_ICONS_URL=https://assets.blockstream.info/icons.json + export MENU_ACTIVE='Liquid' export BASE_HREF=${BASE_HREF:-'/liquid/'} diff --git a/flavors/liquid-mainnet/extras.css b/flavors/liquid-mainnet/extras.css index c4419164..a5fc4483 100644 --- a/flavors/liquid-mainnet/extras.css +++ b/flavors/liquid-mainnet/extras.css @@ -21,6 +21,146 @@ background-image: linear-gradient(-90deg, rgba(13, 141, 119, 1) 0%, rgba(17, 103, 97, 1) 16%, rgba(25, 68, 74, 1) 35%, rgba(29, 42, 48, 1) 57%, rgba(14, 16, 17, 1) 100%); } + +/* New Asset Summary Start*/ +.back-nav{ + margin-top: 100px; + margin-bottom: 50px; +} + +.back-nav a{ + color: #78838e; +} + +.back-nav a:hover{ + color: #c2c2c3; +} + +.back-nav span{ + font-size: 20px; + margin-right: 10px; +} + +.asset-summary{ + color: white; + margin-bottom: 100px; +} + +.asset-logo{ + height:50px; + width:50px; + border-radius: 100%; + margin-right: 20px; +} + +.asset-summary .asset-icon-placeholder{ + width: 50px; + height: 50px; + float: left; + margin-right: 10px; + background: url(img/icons/asset_unknown.png); + background-size: cover; +} + +.asset-summary .asset-icon{ + width: 50px; + height: 50px; + float: left; + margin-right: 10px; +} + +.asset-logo-name{ + display: flex; + margin-bottom: 30px; +} + +.asset-name{ + font-size: 22px; +} + +.asset-name span{ + font-size: 16px; + font-weight: bold; + float: left; + margin-top: -5px; +} + + + +.asset-label-text{ + display: flex; + margin-bottom: 20px; + word-break: break-word; +} + +.asset-label{ + width: 30%; + color: #78838e; +} + +/* Tab CSS Start *******/ +.tabs { + margin-top: 20px; + font-size: 20px; +} + +.tabs input { + display: none; +} + +.tabs label { + display: inline-block; + padding: 10px 0 ; + margin-right: 30px; + width: auto; + text-align: center; + color: #78838e; +} + +.tabs label:hover { + cursor: pointer; +} + +.tabs input:checked + label { + border-bottom: 2px solid #00ccff; + color: white; +} + +.tabs #tab1:checked ~ .content #content1, +.tabs #tab2:checked ~ .content #content2{ + display: block; +} + +.tabs .content > div { + display: none; + padding-top: 20px; + text-align: left; + overflow: auto; +} +/* New Asset Summary End*/ + + +/* Asset List Icons*/ +.assets-table-cell .asset-icon-placeholder{ + width: 25px; + height: 25px; + float: left; + margin-right: 10px; + background: url(img/icons/asset_unknown.png); + background-size: cover; +} + +.assets-table-cell .asset-icon{ + width: 25px; + height: 25px; + float: left; + margin-right: 10px; +} + +.assets-table-name span{ + word-break: break-all; +} + .assets-table .assets-table-row { display: table; table-layout: fixed; @@ -68,6 +208,7 @@ .assets-table .assets-table-row > div:nth-child(1) { padding-left: 12px; + padding-right: 15px; } .assets-table-row { @@ -92,8 +233,16 @@ @media only screen and (max-width: 850px) { - /* assets table */ + .asset-label{ + width: 40%; + } + .asset-text{ + text-align: right; + width: 60%; + } + + /* assets table */ div.assets-table-row.header { display:none; } diff --git a/flavors/liquid-mainnet/www/img/icons/asset_unknown.png b/flavors/liquid-mainnet/www/img/icons/asset_unknown.png new file mode 100644 index 00000000..12bda652 Binary files /dev/null and b/flavors/liquid-mainnet/www/img/icons/asset_unknown.png differ diff --git a/www/light-theme_style.css b/www/light-theme_style.css index c0ebff0f..dc473ab9 100644 --- a/www/light-theme_style.css +++ b/www/light-theme_style.css @@ -97,8 +97,21 @@ body.theme-light { } -@media only screen and (max-width: 850px) { - .theme-light .assets-table-link-row:nth-child(odd) .assets-table-row { - color: #fff !important; +/* Asset Table Light Theme */ +.theme-light .assets-table-row{ + color:rgba(21, 24, 28, 1) !important; +} + +.theme-light .tabs input:checked + label{ + color:rgba(21, 24, 28, 1); +} + +.theme-light .asset-summary{ + color:rgba(21, 24, 28, 1); +} + +@media only screen and (max-width: 850px){ + .theme-light .assets-table-link-row:nth-child(odd) .assets-table-row { + color: #fff !important; } -} \ No newline at end of file +}