Merge branch 'master' into nymkappa/feature/hashrate-moving-average
This commit is contained in:
		
						commit
						c989bbec1b
					
				@ -13,6 +13,7 @@ export interface AbstractBitcoinApi {
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[];
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
			
		||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
			
		||||
}
 | 
			
		||||
export interface BitcoinRpcCredentials {
 | 
			
		||||
  host: string;
 | 
			
		||||
 | 
			
		||||
@ -141,6 +141,15 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
    return outSpends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getEstimatedHashrate(blockHeight: number): Promise<number> {
 | 
			
		||||
    // 120 is the default block span in Core
 | 
			
		||||
    return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
 | 
			
		||||
 | 
			
		||||
@ -61,8 +61,18 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspends(): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -195,6 +195,7 @@ class Server {
 | 
			
		||||
  setUpHttpApiRoutes() {
 | 
			
		||||
    this.app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', routes.$getBatchedOutspends)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees)
 | 
			
		||||
 | 
			
		||||
@ -120,6 +120,30 @@ class Routes {
 | 
			
		||||
    res.json(times);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getBatchedOutspends(req: Request, res: Response) {
 | 
			
		||||
    if (!Array.isArray(req.query.txId)) {
 | 
			
		||||
      res.status(500).send('Not an array');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (req.query.txId.length > 50) {
 | 
			
		||||
      res.status(400).send('Too many txids requested');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const txIds: string[] = [];
 | 
			
		||||
    for (const _txId in req.query.txId) {
 | 
			
		||||
      if (typeof req.query.txId[_txId] === 'string') {
 | 
			
		||||
        txIds.push(req.query.txId[_txId].toString());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds);
 | 
			
		||||
      res.json(batchedOutspends);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getCpfpInfo(req: Request, res: Response) {
 | 
			
		||||
    if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
 | 
			
		||||
      res.status(501).send(`Invalid transaction ID.`);
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@
 | 
			
		||||
 | 
			
		||||
.loader-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  background: #181b2d7f;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy {
 | 
			
		||||
    this.start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy(): void {
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.destroy();
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // initialize the scene without any entry transition
 | 
			
		||||
  setup(transactions: TransactionStripped[]): void {
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.setup(transactions);
 | 
			
		||||
      this.start();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  enter(transactions: TransactionStripped[], direction: string): void {
 | 
			
		||||
    if (this.scene) {
 | 
			
		||||
      this.scene.enter(transactions, direction);
 | 
			
		||||
 | 
			
		||||
@ -29,10 +29,6 @@ export default class BlockScene {
 | 
			
		||||
    this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy(): void {
 | 
			
		||||
    Object.values(this.txs).forEach(tx => tx.destroy());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
@ -46,6 +42,36 @@ export default class BlockScene {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Destroy the current layout and clean up graphics sprites without any exit animation
 | 
			
		||||
  destroy(): void {
 | 
			
		||||
    Object.values(this.txs).forEach(tx => tx.destroy());
 | 
			
		||||
    this.txs = {};
 | 
			
		||||
    this.layout = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // set up the scene with an initial set of transactions, without any transition animation
 | 
			
		||||
  setup(txs: TransactionStripped[]) {
 | 
			
		||||
    // clean up any old transactions
 | 
			
		||||
    Object.values(this.txs).forEach(tx => {
 | 
			
		||||
      tx.destroy();
 | 
			
		||||
      delete this.txs[tx.txid];
 | 
			
		||||
    });
 | 
			
		||||
    this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
 | 
			
		||||
    txs.forEach(tx => {
 | 
			
		||||
      const txView = new TxView(tx, this.vertexArray);
 | 
			
		||||
      this.txs[tx.txid] = txView;
 | 
			
		||||
      this.place(txView);
 | 
			
		||||
      this.saveGridToScreenPosition(txView);
 | 
			
		||||
      this.applyTxUpdate(txView, {
 | 
			
		||||
        display: {
 | 
			
		||||
          position: txView.screenPosition,
 | 
			
		||||
          color: txView.getColor()
 | 
			
		||||
        },
 | 
			
		||||
        duration: 0
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Animate new block entering scene
 | 
			
		||||
  enter(txs: TransactionStripped[], direction) {
 | 
			
		||||
    this.replace(txs, direction);
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
 | 
			
		||||
import { Location } from '@angular/common';
 | 
			
		||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap, tap, debounceTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
 | 
			
		||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
 | 
			
		||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Observable, of, Subscription } from 'rxjs';
 | 
			
		||||
import { Observable, of, Subscription, asyncScheduler } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
			
		||||
@ -33,7 +33,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  strippedTransactions: TransactionStripped[];
 | 
			
		||||
  overviewTransitionDirection: string;
 | 
			
		||||
  isLoadingOverview = true;
 | 
			
		||||
  isAwaitingOverview = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
  blockSubsidy: number;
 | 
			
		||||
  fees: number;
 | 
			
		||||
@ -54,6 +53,9 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  networkChangedSubscription: Subscription;
 | 
			
		||||
  queryParamsSubscription: Subscription;
 | 
			
		||||
  nextBlockSubscription: Subscription = undefined;
 | 
			
		||||
  nextBlockSummarySubscription: Subscription = undefined;
 | 
			
		||||
  nextBlockTxListSubscription: Subscription = undefined;
 | 
			
		||||
 | 
			
		||||
  @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
 | 
			
		||||
 | 
			
		||||
@ -124,6 +126,7 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
          return of(history.state.data.block);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.isLoadingBlock = true;
 | 
			
		||||
          this.isLoadingOverview = true;
 | 
			
		||||
 | 
			
		||||
          let blockInCache: BlockExtended;
 | 
			
		||||
          if (isBlockHeight) {
 | 
			
		||||
@ -152,6 +155,14 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
      tap((block: BlockExtended) => {
 | 
			
		||||
        // Preload previous block summary (execute the http query so the response will be cached)
 | 
			
		||||
        this.unsubscribeNextBlockSubscriptions();
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
 | 
			
		||||
          this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
 | 
			
		||||
          this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe();
 | 
			
		||||
        }, 100);
 | 
			
		||||
 | 
			
		||||
        this.block = block;
 | 
			
		||||
        this.blockHeight = block.height;
 | 
			
		||||
        const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left';
 | 
			
		||||
@ -170,13 +181,9 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.transactions = null;
 | 
			
		||||
        this.transactionsError = null;
 | 
			
		||||
        this.isLoadingOverview = true;
 | 
			
		||||
        this.isAwaitingOverview = true;
 | 
			
		||||
        this.overviewError = true;
 | 
			
		||||
        if (this.blockGraph) {
 | 
			
		||||
          this.blockGraph.exit(direction);
 | 
			
		||||
        }
 | 
			
		||||
        this.overviewError = null;
 | 
			
		||||
      }),
 | 
			
		||||
      debounceTime(300),
 | 
			
		||||
      throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
 | 
			
		||||
      shareReplay(1)
 | 
			
		||||
    );
 | 
			
		||||
    this.transactionSubscription = block$.pipe(
 | 
			
		||||
@ -194,11 +201,6 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      }
 | 
			
		||||
      this.transactions = transactions;
 | 
			
		||||
      this.isLoadingTransactions = false;
 | 
			
		||||
 | 
			
		||||
      if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) {
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
        this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (error) => {
 | 
			
		||||
      this.error = error;
 | 
			
		||||
@ -226,18 +228,19 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
      ),
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
 | 
			
		||||
      this.isAwaitingOverview = false;
 | 
			
		||||
      this.strippedTransactions = transactions;
 | 
			
		||||
      this.overviewTransitionDirection = direction;
 | 
			
		||||
      if (!this.isLoadingTransactions && this.blockGraph) {
 | 
			
		||||
        this.isLoadingOverview = false;
 | 
			
		||||
        this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false);
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
      if (this.blockGraph) {
 | 
			
		||||
        this.blockGraph.destroy();
 | 
			
		||||
        this.blockGraph.setup(this.strippedTransactions);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    (error) => {
 | 
			
		||||
      this.error = error;
 | 
			
		||||
      this.isLoadingOverview = false;
 | 
			
		||||
      this.isAwaitingOverview = false;
 | 
			
		||||
      if (this.blockGraph) {
 | 
			
		||||
        this.blockGraph.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.networkChangedSubscription = this.stateService.networkChanged$
 | 
			
		||||
@ -273,6 +276,19 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.networkChangedSubscription.unsubscribe();
 | 
			
		||||
    this.queryParamsSubscription.unsubscribe();
 | 
			
		||||
    this.unsubscribeNextBlockSubscriptions();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unsubscribeNextBlockSubscriptions() {
 | 
			
		||||
    if (this.nextBlockSubscription !== undefined) {
 | 
			
		||||
      this.nextBlockSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.nextBlockSummarySubscription !== undefined) {
 | 
			
		||||
      this.nextBlockSummarySubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.nextBlockTxListSubscription !== undefined) {
 | 
			
		||||
      this.nextBlockTxListSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO - Refactor this.fees/this.reward for liquid because it is not
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,9 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { environment } from 'src/environments/environment';
 | 
			
		||||
import { AssetsService } from 'src/app/services/assets.service';
 | 
			
		||||
import { map, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { map, tap, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-transactions-list',
 | 
			
		||||
@ -30,7 +31,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
  latestBlock$: Observable<BlockExtended>;
 | 
			
		||||
  outspendsSubscription: Subscription;
 | 
			
		||||
  refreshOutspends$: ReplaySubject<{ [str: string]: Observable<Outspend[]>}> = new ReplaySubject();
 | 
			
		||||
  refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
 | 
			
		||||
  showDetails$ = new BehaviorSubject<boolean>(false);
 | 
			
		||||
  outspends: Outspend[][] = [];
 | 
			
		||||
  assetsMinimal: any;
 | 
			
		||||
@ -38,6 +39,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private assetsService: AssetsService,
 | 
			
		||||
    private ref: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
@ -55,20 +57,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
    this.outspendsSubscription = merge(
 | 
			
		||||
      this.refreshOutspends$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          switchMap((observableObject) => forkJoin(observableObject)),
 | 
			
		||||
          map((outspends: any) => {
 | 
			
		||||
            const newOutspends: Outspend[] = [];
 | 
			
		||||
            for (const i in outspends) {
 | 
			
		||||
              if (outspends.hasOwnProperty(i)) {
 | 
			
		||||
                newOutspends.push(outspends[i]);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            this.outspends = this.outspends.concat(newOutspends);
 | 
			
		||||
          switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
 | 
			
		||||
          tap((outspends: Outspend[][]) => {
 | 
			
		||||
            this.outspends = this.outspends.concat(outspends);
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      this.stateService.utxoSpent$
 | 
			
		||||
        .pipe(
 | 
			
		||||
          map((utxoSpent) => {
 | 
			
		||||
          tap((utxoSpent) => {
 | 
			
		||||
            for (const i in utxoSpent) {
 | 
			
		||||
              this.outspends[0][i] = {
 | 
			
		||||
                spent: true,
 | 
			
		||||
@ -96,7 +92,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
        }
 | 
			
		||||
      }, 10);
 | 
			
		||||
    }
 | 
			
		||||
    const observableObject = {};
 | 
			
		||||
 | 
			
		||||
    this.transactions.forEach((tx, i) => {
 | 
			
		||||
      tx['@voutLimit'] = true;
 | 
			
		||||
      tx['@vinLimit'] = true;
 | 
			
		||||
@ -117,10 +113,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
 | 
			
		||||
 | 
			
		||||
        tx['addressValue'] = addressIn - addressOut;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      observableObject[i] = this.electrsApiService.getOutspends$(tx.txid);
 | 
			
		||||
    });
 | 
			
		||||
    this.refreshOutspends$.next(observableObject);
 | 
			
		||||
 | 
			
		||||
    this.refreshOutspends$.next(this.transactions.map((tx) => tx.txid));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onScroll() {
 | 
			
		||||
 | 
			
		||||
@ -5,6 +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';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
@ -74,6 +75,14 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<number[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getOutspendsBatched$(txIds: string[]): Observable<Outspend[][]> {
 | 
			
		||||
    let params = new HttpParams();
 | 
			
		||||
    txIds.forEach((txId: string) => {
 | 
			
		||||
      params = params.append('txId[]', txId);
 | 
			
		||||
    });
 | 
			
		||||
    return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestDonation$(amount: number, orderId: string): Observable<any> {
 | 
			
		||||
    const params = {
 | 
			
		||||
      amount: amount,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user