Merge branch 'master' into mononaut/fix-mobile-bottom-nav
This commit is contained in:
commit
2e7a701ca7
@ -18,6 +18,7 @@ import blocks from '../blocks';
|
||||
import bitcoinClient from './bitcoin-client';
|
||||
import difficultyAdjustment from '../difficulty-adjustment';
|
||||
import transactionRepository from '../../repositories/TransactionRepository';
|
||||
import rbfCache from '../rbf-cache';
|
||||
|
||||
class BitcoinRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@ -31,6 +32,8 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||
try {
|
||||
@ -589,6 +592,28 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getRbfHistory(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getReplaces(req.params.txId);
|
||||
res.json(result || []);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getCachedTx(req: Request, res: Response) {
|
||||
try {
|
||||
const result = rbfCache.getTx(req.params.txId);
|
||||
if (result) {
|
||||
res.json(result);
|
||||
} else {
|
||||
res.status(404).send('not found');
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getTransactionOutspends(req: Request, res: Response) {
|
||||
try {
|
||||
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
||||
|
@ -35,24 +35,31 @@ export class Common {
|
||||
}
|
||||
|
||||
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
|
||||
const arr = [transactions[transactions.length - 1].effectiveFeePerVsize];
|
||||
const filtered: TransactionExtended[] = [];
|
||||
let lastValidRate = Infinity;
|
||||
// filter out anomalous fee rates to ensure monotonic range
|
||||
for (const tx of transactions) {
|
||||
if (tx.effectiveFeePerVsize <= lastValidRate) {
|
||||
filtered.push(tx);
|
||||
lastValidRate = tx.effectiveFeePerVsize;
|
||||
}
|
||||
}
|
||||
const arr = [filtered[filtered.length - 1].effectiveFeePerVsize];
|
||||
const chunk = 1 / (rangeLength - 1);
|
||||
let itemsToAdd = rangeLength - 2;
|
||||
|
||||
while (itemsToAdd > 0) {
|
||||
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].effectiveFeePerVsize);
|
||||
arr.push(filtered[Math.floor(filtered.length * chunk * itemsToAdd)].effectiveFeePerVsize);
|
||||
itemsToAdd--;
|
||||
}
|
||||
|
||||
arr.push(transactions[0].effectiveFeePerVsize);
|
||||
arr.push(filtered[0].effectiveFeePerVsize);
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
||||
const matches: { [txid: string]: TransactionExtended } = {};
|
||||
deleted
|
||||
// The replaced tx must have at least one input with nSequence < maxint-1 (That’s the opt-in)
|
||||
.filter((tx) => tx.vin.some((vin) => vin.sequence < 0xfffffffe))
|
||||
.forEach((deletedTx) => {
|
||||
const foundMatches = added.find((addedTx) => {
|
||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||
@ -61,7 +68,7 @@ export class Common {
|
||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
||||
// Spends one or more of the same inputs
|
||||
&& deletedTx.vin.some((deletedVin) =>
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid));
|
||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||
});
|
||||
if (foundMatches) {
|
||||
matches[deletedTx.txid] = foundMatches;
|
||||
|
@ -461,8 +461,8 @@ class DatabaseMigration {
|
||||
await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions'));
|
||||
try {
|
||||
await this.$convertCompactCpfpTables();
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
||||
await this.updateToSchemaVersion(52);
|
||||
} catch(e) {
|
||||
logger.warn('' + (e instanceof Error ? e.message : e));
|
||||
|
@ -55,11 +55,12 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
||||
clChannelsDict[clChannel.short_channel_id] = clChannel;
|
||||
clChannelsDictCount[clChannel.short_channel_id] = 1;
|
||||
} else {
|
||||
consolidatedChannelList.push(
|
||||
await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
|
||||
);
|
||||
delete clChannelsDict[clChannel.short_channel_id];
|
||||
clChannelsDictCount[clChannel.short_channel_id]++;
|
||||
const fullChannel = await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]);
|
||||
if (fullChannel !== null) {
|
||||
consolidatedChannelList.push(fullChannel);
|
||||
delete clChannelsDict[clChannel.short_channel_id];
|
||||
clChannelsDictCount[clChannel.short_channel_id]++;
|
||||
}
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||
@ -74,7 +75,10 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
||||
channelProcessed = 0;
|
||||
const keys = Object.keys(clChannelsDict);
|
||||
for (const short_channel_id of keys) {
|
||||
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
|
||||
const incompleteChannel = await buildIncompleteChannel(clChannelsDict[short_channel_id]);
|
||||
if (incompleteChannel !== null) {
|
||||
consolidatedChannelList.push(incompleteChannel);
|
||||
}
|
||||
|
||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||
@ -92,10 +96,13 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
||||
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
|
||||
* In this case, clightning knows the channel policy for both nodes
|
||||
*/
|
||||
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
|
||||
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel | null> {
|
||||
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
|
||||
|
||||
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
const parts = clChannelA.short_channel_id.split('x');
|
||||
const outputIdx = parts[2];
|
||||
|
||||
@ -115,8 +122,11 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
|
||||
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
|
||||
* In this case, clightning knows the channel policy of only one node
|
||||
*/
|
||||
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
|
||||
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel | null> {
|
||||
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
const parts = clChannel.short_channel_id.split('x');
|
||||
const outputIdx = parts[2];
|
||||
|
||||
|
@ -210,7 +210,7 @@ class Mempool {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (this.mempoolCache[rbfTransaction]) {
|
||||
// Store replaced transactions
|
||||
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
|
||||
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
|
||||
// Erase the replaced transactions from the local mempool
|
||||
delete this.mempoolCache[rbfTransaction];
|
||||
}
|
||||
@ -236,6 +236,7 @@ class Mempool {
|
||||
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
||||
if (lazyDeleteAt && lazyDeleteAt < now) {
|
||||
delete this.mempoolCache[tx];
|
||||
rbfCache.evict(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,62 @@
|
||||
export interface CachedRbf {
|
||||
txid: string;
|
||||
expires: Date;
|
||||
}
|
||||
import { TransactionExtended } from "../mempool.interfaces";
|
||||
|
||||
class RbfCache {
|
||||
private cache: { [txid: string]: CachedRbf; } = {};
|
||||
private replacedBy: { [txid: string]: string; } = {};
|
||||
private replaces: { [txid: string]: string[] } = {};
|
||||
private txs: { [txid: string]: TransactionExtended } = {};
|
||||
private expiring: { [txid: string]: Date } = {};
|
||||
|
||||
constructor() {
|
||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
public add(replacedTxId: string, newTxId: string): void {
|
||||
this.cache[replacedTxId] = {
|
||||
expires: new Date(Date.now() + 1000 * 604800), // 1 week
|
||||
txid: newTxId,
|
||||
};
|
||||
public add(replacedTx: TransactionExtended, newTxId: string): void {
|
||||
this.replacedBy[replacedTx.txid] = newTxId;
|
||||
this.txs[replacedTx.txid] = replacedTx;
|
||||
if (!this.replaces[newTxId]) {
|
||||
this.replaces[newTxId] = [];
|
||||
}
|
||||
this.replaces[newTxId].push(replacedTx.txid);
|
||||
}
|
||||
|
||||
public get(txId: string): CachedRbf | undefined {
|
||||
return this.cache[txId];
|
||||
public getReplacedBy(txId: string): string | undefined {
|
||||
return this.replacedBy[txId];
|
||||
}
|
||||
|
||||
public getReplaces(txId: string): string[] | undefined {
|
||||
return this.replaces[txId];
|
||||
}
|
||||
|
||||
public getTx(txId: string): TransactionExtended | undefined {
|
||||
return this.txs[txId];
|
||||
}
|
||||
|
||||
// flag a transaction as removed from the mempool
|
||||
public evict(txid): void {
|
||||
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
const currentDate = new Date();
|
||||
for (const c in this.cache) {
|
||||
if (this.cache[c].expires < currentDate) {
|
||||
delete this.cache[c];
|
||||
for (const txid in this.expiring) {
|
||||
if (this.expiring[txid] < currentDate) {
|
||||
delete this.expiring[txid];
|
||||
this.remove(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove a transaction & all previous versions from the cache
|
||||
private remove(txid): void {
|
||||
// don't remove a transaction while a newer version remains in the mempool
|
||||
if (this.replaces[txid] && !this.replacedBy[txid]) {
|
||||
const replaces = this.replaces[txid];
|
||||
delete this.replaces[txid];
|
||||
for (const tx of replaces) {
|
||||
// recursively remove prior versions from the cache
|
||||
delete this.replacedBy[tx];
|
||||
delete this.txs[tx];
|
||||
this.remove(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,10 +58,10 @@ class WebsocketHandler {
|
||||
client['track-tx'] = parsedMessage['track-tx'];
|
||||
// Client is telling the transaction wasn't found
|
||||
if (parsedMessage['watch-mempool']) {
|
||||
const rbfCacheTx = rbfCache.get(client['track-tx']);
|
||||
if (rbfCacheTx) {
|
||||
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
|
||||
if (rbfCacheTxid) {
|
||||
response['txReplaced'] = {
|
||||
txid: rbfCacheTx.txid,
|
||||
txid: rbfCacheTxid,
|
||||
};
|
||||
client['track-tx'] = null;
|
||||
} else {
|
||||
@ -467,6 +467,7 @@ class WebsocketHandler {
|
||||
for (const txId of txIds) {
|
||||
delete _memPool[txId];
|
||||
removed.push(txId);
|
||||
rbfCache.evict(txId);
|
||||
}
|
||||
|
||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||
|
@ -210,6 +210,9 @@ class NetworkSyncService {
|
||||
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
||||
for (const channel of channels) {
|
||||
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
|
||||
if (!transaction) {
|
||||
continue;
|
||||
}
|
||||
await DB.query(`
|
||||
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
|
||||
[transaction.timestamp, channel.id]
|
||||
|
@ -71,7 +71,7 @@ class FundingTxFetcher {
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
|
||||
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number} | null> {
|
||||
channelId = Common.channelIntegerIdToShortId(channelId);
|
||||
|
||||
if (this.fundingTxCache[channelId]) {
|
||||
@ -102,6 +102,11 @@ class FundingTxFetcher {
|
||||
const rawTx = await bitcoinClient.getRawTransaction(txid);
|
||||
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
|
||||
|
||||
if (!tx || !tx.vout || tx.vout.length < parseInt(outputIdx, 10) + 1 || tx.vout[outputIdx].value === undefined) {
|
||||
logger.err(`Cannot find blockchain funding tx for channel id ${channelId}. Possible reasons are: bitcoin backend timeout or the channel shortId is not valid`);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.fundingTxCache[channelId] = {
|
||||
timestamp: block.time,
|
||||
txid: txid,
|
||||
|
3
contributors/piterden.txt
Normal file
3
contributors/piterden.txt
Normal file
@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 17, 2022.
|
||||
|
||||
Signed: piterden
|
@ -17,7 +17,7 @@ _Note: address lookups require an Electrum Server and will not work with this co
|
||||
|
||||
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
|
||||
|
||||
```
|
||||
```ini
|
||||
txindex=1
|
||||
server=1
|
||||
rpcuser=mempool
|
||||
@ -26,7 +26,7 @@ rpcpassword=mempool
|
||||
|
||||
If you want to use different credentials, specify them in the `docker-compose.yml` file:
|
||||
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
MEMPOOL_BACKEND: "none"
|
||||
@ -54,7 +54,7 @@ First, configure `bitcoind` as specified above, and make sure your Electrum Serv
|
||||
|
||||
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
|
||||
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
MEMPOOL_BACKEND: "electrum"
|
||||
@ -85,7 +85,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"MEMPOOL": {
|
||||
"NETWORK": "mainnet",
|
||||
"BACKEND": "electrum",
|
||||
@ -116,7 +116,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
MEMPOOL_NETWORK: ""
|
||||
@ -153,8 +153,8 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
"CORE_RPC": {
|
||||
```json
|
||||
"CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
"USERNAME": "mempool",
|
||||
@ -163,7 +163,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
CORE_RPC_HOST: ""
|
||||
@ -176,7 +176,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"ELECTRUM": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 50002,
|
||||
@ -185,7 +185,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
ELECTRUM_HOST: ""
|
||||
@ -197,14 +197,14 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"ESPLORA": {
|
||||
"REST_API_URL": "http://127.0.0.1:3000"
|
||||
},
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
ESPLORA_REST_API_URL: ""
|
||||
@ -214,7 +214,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"SECOND_CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": 8332,
|
||||
@ -224,7 +224,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
SECOND_CORE_RPC_HOST: ""
|
||||
@ -237,7 +237,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"DATABASE": {
|
||||
"ENABLED": true,
|
||||
"HOST": "127.0.0.1",
|
||||
@ -249,7 +249,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
DATABASE_ENABLED: ""
|
||||
@ -264,7 +264,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"SYSLOG": {
|
||||
"ENABLED": true,
|
||||
"HOST": "127.0.0.1",
|
||||
@ -275,7 +275,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
SYSLOG_ENABLED: ""
|
||||
@ -289,7 +289,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"STATISTICS": {
|
||||
"ENABLED": true,
|
||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||
@ -297,7 +297,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
STATISTICS_ENABLED: ""
|
||||
@ -308,7 +308,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"BISQ": {
|
||||
"ENABLED": false,
|
||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||
@ -316,7 +316,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
BISQ_ENABLED: ""
|
||||
@ -327,7 +327,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"SOCKS5PROXY": {
|
||||
"ENABLED": false,
|
||||
"HOST": "127.0.0.1",
|
||||
@ -338,7 +338,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
SOCKS5PROXY_ENABLED: ""
|
||||
@ -352,7 +352,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"PRICE_DATA_SERVER": {
|
||||
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
||||
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
||||
@ -360,7 +360,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
PRICE_DATA_SERVER_TOR_URL: ""
|
||||
@ -371,7 +371,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"LIGHTNING": {
|
||||
"ENABLED": false
|
||||
"BACKEND": "lnd"
|
||||
@ -383,7 +383,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
LIGHTNING_ENABLED: false
|
||||
@ -398,7 +398,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"LND": {
|
||||
"TLS_CERT_PATH": ""
|
||||
"MACAROON_PATH": ""
|
||||
@ -407,7 +407,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
LND_TLS_CERT_PATH: ""
|
||||
@ -419,14 +419,14 @@ Corresponding `docker-compose.yml` overrides:
|
||||
<br/>
|
||||
|
||||
`mempool-config.json`:
|
||||
```
|
||||
```json
|
||||
"CLIGHTNING": {
|
||||
"SOCKET": ""
|
||||
}
|
||||
```
|
||||
|
||||
Corresponding `docker-compose.yml` overrides:
|
||||
```
|
||||
```yaml
|
||||
api:
|
||||
environment:
|
||||
CLIGHTNING_SOCKET: ""
|
||||
|
@ -138,6 +138,10 @@
|
||||
"translation": "src/locale/messages.hi.xlf",
|
||||
"baseHref": "/hi/"
|
||||
},
|
||||
"ne": {
|
||||
"translation": "src/locale/messages.ne.xlf",
|
||||
"baseHref": "/ne/"
|
||||
},
|
||||
"lt": {
|
||||
"translation": "src/locale/messages.lt.xlf",
|
||||
"baseHref": "/lt/"
|
||||
|
@ -116,6 +116,7 @@ export const languages: Language[] = [
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||
{ code: 'it', name: 'Italiano' }, // Italian
|
||||
{ code: 'he', name: 'עברית' }, // Hebrew
|
||||
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||
|
@ -1,11 +1,9 @@
|
||||
<div class="container-xl">
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<span class="address-link">
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]">
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
<br>
|
||||
|
||||
|
@ -6,12 +6,12 @@
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
</div>
|
||||
|
||||
<span class="tx-link float-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, bisqTx.id]">
|
||||
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
||||
<span class="tx-link">
|
||||
<span class="txid">
|
||||
<app-truncate [text]="bisqTx.id" [lastChars]="12" [link]="['/tx/' | relativeUrl, bisqTx.id]">
|
||||
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
</span>
|
||||
<span class="grow"></span>
|
||||
<div class="container-buttons">
|
||||
|
@ -6,17 +6,16 @@
|
||||
<div class="col-md">
|
||||
<div class="row d-flex justify-content-between">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="title truncated"><span class="first">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span></h1>
|
||||
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
|
||||
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="7" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]"></app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="!address.electrum">
|
||||
<tr>
|
||||
|
@ -2,11 +2,9 @@
|
||||
<div class="title-address">
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<div class="tx-link">
|
||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" >
|
||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,10 +19,11 @@
|
||||
<tbody>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
|
||||
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||
</app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="!address.electrum">
|
||||
<tr>
|
||||
|
@ -2,11 +2,9 @@
|
||||
<div class="title-asset">
|
||||
<h1 i18n="asset|Liquid Asset page title">Asset</h1>
|
||||
<div class="tx-link">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, assetString]">
|
||||
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ assetString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="assetString"></app-clipboard>
|
||||
<app-truncate [text]="assetString" [lastChars]="8" [link]="['/assets/asset/' | relativeUrl, assetString]">
|
||||
<app-clipboard [text]="assetString"></app-clipboard>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -213,6 +213,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
legend: (data.series.length === 0) ? undefined : {
|
||||
padding: [10, 75],
|
||||
data: data.legends,
|
||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
||||
'Min': true,
|
||||
|
@ -10,12 +10,13 @@ const defaultHoverColor = hexToColor('1bd8f4');
|
||||
|
||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||
const auditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
}
|
||||
};
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||
@ -161,13 +162,13 @@ export default class TxView implements TransactionStripped {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
return auditColors.missing;
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return auditColors.selected;
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'found':
|
||||
if (this.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
|
@ -35,12 +35,12 @@
|
||||
<tr *ngIf="tx && tx.status && tx.status.length">
|
||||
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
|
||||
<ng-container [ngSwitch]="tx?.status">
|
||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
|
||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">Match</td>
|
||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">Removed</td>
|
||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">Marginal fee rate</td>
|
||||
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</td>
|
||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">Added</td>
|
||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">Marginal fee rate</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -4,8 +4,7 @@ import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { Location } from '@angular/common';
|
||||
import { config } from 'process';
|
||||
import { CacheService } from 'src/app/services/cache.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
interface BlockchainBlock extends BlockExtended {
|
||||
placeholder?: boolean;
|
||||
|
@ -64,26 +64,6 @@
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
|
||||
&.truncated {
|
||||
text-overflow: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
.first {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.last-four {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .title-wrapper {
|
||||
|
@ -13,21 +13,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.rewards-per-tx" i18n-ngbTooltip="mining.rewards-per-tx"
|
||||
ngbTooltip="Reward Per Tx" placement="bottom" #rewardspertx [disableTooltip]="!isEllipsisActive(rewardspertx)">Reward Per Tx</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" ngbTooltip="Average miners' reward per transaction in the past 144 blocks" placement="bottom">
|
||||
<h5 class="card-title" i18n="mining.fees-per-block" i18n-ngbTooltip="mining.fees-per-block"
|
||||
ngbTooltip="Avg Block Fees" placement="bottom" #rewardsperblock [disableTooltip]="!isEllipsisActive(rewardsperblock)">Avg Block Fees</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.fees-per-block-desc" ngbTooltip="Average fees per block in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">
|
||||
{{ rewardStats.rewardPerTx | amountShortener: 2 }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>
|
||||
{{ (rewardStats.feePerBlock / 100000000) | amountShortener: 4 }}
|
||||
<span i18n="shared.btc-block|BTC/block">BTC/block</span>
|
||||
</div>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="rewardStats.rewardPerTx"></app-fiat>
|
||||
<app-fiat [value]="rewardStats.feePerBlock"></app-fiat>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="mining.average-fee" i18n-ngbTooltip="mining.average-fee"
|
||||
ngbTooltip="Average Fee" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Average Fee</h5>
|
||||
ngbTooltip="Avg Tx Fee" placement="bottom" #averagefee [disableTooltip]="!isEllipsisActive(averagefee)">Avg Tx Fee</h5>
|
||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
|
||||
<div class="fee-text">{{ rewardStats.feePerTx | amountShortener: 2 }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats/tx</span>
|
||||
|
@ -42,8 +42,8 @@ export class RewardStatsComponent implements OnInit {
|
||||
map((stats) => {
|
||||
return {
|
||||
totalReward: stats.totalReward,
|
||||
rewardPerTx: stats.totalReward / stats.totalTx,
|
||||
feePerTx: stats.totalFee / stats.totalTx,
|
||||
feePerBlock: stats.totalFee / 144,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -4,7 +4,7 @@
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="title truncated"><span class="first">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span></h1>
|
||||
<h1 class="title truncated"><app-truncate [text]="txId"></app-truncate></h1>
|
||||
</div>
|
||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||
<app-tx-features [tx]="tx"></app-tx-features>
|
||||
|
@ -3,21 +3,25 @@
|
||||
<div class="title-block">
|
||||
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]">
|
||||
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
|
||||
</a>
|
||||
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size">
|
||||
<div *ngIf="rbfReplaces?.length" class="alert alert-mempool" role="alert">
|
||||
<span i18n="transaction.rbf.replaced|RBF replaced">This transaction replaced:</span>
|
||||
<div class="tx-list">
|
||||
<app-truncate [text]="replaced" [lastChars]="12" *ngFor="let replaced of rbfReplaces" [link]="['/tx/' | relativeUrl, replaced]"></app-truncate>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
<span class="tx-link float-left">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txId]">
|
||||
<span class="d-inline d-lg-none">{{ txId | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ txId }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="txId"></app-clipboard>
|
||||
<span class="tx-link">
|
||||
<span class="txid">
|
||||
<app-truncate [text]="txId" [lastChars]="12" [link]="['/tx/' | relativeUrl, txId]">
|
||||
<app-clipboard [text]="txId"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="container-buttons">
|
||||
@ -28,7 +32,10 @@
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="tx && !tx?.status.confirmed">
|
||||
<ng-template [ngIf]="tx && !tx?.status?.confirmed && replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="tx && !tx?.status?.confirmed && !replaced">
|
||||
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
@ -91,7 +98,7 @@
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<ng-template [ngIf]="transactionTime !== 0">
|
||||
<ng-template [ngIf]="transactionTime !== 0 && !replaced">
|
||||
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
@ -103,7 +110,7 @@
|
||||
</tr>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<tr *ngIf="!replaced">
|
||||
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||
<td>
|
||||
<ng-template [ngIf]="txInBlockIndex === undefined" [ngIfElse]="estimationTmpl">
|
||||
@ -144,12 +151,12 @@
|
||||
<br>
|
||||
|
||||
<h2 class="text-left">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="xs"></fa-icon></h2>
|
||||
<div class="box">
|
||||
<table class="table table-borderless table-striped">
|
||||
<div class="box cpfp-details">
|
||||
<table class="table table-fixed table-borderless table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th i18n="transactions-list.vout.scriptpubkey-type">Type</th>
|
||||
<th i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="txids" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</th>
|
||||
<th i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<th class="d-none d-lg-table-cell"></th>
|
||||
@ -159,10 +166,8 @@
|
||||
<ng-template [ngIf]="cpfpInfo?.descendants?.length">
|
||||
<tr *ngFor="let cpfpTx of cpfpInfo.descendants">
|
||||
<td><span class="badge badge-primary" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
|
||||
</a>
|
||||
<td>
|
||||
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
@ -172,11 +177,8 @@
|
||||
<ng-template [ngIf]="cpfpInfo?.bestDescendant">
|
||||
<tr>
|
||||
<td><span class="badge badge-success" i18n="transaction.descendant|Descendant">Descendant</span></td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]">
|
||||
<span class="d-inline d-lg-none">{{ cpfpInfo.bestDescendant.txid | shortenString : 8 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ cpfpInfo.bestDescendant.txid }}</span>
|
||||
</a>
|
||||
<td class="txids">
|
||||
<app-truncate [text]="cpfpInfo.bestDescendant.txid" [link]="['/tx' | relativeUrl, cpfpInfo.bestDescendant.txid]"></app-truncate>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpInfo.bestDescendant.weight / 4 | vbytes: 2"></td>
|
||||
<td>{{ cpfpInfo.bestDescendant.fee / (cpfpInfo.bestDescendant.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
@ -186,10 +188,8 @@
|
||||
<ng-template [ngIf]="cpfpInfo?.ancestors?.length">
|
||||
<tr *ngFor="let cpfpTx of cpfpInfo.ancestors">
|
||||
<td><span class="badge badge-primary" i18n="transaction.ancestor|Transaction Ancestor">Ancestor</span></td>
|
||||
<td><a [routerLink]="['/tx' | relativeUrl, cpfpTx.txid]">
|
||||
<span class="d-inline d-lg-none">{{ cpfpTx.txid | shortenString : 8 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ cpfpTx.txid }}</span>
|
||||
</a>
|
||||
<td class="txids">
|
||||
<app-truncate [text]="cpfpTx.txid" [link]="['/tx' | relativeUrl, cpfpTx.txid]"></app-truncate>
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell" [innerHTML]="cpfpTx.weight / 4 | vbytes: 2"></td>
|
||||
<td>{{ cpfpTx.fee / (cpfpTx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
|
@ -19,22 +19,32 @@
|
||||
}
|
||||
}
|
||||
.tx-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
width: 0;
|
||||
max-width: 100%;
|
||||
margin-right: 0px;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
@media (min-width: 651px) {
|
||||
display: flex;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0px;
|
||||
margin-right: 1em;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
width: 100%;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.txid {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.td-width {
|
||||
@ -188,4 +198,16 @@
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cpfp-details {
|
||||
.txids {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-list {
|
||||
.alert-link {
|
||||
display: block;
|
||||
}
|
||||
}
|
@ -40,15 +40,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
fetchRbfSubscription: Subscription;
|
||||
fetchCachedTxSubscription: Subscription;
|
||||
txReplacedSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
replaced: boolean = false;
|
||||
rbfReplaces: string[];
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
showCpfpDetails = false;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
now = new Date().getTime();
|
||||
timeAvg$: Observable<number>;
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
@ -122,7 +128,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
delay(2000)
|
||||
)))
|
||||
)),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
@ -155,6 +165,49 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
});
|
||||
|
||||
this.fetchRbfSubscription = this.fetchRbfHistory$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
this.apiService
|
||||
.getRbfHistory$(txId)
|
||||
),
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
})
|
||||
).subscribe((replaces) => {
|
||||
this.rbfReplaces = replaces;
|
||||
});
|
||||
|
||||
this.fetchCachedTxSubscription = this.fetchCachedTx$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
this.apiService
|
||||
.getRbfCachedTx$(txId)
|
||||
),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe((tx) => {
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tx = tx;
|
||||
if (tx.fee === undefined) {
|
||||
this.tx.fee = 0;
|
||||
}
|
||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||
this.isLoadingTx = false;
|
||||
this.error = undefined;
|
||||
this.waitingForTransaction = false;
|
||||
this.graphExpanded = false;
|
||||
this.setupGraph();
|
||||
|
||||
if (!this.tx?.status?.confirmed) {
|
||||
this.fetchRbfHistory$.next(this.tx.txid);
|
||||
}
|
||||
});
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
@ -268,6 +321,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
} else {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
this.fetchRbfHistory$.next(this.tx.txid);
|
||||
}
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
},
|
||||
@ -299,6 +353,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
this.rbfTransaction = rbfTransaction;
|
||||
this.cacheService.setTxCache([this.rbfTransaction]);
|
||||
this.replaced = true;
|
||||
if (rbfTransaction && !this.tx) {
|
||||
this.fetchCachedTx$.next(this.txId);
|
||||
}
|
||||
});
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
@ -364,8 +422,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.waitingForTransaction = false;
|
||||
this.isLoadingTx = true;
|
||||
this.rbfTransaction = undefined;
|
||||
this.replaced = false;
|
||||
this.transactionTime = -1;
|
||||
this.cpfpInfo = null;
|
||||
this.rbfReplaces = [];
|
||||
this.showCpfpDetails = false;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.leaveTransaction();
|
||||
@ -431,6 +491,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.fetchCpfpSubscription.unsubscribe();
|
||||
this.fetchRbfSubscription.unsubscribe();
|
||||
this.fetchCachedTxSubscription.unsubscribe();
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
|
@ -1,16 +1,14 @@
|
||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
||||
<div *ngIf="!transactionPage" class="header-bg box tx-page-container">
|
||||
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
|
||||
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
|
||||
<app-truncate [text]="tx.txid"></app-truncate>
|
||||
</a>
|
||||
<div class="float-right">
|
||||
<div>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
|
||||
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
|
||||
<i><app-time-since [time]="tx.firstSeen" [fastRender]="true"></app-time-since></i>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="header-bg box" infiniteScroll [alwaysCallback]="true" [infiniteScrollDistance]="2" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="onScroll()" [attr.data-cy]="'tx-' + i">
|
||||
@ -18,7 +16,7 @@
|
||||
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
||||
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
@ -49,7 +47,7 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</td>
|
||||
<td>
|
||||
<td class="address-cell">
|
||||
<div [ngSwitch]="true">
|
||||
<ng-container *ngSwitchCase="vin.is_coinbase"><span i18n="transactions-list.coinbase">Coinbase</span><ng-template [ngIf]="network !== 'liquid' && network !== 'liquidtestnet'"> <span i18n="transactions-list.newly-generated-coins">(Newly Generated Coins)</span></ng-template><br /><a placement="bottom" [ngbTooltip]="vin.scriptsig | hex2ascii"><span class="badge badge-secondary scriptmessage longer">{{ vin.scriptsig | hex2ascii }}</span></a></ng-container>
|
||||
<ng-container *ngSwitchCase="vin.is_pegin">
|
||||
@ -66,12 +64,8 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #defaultAddress>
|
||||
<a class="shortable-address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-inline-flex justify-content-start">
|
||||
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
|
||||
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
|
||||
</span>
|
||||
<a class="address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<ng-template #vinScriptPubkeyType>
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
@ -100,7 +94,7 @@
|
||||
</tr>
|
||||
<tr *ngIf="(showDetails$ | async) === true">
|
||||
<td colspan="3" class="details-container" >
|
||||
<table class="table table-striped table-borderless details-table mb-3">
|
||||
<table class="table table-striped table-fixed table-borderless details-table mb-3">
|
||||
<tbody>
|
||||
<ng-template [ngIf]="vin.scriptsig">
|
||||
<tr>
|
||||
@ -112,9 +106,23 @@
|
||||
<td style="text-align: left;">{{ vin.scriptsig }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="vin.witness">
|
||||
<tr *ngIf="vin.witness" class="vin-witness">
|
||||
<td i18n="transactions-list.witness">Witness</td>
|
||||
<td style="text-align: left;">{{ vin.witness.join(' ') }}</td>
|
||||
<td style="text-align: left;">
|
||||
<ng-container *ngFor="let witness of vin.witness; index as i">
|
||||
<input type="checkbox" [id]="'tx' + vindex + 'witness' + i" style="display: none;">
|
||||
<p class="witness-item" [class.accordioned]="witness.length > 1000">
|
||||
{{ witness }}
|
||||
</p>
|
||||
<div class="witness-toggle" *ngIf="witness.length > 1000">
|
||||
<span class="ellipsis">...</span>
|
||||
<label [for]="'tx' + vindex + 'witness' + i" class="btn btn-sm btn-primary mt-2">
|
||||
<span class="show-all" i18n="show-all">Show all</span>
|
||||
<span class="show-less" i18n="show-less">Show less</span>
|
||||
</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="vin.inner_redeemscript_asm">
|
||||
<td i18n="transactions-list.p2sh-redeem-script">P2SH redeem script</td>
|
||||
@ -153,7 +161,7 @@
|
||||
<ng-template #showMoreInputsLabel>
|
||||
<span i18n="show-more">Show more</span>
|
||||
</ng-template>
|
||||
({{ tx.vin.length - getVinLimit(tx) }} <span i18n="inputs-remaining">remaining</span>)
|
||||
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: tx.vin.length - getVinLimit(tx)}"></ng-container>)
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -162,20 +170,16 @@
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col mobile-bottomcol">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vout">
|
||||
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vout">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
}">
|
||||
<td>
|
||||
<a class="shortable-address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-inline-flex justify-content-start">
|
||||
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
|
||||
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
|
||||
</span>
|
||||
<td class="address-cell">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||
@ -185,13 +189,11 @@
|
||||
<ng-container i18n="transactions-list.peg-out-to">Peg-out to <ng-container *ngTemplateOutlet="pegOutLink"></ng-container></ng-container>
|
||||
<ng-template #pegOutLink>
|
||||
<a *ngIf="stateService.env.BASE_MODULE === 'liquid'; else localPegoutLink" [attr.href]="'https://mempool.space/address/' + vout.pegout.scriptpubkey_address" title="{{ vout.pegout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vout.pegout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-block">{{ vout.pegout.scriptpubkey_address | shortenString : 35 }}</span>
|
||||
<app-truncate [text]="vout.pegout.scriptpubkey_address"></app-truncate>
|
||||
</a>
|
||||
<ng-template #localPegoutLink>
|
||||
<a [routerLink]="['/address/', vout.pegout.scriptpubkey_address]" title="{{ vout.pegout.scriptpubkey_address }}">
|
||||
<span class="d-block d-lg-none">{{ vout.pegout.scriptpubkey_address | shortenString : 16 }}</span>
|
||||
<span class="d-none d-lg-block">{{ vout.pegout.scriptpubkey_address | shortenString : 35 }}</span>
|
||||
<app-truncate [text]="vout.pegout.scriptpubkey_address"></app-truncate>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@ -270,7 +272,7 @@
|
||||
<ng-template #showMoreOutputsLabel>
|
||||
<span i18n="show-more">Show more</span>
|
||||
</ng-template>
|
||||
({{ tx.vout.length - getVoutLimit(tx) }} <span i18n="outputs-remaining">remaining</span>)
|
||||
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: tx.vout.length - getVoutLimit(tx)}"></ng-container>)
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -324,3 +326,5 @@
|
||||
<br />
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
.arrow-td {
|
||||
width: 20px;
|
||||
width: 30px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@ -45,6 +45,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
td.amount {
|
||||
width: 32.5%;
|
||||
}
|
||||
|
||||
.extra-info {
|
||||
display: none;
|
||||
@media (min-width: 576px) {
|
||||
@ -81,6 +85,10 @@
|
||||
}
|
||||
|
||||
.tx-page-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
white-space: nowrap;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
@ -97,9 +105,7 @@
|
||||
&:first-child {
|
||||
color: #ffffff66;
|
||||
white-space: pre-wrap;
|
||||
@media (min-width: 476px) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
width: 150px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
word-break: break-all;
|
||||
@ -130,14 +136,7 @@ h2 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.addr-left {
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: -7px
|
||||
}
|
||||
|
||||
.addr-right {
|
||||
.address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@ -146,3 +145,50 @@ h2 {
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
margin-inline-end: 2em;
|
||||
}
|
||||
|
||||
.vin-witness {
|
||||
.witness-item.accordioned {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input:checked + .witness-item.accordioned {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.witness-toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.show-all {
|
||||
display: inline;
|
||||
}
|
||||
.show-less {
|
||||
display: none;
|
||||
}
|
||||
.ellipsis {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked ~ .witness-toggle {
|
||||
.show-all {
|
||||
display: none;
|
||||
}
|
||||
.show-less {
|
||||
display: inline;
|
||||
}
|
||||
.ellipsis {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
@ -31,8 +31,7 @@
|
||||
<p *ngIf="!isConnector">Peg Out</p>
|
||||
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
|
||||
<p class="address">
|
||||
<span class="first">{{ line.pegout.slice(0, -4) }}</span>
|
||||
<span class="last-four">{{ line.pegout.slice(-4) }}</span>
|
||||
<app-truncate [text]="line.pegout"></app-truncate>
|
||||
</p>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
@ -49,8 +48,7 @@
|
||||
<ng-container *ngIf="isConnector && line.txid">
|
||||
<p>
|
||||
<span i18n="transaction">Transaction</span>
|
||||
<span class="first">{{ line.txid.slice(0, 8) }}</span>...
|
||||
<span class="last-four">{{ line.txid.slice(-4) }}</span>
|
||||
<app-truncate [text]="line.txid"></app-truncate>
|
||||
</p>
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span> #{{ line.vout + 1 }}</p>
|
||||
@ -60,8 +58,7 @@
|
||||
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
|
||||
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
|
||||
<p *ngIf="line.type !== 'fee' && line.address" class="address">
|
||||
<span class="first">{{ line.address.slice(0, -4) }}</span>
|
||||
<span class="last-four">{{ line.address.slice(-4) }}</span>
|
||||
<app-truncate [text]="line.address"></app-truncate>
|
||||
</p>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
@ -17,22 +17,5 @@
|
||||
.address {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
|
||||
.first {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
.last-four {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -127,7 +127,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||
<td class="table-cell-txid"><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-fees">{{ transaction.fee / transaction.vsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
|
@ -12,7 +12,8 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
|
||||
routes = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'api/rest'
|
||||
redirectTo: 'api/rest',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'api/:type',
|
||||
@ -20,11 +21,13 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
redirectTo: 'api/rest'
|
||||
redirectTo: 'api/rest',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'api/rest'
|
||||
redirectTo: 'api/rest',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
} else {
|
||||
|
@ -1,10 +1,9 @@
|
||||
<div class="mb-2 box-top">
|
||||
<div class="box-left text-truncate">
|
||||
<h3 class="mb-0 text-truncate">{{ channel.alias || '?' }}</h3>
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
|
||||
{{ channel.public_key | shortenString : 12 }}
|
||||
</a>
|
||||
<app-clipboard [text]="channel.public_key"></app-clipboard>
|
||||
<app-truncate [text]="channel.public_key" [lastChars]="6" [link]="['/lightning/node' | relativeUrl, channel.public_key]">
|
||||
<app-clipboard [text]="channel.public_key"></app-clipboard>
|
||||
</app-truncate>
|
||||
</div>
|
||||
<div class="box-right">
|
||||
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 767.98px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@ -10,16 +10,13 @@
|
||||
.tx-link {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@media (min-width: 650px) {
|
||||
@media (min-width: 768px) {
|
||||
top: 1px;
|
||||
position: relative;
|
||||
align-self: end;
|
||||
margin-left: 15px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
margin-bottom: 4px;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
order: 2;
|
||||
|
@ -49,10 +49,9 @@
|
||||
<td class="alias text-left">
|
||||
<div>{{ node.alias || '?' }}</div>
|
||||
<div class="second-line">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
<span>{{ node.public_key | shortenString : publicKeySize }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
||||
<app-truncate [text]="node.public_key" [maxWidth]="200" [lastChars]="6" [link]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</td>
|
||||
<td class="alias text-left d-none d-md-table-cell">
|
||||
|
@ -56,7 +56,7 @@
|
||||
|
||||
<!-- Top nodes per capacity -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 409px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
@ -70,7 +70,7 @@
|
||||
|
||||
<!-- Top nodes per channels -->
|
||||
<div class="col">
|
||||
<div class="card" style="height: 409px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
|
@ -3,11 +3,11 @@
|
||||
<div class="title-container mb-2" *ngIf="!error">
|
||||
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
|
||||
<span class="tx-link">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
<span class="d-inline d-lg-none">{{ node.public_key | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ node.public_key }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||
<span class="node-id">
|
||||
<app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -215,7 +215,7 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasDetails" class="text-right mt-3">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="node.details|Node Details">Details</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!error">
|
||||
|
@ -8,6 +8,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.node-id {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
|
@ -21,7 +21,7 @@
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px">
|
||||
<table class="table table-borderless table-fixed text-center m-auto" style="max-width: 900px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left rank" i18n="mining.rank">Rank</th>
|
||||
|
@ -42,14 +42,14 @@
|
||||
}
|
||||
|
||||
.rank {
|
||||
width: 20%;
|
||||
width: 8%;
|
||||
@media (max-width: 576px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 20%;
|
||||
width: 36%;
|
||||
@media (max-width: 576px) {
|
||||
width: 80%;
|
||||
max-width: 150px;
|
||||
@ -59,21 +59,21 @@
|
||||
}
|
||||
|
||||
.share {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
@media (max-width: 576px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.nodes {
|
||||
width: 20%;
|
||||
width: 15%;
|
||||
@media (max-width: 576px) {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.capacity {
|
||||
width: 20%;
|
||||
width: 26%;
|
||||
@media (max-width: 576px) {
|
||||
width: 10%;
|
||||
max-width: 100px;
|
||||
@ -91,3 +91,8 @@ a:hover .link {
|
||||
.flag {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.text-truncate .link {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@
|
||||
<app-toggle [textLeft]="'Sort by nodes'" [textRight]="'capacity'" [checked]="true" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px" *ngIf="!widget">
|
||||
<table class="table table-borderless table-fixed text-center m-auto" style="max-width: 900px" *ngIf="!widget">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="rank text-left pl-0" i18n="mining.rank">Rank</th>
|
||||
|
@ -4,7 +4,7 @@
|
||||
</h1>
|
||||
|
||||
<div [class]="widget ? 'widget' : 'full'">
|
||||
<table class="table table-borderless">
|
||||
<table class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="rank"></th>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
@ -29,10 +29,10 @@
|
||||
{{ node.channels | number }}
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||
|
@ -1,7 +1,7 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
padding-bottom: 100px;
|
||||
@media (min-width: 767.98px) {
|
||||
@media (min-width: 960px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
@ -15,40 +15,44 @@
|
||||
width: 5%;
|
||||
}
|
||||
.widget .rank {
|
||||
@media (min-width: 767.98px) {
|
||||
@media (min-width: 960px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .alias {
|
||||
width: 10%;
|
||||
width: 20%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
@media (max-width: 960px) {
|
||||
width: 40%;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
.widget .alias {
|
||||
width: 55%;
|
||||
width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .capacity {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
.widget .capacity {
|
||||
width: 32%;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
@ -57,28 +61,31 @@
|
||||
.full .channels {
|
||||
width: 15%;
|
||||
padding-right: 50px;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-first {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-update {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .location {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 15%;
|
||||
@media (max-width: 960px) {
|
||||
width: 30%;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -29,10 +29,10 @@
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt" [hideTimeSince]="true"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||
|
@ -1,7 +1,7 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
padding-bottom: 100px;
|
||||
@media (min-width: 767.98px) {
|
||||
@media (min-width: 960px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
@ -15,70 +15,77 @@
|
||||
width: 5%;
|
||||
}
|
||||
.widget .rank {
|
||||
@media (min-width: 767.98px) {
|
||||
@media (min-width: 960px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .alias {
|
||||
width: 10%;
|
||||
width: 20%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
@media (max-width: 960px) {
|
||||
width: 40%;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
.widget .alias {
|
||||
width: 55%;
|
||||
width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .channels {
|
||||
.full .capacity {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
.widget .channels {
|
||||
.widget .capacity {
|
||||
width: 32%;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .capacity {
|
||||
.full .channels {
|
||||
width: 15%;
|
||||
padding-right: 50px;
|
||||
@media (max-width: 767.98px) {
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-first {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-update {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 10%;
|
||||
@media (max-width: 960px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .location {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 15%;
|
||||
@media (max-width: 960px) {
|
||||
width: 30%;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITrans
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { Outspend } from '../interfaces/electrs.interface';
|
||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -119,6 +119,14 @@ export class ApiService {
|
||||
return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address);
|
||||
}
|
||||
|
||||
getRbfHistory$(txid: string): Observable<string[]> {
|
||||
return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/replaces');
|
||||
}
|
||||
|
||||
getRbfCachedTx$(txid: string): Observable<Transaction> {
|
||||
return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/cached');
|
||||
}
|
||||
|
||||
listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
|
||||
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<span *ngIf="seconds === undefined">-</span>
|
||||
<span *ngIf="seconds !== undefined">
|
||||
‎{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
|
||||
<div class="lg-inline">
|
||||
<div class="lg-inline" *ngIf="!hideTimeSince">
|
||||
<i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -10,6 +10,7 @@ export class TimestampComponent implements OnChanges {
|
||||
@Input() unixTime: number;
|
||||
@Input() dateString: string;
|
||||
@Input() customFormat: string;
|
||||
@Input() hideTimeSince: boolean = false;
|
||||
|
||||
seconds: number | undefined = undefined;
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null">
|
||||
<ng-container *ngIf="link">
|
||||
<a [routerLink]="link" class="truncate-link">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!link">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
</ng-container>
|
||||
<ng-content></ng-content>
|
||||
</span>
|
||||
|
||||
<ng-template #ltrTruncated>
|
||||
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #rtlTruncated>
|
||||
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
|
||||
</ng-template>
|
@ -0,0 +1,26 @@
|
||||
.truncate {
|
||||
text-overflow: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
|
||||
.truncate-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.first {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.last-four {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-truncate',
|
||||
templateUrl: './truncate.component.html',
|
||||
styleUrls: ['./truncate.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TruncateComponent {
|
||||
@Input() text: string;
|
||||
@Input() link: any = null;
|
||||
@Input() lastChars: number = 4;
|
||||
@Input() maxWidth: number = null;
|
||||
rtl: boolean;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.rtl = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,6 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
})
|
||||
export class Decimal2HexPipe implements PipeTransform {
|
||||
transform(decimal: number): string {
|
||||
return `0x` + decimal.toString(16);
|
||||
return `0x` + ( decimal.toString(16) ).padStart(8, '0');
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
|
||||
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
|
||||
import { ChangeComponent } from '../components/change/change.component';
|
||||
import { SatsComponent } from './components/sats/sats.component';
|
||||
import { TruncateComponent } from './components/truncate/truncate.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
@ -152,6 +153,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
SvgImagesComponent,
|
||||
ChangeComponent,
|
||||
SatsComponent,
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
@ -252,6 +254,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
||||
SvgImagesComponent,
|
||||
ChangeComponent,
|
||||
SatsComponent,
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6425
frontend/src/locale/messages.da.xlf
Normal file
6425
frontend/src/locale/messages.da.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6744
frontend/src/locale/messages.ne.xlf
Normal file
6744
frontend/src/locale/messages.ne.xlf
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -106,6 +106,7 @@ http {
|
||||
~*^vi vi;
|
||||
~*^zh zh;
|
||||
~*^hi hi;
|
||||
~*^ne ne;
|
||||
~*^lt lt;
|
||||
}
|
||||
|
||||
@ -142,6 +143,7 @@ http {
|
||||
~*^vi vi;
|
||||
~*^zh zh;
|
||||
~*^hi hi;
|
||||
~*^ne ne;
|
||||
~*^lt lt;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user