Merge branch 'master' into natsoni/add-blocks-logo
This commit is contained in:
		
						commit
						46d99db167
					
				@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
  @Output() changeMode = new EventEmitter<boolean>();
 | 
					  @Output() changeMode = new EventEmitter<boolean>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  calculating = true;
 | 
					  calculating = true;
 | 
				
			||||||
 | 
					  processing = false;
 | 
				
			||||||
  selectedOption: 'wait' | 'accel';
 | 
					  selectedOption: 'wait' | 'accel';
 | 
				
			||||||
  cantPayReason = '';
 | 
					  cantPayReason = '';
 | 
				
			||||||
  quoteError = ''; // error fetching estimate or initial data
 | 
					  quoteError = ''; // error fetching estimate or initial data
 | 
				
			||||||
@ -380,9 +381,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
   * Account-based acceleration request
 | 
					   * Account-based acceleration request
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  accelerateWithMempoolAccount(): void {
 | 
					  accelerateWithMempoolAccount(): void {
 | 
				
			||||||
    if (!this.canPay || this.calculating) {
 | 
					    if (!this.canPay || this.calculating || this.processing) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    this.processing = true;
 | 
				
			||||||
    if (this.accelerationSubscription) {
 | 
					    if (this.accelerationSubscription) {
 | 
				
			||||||
      this.accelerationSubscription.unsubscribe();
 | 
					      this.accelerationSubscription.unsubscribe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -392,6 +394,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
      this.accelerationUUID
 | 
					      this.accelerationUUID
 | 
				
			||||||
    ).subscribe({
 | 
					    ).subscribe({
 | 
				
			||||||
      next: () => {
 | 
					      next: () => {
 | 
				
			||||||
 | 
					        this.processing = false;
 | 
				
			||||||
        this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					        this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
        this.audioService.playSound('ascend-chime-cartoon');
 | 
					        this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
        this.showSuccess = true;
 | 
					        this.showSuccess = true;
 | 
				
			||||||
@ -399,6 +402,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
        this.moveToStep('paid');
 | 
					        this.moveToStep('paid');
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      error: (response) => {
 | 
					      error: (response) => {
 | 
				
			||||||
 | 
					        this.processing = false;
 | 
				
			||||||
        this.accelerateError = response.error;
 | 
					        this.accelerateError = response.error;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -468,10 +472,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
   * APPLE PAY
 | 
					   * APPLE PAY
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async requestApplePayPayment(): Promise<void> {
 | 
					  async requestApplePayPayment(): Promise<void> {
 | 
				
			||||||
 | 
					    if (this.processing) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (this.conversionsSubscription) {
 | 
					    if (this.conversionsSubscription) {
 | 
				
			||||||
      this.conversionsSubscription.unsubscribe();
 | 
					      this.conversionsSubscription.unsubscribe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.processing = true;
 | 
				
			||||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
					    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
				
			||||||
      async (conversions) => {
 | 
					      async (conversions) => {
 | 
				
			||||||
        this.conversions = conversions;
 | 
					        this.conversions = conversions;
 | 
				
			||||||
@ -496,6 +504,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
            console.error(`Unable to find apple pay button id='apple-pay-button'`);
 | 
					            console.error(`Unable to find apple pay button id='apple-pay-button'`);
 | 
				
			||||||
            // Try again
 | 
					            // Try again
 | 
				
			||||||
            setTimeout(this.requestApplePayPayment.bind(this), 500);
 | 
					            setTimeout(this.requestApplePayPayment.bind(this), 500);
 | 
				
			||||||
 | 
					            this.processing = false;
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          this.loadingApplePay = false;
 | 
					          this.loadingApplePay = false;
 | 
				
			||||||
@ -507,6 +516,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
					              if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
				
			||||||
                console.error(`Cannot retreive payment card details`);
 | 
					                console.error(`Cannot retreive payment card details`);
 | 
				
			||||||
                this.accelerateError = 'apple_pay_no_card_details';
 | 
					                this.accelerateError = 'apple_pay_no_card_details';
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
					              const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
				
			||||||
@ -518,6 +528,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                this.accelerationUUID
 | 
					                this.accelerationUUID
 | 
				
			||||||
              ).subscribe({
 | 
					              ).subscribe({
 | 
				
			||||||
                next: () => {
 | 
					                next: () => {
 | 
				
			||||||
 | 
					                  this.processing = false;
 | 
				
			||||||
                  this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					                  this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
                  this.audioService.playSound('ascend-chime-cartoon');
 | 
					                  this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
                  if (this.applePay) {
 | 
					                  if (this.applePay) {
 | 
				
			||||||
@ -528,6 +539,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                  }, 1000);
 | 
					                  }, 1000);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                error: (response) => {
 | 
					                error: (response) => {
 | 
				
			||||||
 | 
					                  this.processing = false;
 | 
				
			||||||
                  this.accelerateError = response.error;
 | 
					                  this.accelerateError = response.error;
 | 
				
			||||||
                  if (!(response.status === 403 && response.error === 'not_available')) {
 | 
					                  if (!(response.status === 403 && response.error === 'not_available')) {
 | 
				
			||||||
                    setTimeout(() => {
 | 
					                    setTimeout(() => {
 | 
				
			||||||
@ -539,6 +551,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
 | 
					              this.processing = false;
 | 
				
			||||||
              let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
					              let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
				
			||||||
              if (tokenResult.errors) {
 | 
					              if (tokenResult.errors) {
 | 
				
			||||||
                errorMessage += ` and errors: ${JSON.stringify(
 | 
					                errorMessage += ` and errors: ${JSON.stringify(
 | 
				
			||||||
@ -549,6 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          this.processing = false;
 | 
				
			||||||
          console.error(e);
 | 
					          console.error(e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -559,10 +573,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
   * GOOGLE PAY
 | 
					   * GOOGLE PAY
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async requestGooglePayPayment(): Promise<void> {
 | 
					  async requestGooglePayPayment(): Promise<void> {
 | 
				
			||||||
 | 
					    if (this.processing) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (this.conversionsSubscription) {
 | 
					    if (this.conversionsSubscription) {
 | 
				
			||||||
      this.conversionsSubscription.unsubscribe();
 | 
					      this.conversionsSubscription.unsubscribe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    this.processing = true;
 | 
				
			||||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
					    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
				
			||||||
      async (conversions) => {
 | 
					      async (conversions) => {
 | 
				
			||||||
        this.conversions = conversions;
 | 
					        this.conversions = conversions;
 | 
				
			||||||
@ -597,6 +615,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
            if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
					            if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
 | 
				
			||||||
              console.error(`Cannot retreive payment card details`);
 | 
					              console.error(`Cannot retreive payment card details`);
 | 
				
			||||||
              this.accelerateError = 'apple_pay_no_card_details';
 | 
					              this.accelerateError = 'apple_pay_no_card_details';
 | 
				
			||||||
 | 
					              this.processing = false;
 | 
				
			||||||
              return;
 | 
					              return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
					            const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
 | 
				
			||||||
@ -608,6 +627,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
              this.accelerationUUID
 | 
					              this.accelerationUUID
 | 
				
			||||||
            ).subscribe({
 | 
					            ).subscribe({
 | 
				
			||||||
              next: () => {
 | 
					              next: () => {
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
                this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					                this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
                this.audioService.playSound('ascend-chime-cartoon');
 | 
					                this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
                if (this.googlePay) {
 | 
					                if (this.googlePay) {
 | 
				
			||||||
@ -618,6 +638,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                }, 1000);
 | 
					                }, 1000);
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              error: (response) => {
 | 
					              error: (response) => {
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
                this.accelerateError = response.error;
 | 
					                this.accelerateError = response.error;
 | 
				
			||||||
                if (!(response.status === 403 && response.error === 'not_available')) {
 | 
					                if (!(response.status === 403 && response.error === 'not_available')) {
 | 
				
			||||||
                  setTimeout(() => {
 | 
					                  setTimeout(() => {
 | 
				
			||||||
@ -629,6 +650,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
              }
 | 
					              }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
 | 
					            this.processing = false;
 | 
				
			||||||
            let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
					            let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
 | 
				
			||||||
            if (tokenResult.errors) {
 | 
					            if (tokenResult.errors) {
 | 
				
			||||||
              errorMessage += ` and errors: ${JSON.stringify(
 | 
					              errorMessage += ` and errors: ${JSON.stringify(
 | 
				
			||||||
@ -646,10 +668,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
   * CASHAPP
 | 
					   * CASHAPP
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  async requestCashAppPayment(): Promise<void> {
 | 
					  async requestCashAppPayment(): Promise<void> {
 | 
				
			||||||
 | 
					    if (this.processing) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (this.conversionsSubscription) {
 | 
					    if (this.conversionsSubscription) {
 | 
				
			||||||
      this.conversionsSubscription.unsubscribe();
 | 
					      this.conversionsSubscription.unsubscribe();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.processing = true;
 | 
				
			||||||
    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
					    this.conversionsSubscription = this.stateService.conversions$.subscribe(
 | 
				
			||||||
      async (conversions) => {
 | 
					      async (conversions) => {
 | 
				
			||||||
        this.conversions = conversions;
 | 
					        this.conversions = conversions;
 | 
				
			||||||
@ -680,6 +706,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
        this.cashAppPay.addEventListener('ontokenization', event => {
 | 
					        this.cashAppPay.addEventListener('ontokenization', event => {
 | 
				
			||||||
          const { tokenResult, error } = event.detail;
 | 
					          const { tokenResult, error } = event.detail;
 | 
				
			||||||
          if (error) {
 | 
					          if (error) {
 | 
				
			||||||
 | 
					            this.processing = false;
 | 
				
			||||||
            this.accelerateError = error;
 | 
					            this.accelerateError = error;
 | 
				
			||||||
          } else if (tokenResult.status === 'OK') {
 | 
					          } else if (tokenResult.status === 'OK') {
 | 
				
			||||||
            this.servicesApiService.accelerateWithCashApp$(
 | 
					            this.servicesApiService.accelerateWithCashApp$(
 | 
				
			||||||
@ -690,6 +717,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
              this.accelerationUUID
 | 
					              this.accelerationUUID
 | 
				
			||||||
            ).subscribe({
 | 
					            ).subscribe({
 | 
				
			||||||
              next: () => {
 | 
					              next: () => {
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
                this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
					                this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
 | 
				
			||||||
                this.audioService.playSound('ascend-chime-cartoon');
 | 
					                this.audioService.playSound('ascend-chime-cartoon');
 | 
				
			||||||
                if (this.cashAppPay) {
 | 
					                if (this.cashAppPay) {
 | 
				
			||||||
@ -704,6 +732,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
 | 
				
			|||||||
                }, 1000);
 | 
					                }, 1000);
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              error: (response) => {
 | 
					              error: (response) => {
 | 
				
			||||||
 | 
					                this.processing = false;
 | 
				
			||||||
                this.accelerateError = response.error;
 | 
					                this.accelerateError = response.error;
 | 
				
			||||||
                if (!(response.status === 403 && response.error === 'not_available')) {
 | 
					                if (!(response.status === 403 && response.error === 'not_available')) {
 | 
				
			||||||
                  setTimeout(() => {
 | 
					                  setTimeout(() => {
 | 
				
			||||||
 | 
				
			|||||||
@ -47,13 +47,14 @@
 | 
				
			|||||||
      <tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
 | 
					      <tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
 | 
				
			||||||
        <td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
 | 
					        <td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
 | 
				
			||||||
        <td class="value" *ngIf="accelerationInfo.pools">
 | 
					        <td class="value" *ngIf="accelerationInfo.pools">
 | 
				
			||||||
          <ng-container *ngFor="let pool of accelerationInfo.pools">
 | 
					          <ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
 | 
				
			||||||
            <img *ngIf="accelerationInfo.poolsData[pool]" 
 | 
					            <img *ngIf="accelerationInfo.poolsData[pool]" 
 | 
				
			||||||
              class="pool-logo" 
 | 
					              class="pool-logo" 
 | 
				
			||||||
              [style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
 | 
					              [style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
 | 
				
			||||||
              [src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'" 
 | 
					              [src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'" 
 | 
				
			||||||
              onError="this.src = '/resources/mining-pools/default.svg'" 
 | 
					              onError="this.src = '/resources/mining-pools/default.svg'" 
 | 
				
			||||||
              [alt]="'Logo of ' + pool.name + ' mining pool'">
 | 
					              [alt]="'Logo of ' + pool.name + ' mining pool'">
 | 
				
			||||||
 | 
					            <br *ngIf="i % 6 === 5">
 | 
				
			||||||
          </ng-container>
 | 
					          </ng-container>
 | 
				
			||||||
        </td>
 | 
					        </td>
 | 
				
			||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  .label {
 | 
					  .label {
 | 
				
			||||||
    padding-right: 30px;
 | 
					    padding-right: 30px;
 | 
				
			||||||
 | 
					    vertical-align: top;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .pool-logo {
 | 
					  .pool-logo {
 | 
				
			||||||
@ -30,7 +31,8 @@
 | 
				
			|||||||
    height: 22px;
 | 
					    height: 22px;
 | 
				
			||||||
    position: relative;
 | 
					    position: relative;
 | 
				
			||||||
    top: -1px;
 | 
					    top: -1px;
 | 
				
			||||||
    margin-right: 3px;
 | 
					    margin-right: 4px;
 | 
				
			||||||
 | 
					    margin-bottom: 4px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .oobFees {
 | 
					  .oobFees {
 | 
				
			||||||
 | 
				
			|||||||
@ -94,6 +94,20 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </ng-container>
 | 
					    </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && utxos && utxos.length > 2">
 | 
				
			||||||
 | 
					      <br>
 | 
				
			||||||
 | 
					      <div class="title-tx">
 | 
				
			||||||
 | 
					        <h2 class="text-left" i18n="address.unspent-outputs">Unspent Outputs</h2>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="box">
 | 
				
			||||||
 | 
					        <div class="row">
 | 
				
			||||||
 | 
					          <div class="col-md">
 | 
				
			||||||
 | 
					            <app-utxo-graph [utxos]="utxos" left="80" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <br>
 | 
					    <br>
 | 
				
			||||||
    <div class="title-tx">
 | 
					    <div class="title-tx">
 | 
				
			||||||
      <h2 class="text-left">
 | 
					      <h2 class="text-left">
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
 | 
				
			|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
					import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
				
			||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
					import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
				
			||||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
 | 
					import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
 | 
				
			||||||
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
 | 
					import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
 | 
				
			||||||
import { WebsocketService } from '../../services/websocket.service';
 | 
					import { WebsocketService } from '../../services/websocket.service';
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
import { AudioService } from '../../services/audio.service';
 | 
					import { AudioService } from '../../services/audio.service';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { of, merge, Subscription, Observable } from 'rxjs';
 | 
					import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
 | 
				
			||||||
import { SeoService } from '../../services/seo.service';
 | 
					import { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
 | 
					import { seoDescriptionNetwork } from '../../shared/common.utils';
 | 
				
			||||||
import { AddressInformation } from '../../interfaces/node-api.interface';
 | 
					import { AddressInformation } from '../../interfaces/node-api.interface';
 | 
				
			||||||
@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
  addressString: string;
 | 
					  addressString: string;
 | 
				
			||||||
  isLoadingAddress = true;
 | 
					  isLoadingAddress = true;
 | 
				
			||||||
  transactions: Transaction[];
 | 
					  transactions: Transaction[];
 | 
				
			||||||
 | 
					  utxos: Utxo[];
 | 
				
			||||||
  isLoadingTransactions = true;
 | 
					  isLoadingTransactions = true;
 | 
				
			||||||
  retryLoadMore = false;
 | 
					  retryLoadMore = false;
 | 
				
			||||||
  error: any;
 | 
					  error: any;
 | 
				
			||||||
@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
          this.address = null;
 | 
					          this.address = null;
 | 
				
			||||||
          this.isLoadingTransactions = true;
 | 
					          this.isLoadingTransactions = true;
 | 
				
			||||||
          this.transactions = null;
 | 
					          this.transactions = null;
 | 
				
			||||||
 | 
					          this.utxos = null;
 | 
				
			||||||
          this.addressInfo = null;
 | 
					          this.addressInfo = null;
 | 
				
			||||||
          this.exampleChannel = null;
 | 
					          this.exampleChannel = null;
 | 
				
			||||||
          document.body.scrollTo(0, 0);
 | 
					          document.body.scrollTo(0, 0);
 | 
				
			||||||
@ -212,11 +214,19 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
          this.updateChainStats();
 | 
					          this.updateChainStats();
 | 
				
			||||||
          this.isLoadingAddress = false;
 | 
					          this.isLoadingAddress = false;
 | 
				
			||||||
          this.isLoadingTransactions = true;
 | 
					          this.isLoadingTransactions = true;
 | 
				
			||||||
          return address.is_pubkey
 | 
					          const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos;
 | 
				
			||||||
 | 
					          return forkJoin([
 | 
				
			||||||
 | 
					            address.is_pubkey
 | 
				
			||||||
              ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
 | 
					              ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
 | 
				
			||||||
              : this.electrsApiService.getAddressTransactions$(address.address);
 | 
					              : this.electrsApiService.getAddressTransactions$(address.address),
 | 
				
			||||||
 | 
					            utxoCount >= 2 && utxoCount <= 500 ? (address.is_pubkey
 | 
				
			||||||
 | 
					              ? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
 | 
				
			||||||
 | 
					              : this.electrsApiService.getAddressUtxos$(address.address)) : of([])
 | 
				
			||||||
 | 
					          ]);
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        switchMap((transactions) => {
 | 
					        switchMap(([transactions, utxos]) => {
 | 
				
			||||||
 | 
					          this.utxos = utxos;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          this.tempTransactions = transactions;
 | 
					          this.tempTransactions = transactions;
 | 
				
			||||||
          if (transactions.length) {
 | 
					          if (transactions.length) {
 | 
				
			||||||
            this.lastTransactionTxId = transactions[transactions.length - 1].txid;
 | 
					            this.lastTransactionTxId = transactions[transactions.length - 1].txid;
 | 
				
			||||||
@ -334,6 +344,23 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update utxos in-place
 | 
				
			||||||
 | 
					    for (const vin of transaction.vin) {
 | 
				
			||||||
 | 
					      const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout);
 | 
				
			||||||
 | 
					      if (utxoIndex !== -1) {
 | 
				
			||||||
 | 
					        this.utxos.splice(utxoIndex, 1);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const [index, vout] of transaction.vout.entries()) {
 | 
				
			||||||
 | 
					      if (vout.scriptpubkey_address === this.address.address) {
 | 
				
			||||||
 | 
					        this.utxos.push({
 | 
				
			||||||
 | 
					          txid: transaction.txid,
 | 
				
			||||||
 | 
					          vout: index,
 | 
				
			||||||
 | 
					          value: vout.value,
 | 
				
			||||||
 | 
					          status: JSON.parse(JSON.stringify(transaction.status)),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -346,6 +373,26 @@ export class AddressComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    this.transactions.splice(index, 1);
 | 
					    this.transactions.splice(index, 1);
 | 
				
			||||||
    this.transactions = this.transactions.slice();
 | 
					    this.transactions = this.transactions.slice();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update utxos in-place
 | 
				
			||||||
 | 
					    for (const vin of transaction.vin) {
 | 
				
			||||||
 | 
					      if (vin.prevout?.scriptpubkey_address === this.address.address) {
 | 
				
			||||||
 | 
					        this.utxos.push({
 | 
				
			||||||
 | 
					          txid: vin.txid,
 | 
				
			||||||
 | 
					          vout: vin.vout,
 | 
				
			||||||
 | 
					          value: vin.prevout.value,
 | 
				
			||||||
 | 
					          status: { confirmed: true }, // Assuming the input was confirmed
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const [index, vout] of transaction.vout.entries()) {
 | 
				
			||||||
 | 
					      if (vout.scriptpubkey_address === this.address.address) {
 | 
				
			||||||
 | 
					        const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index);
 | 
				
			||||||
 | 
					        if (utxoIndex !== -1) {
 | 
				
			||||||
 | 
					          this.utxos.splice(utxoIndex, 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@
 | 
				
			|||||||
          <div class="input-group-prepend">
 | 
					          <div class="input-group-prepend">
 | 
				
			||||||
            <span class="input-group-text">{{ currency$ | async }}</span>
 | 
					            <span class="input-group-text">{{ currency$ | async }}</span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <input type="text" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
 | 
					          <input type="text" inputmode="numeric" class="form-control" formControlName="fiat" (input)="transformInput('fiat')" (click)="selectAll($event)">
 | 
				
			||||||
          <app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
					          <app-clipboard [button]="true" [text]="form.get('fiat').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -20,7 +20,7 @@
 | 
				
			|||||||
          <div class="input-group-prepend">
 | 
					          <div class="input-group-prepend">
 | 
				
			||||||
            <span class="input-group-text">BTC</span>
 | 
					            <span class="input-group-text">BTC</span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <input type="text" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
 | 
					          <input type="text" inputmode="numeric" class="form-control" formControlName="bitcoin" (input)="transformInput('bitcoin')" (click)="selectAll($event)">
 | 
				
			||||||
          <app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
					          <app-clipboard [button]="true" [text]="form.get('bitcoin').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,7 +28,7 @@
 | 
				
			|||||||
          <div class="input-group-prepend">
 | 
					          <div class="input-group-prepend">
 | 
				
			||||||
            <span class="input-group-text" i18n="shared.sats">sats</span>
 | 
					            <span class="input-group-text" i18n="shared.sats">sats</span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <input type="text" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
 | 
					          <input type="text" inputmode="numeric" class="form-control" formControlName="satoshis" (input)="transformInput('satoshis')" (click)="selectAll($event)">
 | 
				
			||||||
          <app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
					          <app-clipboard [button]="true" [text]="form.get('satoshis').value" [class]="'btn btn-lg btn-secondary ml-1'"></app-clipboard>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
 | 
				
			|||||||
@ -65,6 +65,7 @@
 | 
				
			|||||||
              }
 | 
					              }
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          @if (!replaced) {
 | 
				
			||||||
            <div class="field narrower">
 | 
					            <div class="field narrower">
 | 
				
			||||||
              <div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
 | 
					              <div class="label" i18n="transaction.eta|Transaction ETA">ETA</div>
 | 
				
			||||||
              <div class="value">
 | 
					              <div class="value">
 | 
				
			||||||
@ -82,6 +83,7 @@
 | 
				
			|||||||
                </ng-template>
 | 
					                </ng-template>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>  
 | 
					            </div>  
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        } @else if (tx && tx.status?.confirmed) {
 | 
					        } @else if (tx && tx.status?.confirmed) {
 | 
				
			||||||
          <div class="field narrower mt-2">
 | 
					          <div class="field narrower mt-2">
 | 
				
			||||||
            <div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
 | 
					            <div class="label" i18n="transaction.confirmed-at">Confirmed at</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -192,7 +192,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
    this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
 | 
					    this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.stateService.isLiquid()) {
 | 
					    if (!this.stateService.isLiquid()) {
 | 
				
			||||||
      this.miningService.getMiningStats('1w').subscribe(stats => {
 | 
					      this.miningService.getMiningStats('1m').subscribe(stats => {
 | 
				
			||||||
        this.miningStats = stats;
 | 
					        this.miningStats = stats;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -491,7 +491,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
          if (this.stateService.network === '') {
 | 
					          if (this.stateService.network === '') {
 | 
				
			||||||
            if (!this.mempoolPosition.accelerated) {
 | 
					            if (!this.mempoolPosition.accelerated) {
 | 
				
			||||||
              if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
 | 
					              if (!this.accelerationFlowCompleted && !this.hideAccelerationSummary && !this.showAccelerationSummary) {
 | 
				
			||||||
                this.miningService.getMiningStats('1w').subscribe(stats => {
 | 
					                this.miningService.getMiningStats('1m').subscribe(stats => {
 | 
				
			||||||
                  this.miningStats = stats;
 | 
					                  this.miningStats = stats;
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div [class.full-container]="!widget">
 | 
				
			||||||
 | 
					  <ng-container *ngIf="!error">
 | 
				
			||||||
 | 
					    <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, paddingBottom: !widget}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
				
			||||||
 | 
					      (chartInit)="onChartInit($event)">
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
				
			||||||
 | 
					      <div class="spinner-border text-light"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </ng-container>
 | 
				
			||||||
 | 
					  <ng-container *ngIf="error">
 | 
				
			||||||
 | 
					    <div class="error-wrapper">
 | 
				
			||||||
 | 
					      <p class="error">{{ error }}</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
 | 
				
			||||||
 | 
					    <div class="spinner-border text-light"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					.card-header {
 | 
				
			||||||
 | 
					  border-bottom: 0;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  @media (min-width: 465px) {
 | 
				
			||||||
 | 
					    font-size: 20px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  @media (min-width: 992px) {
 | 
				
			||||||
 | 
					    height: 40px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.main-title {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  color: var(--fg);
 | 
				
			||||||
 | 
					  opacity: var(--opacity);
 | 
				
			||||||
 | 
					  margin-top: -13px;
 | 
				
			||||||
 | 
					  font-size: 10px;
 | 
				
			||||||
 | 
					  text-transform: uppercase;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  padding-bottom: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.full-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  padding: 0px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 400px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error-wrapper {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  font-size: 15px;
 | 
				
			||||||
 | 
					  color: grey;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.chart {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding-right: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.chart-widget {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.disabled {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  opacity: 0.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										285
									
								
								frontend/src/app/components/utxo-graph/utxo-graph.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								frontend/src/app/components/utxo-graph/utxo-graph.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,285 @@
 | 
				
			|||||||
 | 
					import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
 | 
				
			||||||
 | 
					import { EChartsOption } from '../../graphs/echarts';
 | 
				
			||||||
 | 
					import { BehaviorSubject, Subscription } from 'rxjs';
 | 
				
			||||||
 | 
					import { Utxo } from '../../interfaces/electrs.interface';
 | 
				
			||||||
 | 
					import { StateService } from '../../services/state.service';
 | 
				
			||||||
 | 
					import { Router } from '@angular/router';
 | 
				
			||||||
 | 
					import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
 | 
				
			||||||
 | 
					import { renderSats } from '../../shared/common.utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-utxo-graph',
 | 
				
			||||||
 | 
					  templateUrl: './utxo-graph.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./utxo-graph.component.scss'],
 | 
				
			||||||
 | 
					  styles: [`
 | 
				
			||||||
 | 
					    .loadingGraphs {
 | 
				
			||||||
 | 
					      position: absolute;
 | 
				
			||||||
 | 
					      top: 50%;
 | 
				
			||||||
 | 
					      left: calc(50% - 15px);
 | 
				
			||||||
 | 
					      z-index: 99;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `],
 | 
				
			||||||
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class UtxoGraphComponent implements OnChanges, OnDestroy {
 | 
				
			||||||
 | 
					  @Input() utxos: Utxo[];
 | 
				
			||||||
 | 
					  @Input() height: number = 200;
 | 
				
			||||||
 | 
					  @Input() right: number | string = 10;
 | 
				
			||||||
 | 
					  @Input() left: number | string = 70;
 | 
				
			||||||
 | 
					  @Input() widget: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  subscription: Subscription;
 | 
				
			||||||
 | 
					  redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  chartOptions: EChartsOption = {};
 | 
				
			||||||
 | 
					  chartInitOptions = {
 | 
				
			||||||
 | 
					    renderer: 'svg',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  error: any;
 | 
				
			||||||
 | 
					  isLoading = true;
 | 
				
			||||||
 | 
					  chartInstance: any = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    public stateService: StateService,
 | 
				
			||||||
 | 
					    private cd: ChangeDetectorRef,
 | 
				
			||||||
 | 
					    private zone: NgZone,
 | 
				
			||||||
 | 
					    private router: Router,
 | 
				
			||||||
 | 
					    private relativeUrlPipe: RelativeUrlPipe,
 | 
				
			||||||
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnChanges(changes: SimpleChanges): void {
 | 
				
			||||||
 | 
					    this.isLoading = true;
 | 
				
			||||||
 | 
					    if (!this.utxos) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (changes.utxos) {
 | 
				
			||||||
 | 
					      this.prepareChartOptions(this.utxos);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  prepareChartOptions(utxos: Utxo[]) {
 | 
				
			||||||
 | 
					    if (!utxos || utxos.length === 0) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.isLoading = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Helper functions
 | 
				
			||||||
 | 
					    const distance = (x1: number, y1: number, x2: number, y2: number): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
 | 
				
			||||||
 | 
					    const intersectionPoints = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): [number, number][] => {
 | 
				
			||||||
 | 
					      const d = distance(x1, y1, x2, y2);
 | 
				
			||||||
 | 
					      const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
 | 
				
			||||||
 | 
					      const h = Math.sqrt(r1 * r1 - a * a);
 | 
				
			||||||
 | 
					      const x3 = x1 + a * (x2 - x1) / d;
 | 
				
			||||||
 | 
					      const y3 = y1 + a * (y2 - y1) / d;
 | 
				
			||||||
 | 
					      return [
 | 
				
			||||||
 | 
					        [x3 + h * (y2 - y1) / d, y3 - h * (x2 - x1) / d],
 | 
				
			||||||
 | 
					        [x3 - h * (y2 - y1) / d, y3 + h * (x2 - x1) / d]
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Naive algorithm to pack circles as tightly as possible without overlaps
 | 
				
			||||||
 | 
					    const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = [];
 | 
				
			||||||
 | 
					    // Pack in descending order of value, and limit to the top 500 to preserve performance
 | 
				
			||||||
 | 
					    const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500);
 | 
				
			||||||
 | 
					    let centerOfMass = { x: 0, y: 0 };
 | 
				
			||||||
 | 
					    let weightOfMass = 0;
 | 
				
			||||||
 | 
					    sortedUtxos.forEach((utxo, index) => {
 | 
				
			||||||
 | 
					      // area proportional to value
 | 
				
			||||||
 | 
					      const r = Math.sqrt(utxo.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // special cases for the first two utxos
 | 
				
			||||||
 | 
					      if (index === 0) {
 | 
				
			||||||
 | 
					        placedCircles.push({ x: 0, y: 0, r, utxo, distances: [0] });
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (index === 1) {
 | 
				
			||||||
 | 
					        const c = placedCircles[0];
 | 
				
			||||||
 | 
					        placedCircles.push({ x: c.r + r, y: 0, r, utxo, distances: [c.r + r, 0] });
 | 
				
			||||||
 | 
					        c.distances.push(c.r + r);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // The best position will be touching two other circles
 | 
				
			||||||
 | 
					      // generate a list of candidate points by finding all such positions
 | 
				
			||||||
 | 
					      // where the circle can be placed without overlapping other circles
 | 
				
			||||||
 | 
					      const candidates: [number, number, number[]][] = [];
 | 
				
			||||||
 | 
					      const numCircles = placedCircles.length;
 | 
				
			||||||
 | 
					      for (let i = 0; i < numCircles; i++) {
 | 
				
			||||||
 | 
					        for (let j = i + 1; j < numCircles; j++) {
 | 
				
			||||||
 | 
					          const c1 = placedCircles[i];
 | 
				
			||||||
 | 
					          const c2 = placedCircles[j];
 | 
				
			||||||
 | 
					          if (c1.distances[j] > (c1.r + c2.r + r + r)) {
 | 
				
			||||||
 | 
					            // too far apart for new circle to touch both
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const points = intersectionPoints(c1.x, c1.y, c1.r + r, c2.x, c2.y, c2.r + r);
 | 
				
			||||||
 | 
					          points.forEach(([x, y]) => {
 | 
				
			||||||
 | 
					            const distances: number[] = [];
 | 
				
			||||||
 | 
					            let valid = true;
 | 
				
			||||||
 | 
					            for (let k = 0; k < numCircles; k++) {
 | 
				
			||||||
 | 
					              const c = placedCircles[k];
 | 
				
			||||||
 | 
					              const d = distance(x, y, c.x, c.y);
 | 
				
			||||||
 | 
					              if (k !== i && k !== j && d < (r + c.r)) {
 | 
				
			||||||
 | 
					                valid = false;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                distances.push(d);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (valid) {
 | 
				
			||||||
 | 
					              candidates.push([x, y, distances]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Pick the candidate closest to the center of mass
 | 
				
			||||||
 | 
					      const [x, y, distances] = candidates.length ? candidates.reduce((closest, candidate) =>
 | 
				
			||||||
 | 
					        distance(candidate[0], candidate[1], centerOfMass[0], centerOfMass[1]) <
 | 
				
			||||||
 | 
					        distance(closest[0], closest[1], centerOfMass[0], centerOfMass[1])
 | 
				
			||||||
 | 
					          ? candidate
 | 
				
			||||||
 | 
					          : closest
 | 
				
			||||||
 | 
					      ) : [0, 0, []];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      placedCircles.push({ x, y, r, utxo, distances });
 | 
				
			||||||
 | 
					      for (let i = 0; i < distances.length; i++) {
 | 
				
			||||||
 | 
					        placedCircles[i].distances.push(distances[i]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      distances.push(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Update center of mass
 | 
				
			||||||
 | 
					      centerOfMass = {
 | 
				
			||||||
 | 
					        x: (centerOfMass.x * weightOfMass + x) / (weightOfMass + r),
 | 
				
			||||||
 | 
					        y: (centerOfMass.y * weightOfMass + y) / (weightOfMass + r),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      weightOfMass += r;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Precompute the bounding box of the graph
 | 
				
			||||||
 | 
					    const minX = Math.min(...placedCircles.map(d => d.x - d.r));
 | 
				
			||||||
 | 
					    const maxX = Math.max(...placedCircles.map(d => d.x + d.r));
 | 
				
			||||||
 | 
					    const minY = Math.min(...placedCircles.map(d => d.y - d.r));
 | 
				
			||||||
 | 
					    const maxY = Math.max(...placedCircles.map(d => d.y + d.r));
 | 
				
			||||||
 | 
					    const width = maxX - minX;
 | 
				
			||||||
 | 
					    const height = maxY - minY;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = placedCircles.map((circle, index) => [
 | 
				
			||||||
 | 
					      circle.utxo,
 | 
				
			||||||
 | 
					      index,
 | 
				
			||||||
 | 
					      circle.x,
 | 
				
			||||||
 | 
					      circle.y,
 | 
				
			||||||
 | 
					      circle.r
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.chartOptions = {
 | 
				
			||||||
 | 
					      series: [{
 | 
				
			||||||
 | 
					        type: 'custom',
 | 
				
			||||||
 | 
					        coordinateSystem: undefined,
 | 
				
			||||||
 | 
					        data,
 | 
				
			||||||
 | 
					        renderItem: (params, api) => {
 | 
				
			||||||
 | 
					          const idx = params.dataIndex;
 | 
				
			||||||
 | 
					          const datum = data[idx];
 | 
				
			||||||
 | 
					          const utxo = datum[0] as Utxo;
 | 
				
			||||||
 | 
					          const chartWidth = api.getWidth();
 | 
				
			||||||
 | 
					          const chartHeight = api.getHeight();
 | 
				
			||||||
 | 
					          const scale = Math.min(chartWidth / width, chartHeight / height);
 | 
				
			||||||
 | 
					          const scaledWidth = width * scale;
 | 
				
			||||||
 | 
					          const scaledHeight = height * scale;
 | 
				
			||||||
 | 
					          const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale;
 | 
				
			||||||
 | 
					          const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale;
 | 
				
			||||||
 | 
					          const x = datum[2] as number;
 | 
				
			||||||
 | 
					          const y = datum[3] as number;
 | 
				
			||||||
 | 
					          const r = datum[4] as number;
 | 
				
			||||||
 | 
					          if (r * scale < 3) {
 | 
				
			||||||
 | 
					            // skip items too small to render cleanly
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const valueStr = renderSats(utxo.value, this.stateService.network);
 | 
				
			||||||
 | 
					          const elements: any[] = [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              type: 'circle',
 | 
				
			||||||
 | 
					              autoBatch: true,
 | 
				
			||||||
 | 
					              shape: {
 | 
				
			||||||
 | 
					                cx: (x * scale) + offsetX,
 | 
				
			||||||
 | 
					                cy: (y * scale) + offsetY,
 | 
				
			||||||
 | 
					                r: (r * scale) - 1,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              style: {
 | 
				
			||||||
 | 
					                fill: '#5470c6',
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ];
 | 
				
			||||||
 | 
					          const labelFontSize = Math.min(36, r * scale * 0.25);
 | 
				
			||||||
 | 
					          if (labelFontSize > 8) {
 | 
				
			||||||
 | 
					            elements.push({
 | 
				
			||||||
 | 
					              type: 'text',
 | 
				
			||||||
 | 
					              x: (x * scale) + offsetX,
 | 
				
			||||||
 | 
					              y: (y * scale) + offsetY,
 | 
				
			||||||
 | 
					              style: {
 | 
				
			||||||
 | 
					                text: valueStr,
 | 
				
			||||||
 | 
					                fontSize: labelFontSize,
 | 
				
			||||||
 | 
					                fill: '#fff',
 | 
				
			||||||
 | 
					                align: 'center',
 | 
				
			||||||
 | 
					                verticalAlign: 'middle',
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            type: 'group',
 | 
				
			||||||
 | 
					            children: elements,
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }],
 | 
				
			||||||
 | 
					      tooltip: {
 | 
				
			||||||
 | 
					        backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
				
			||||||
 | 
					        borderRadius: 4,
 | 
				
			||||||
 | 
					        shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
				
			||||||
 | 
					        textStyle: {
 | 
				
			||||||
 | 
					          color: 'var(--tooltip-grey)',
 | 
				
			||||||
 | 
					          align: 'left',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        borderColor: '#000',
 | 
				
			||||||
 | 
					        formatter: (params: any): string => {
 | 
				
			||||||
 | 
					          const utxo = params.data[0] as Utxo;
 | 
				
			||||||
 | 
					          const valueStr = renderSats(utxo.value, this.stateService.network);
 | 
				
			||||||
 | 
					          return `
 | 
				
			||||||
 | 
					          <b style="color: white;">${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}</b>
 | 
				
			||||||
 | 
					          <br>
 | 
				
			||||||
 | 
					          ${valueStr}`;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.cd.markForCheck();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onChartClick(e): void {
 | 
				
			||||||
 | 
					    if (e.data?.[0]?.txid) {
 | 
				
			||||||
 | 
					      this.zone.run(() => {
 | 
				
			||||||
 | 
					        const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`);
 | 
				
			||||||
 | 
					        if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) {
 | 
				
			||||||
 | 
					          window.open(url + '?mode=details#vout=' + e.data[0].vout);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onChartInit(ec): void {
 | 
				
			||||||
 | 
					    this.chartInstance = ec;
 | 
				
			||||||
 | 
					    this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnDestroy(): void {
 | 
				
			||||||
 | 
					    if (this.subscription) {
 | 
				
			||||||
 | 
					      this.subscription.unsubscribe();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isMobile(): boolean {
 | 
				
			||||||
 | 
					    return (window.innerWidth <= 767.98);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
// Import tree-shakeable echarts
 | 
					// Import tree-shakeable echarts
 | 
				
			||||||
import * as echarts from 'echarts/core';
 | 
					import * as echarts from 'echarts/core';
 | 
				
			||||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts';
 | 
					import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
 | 
				
			||||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
 | 
					import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
 | 
				
			||||||
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
 | 
					import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
 | 
				
			||||||
// Typescript interfaces
 | 
					// Typescript interfaces
 | 
				
			||||||
@ -12,6 +12,7 @@ echarts.use([
 | 
				
			|||||||
  TitleComponent, TooltipComponent, GridComponent,
 | 
					  TitleComponent, TooltipComponent, GridComponent,
 | 
				
			||||||
  LegendComponent, GeoComponent, DataZoomComponent,
 | 
					  LegendComponent, GeoComponent, DataZoomComponent,
 | 
				
			||||||
  VisualMapComponent, MarkLineComponent,
 | 
					  VisualMapComponent, MarkLineComponent,
 | 
				
			||||||
  LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart
 | 
					  LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
 | 
				
			||||||
 | 
					  CustomChart,
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
 | 
					export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
 | 
				
			||||||
@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
 | 
				
			|||||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
 | 
					import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
 | 
				
			||||||
import { AddressComponent } from '../components/address/address.component';
 | 
					import { AddressComponent } from '../components/address/address.component';
 | 
				
			||||||
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
 | 
					import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
 | 
				
			||||||
 | 
					import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component';
 | 
				
			||||||
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
 | 
					import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
 | 
				
			||||||
import { CommonModule } from '@angular/common';
 | 
					import { CommonModule } from '@angular/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common';
 | 
				
			|||||||
    HashrateChartPoolsComponent,
 | 
					    HashrateChartPoolsComponent,
 | 
				
			||||||
    BlockHealthGraphComponent,
 | 
					    BlockHealthGraphComponent,
 | 
				
			||||||
    AddressGraphComponent,
 | 
					    AddressGraphComponent,
 | 
				
			||||||
 | 
					    UtxoGraphComponent,
 | 
				
			||||||
    ActiveAccelerationBox,
 | 
					    ActiveAccelerationBox,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
 | 
				
			|||||||
@ -233,3 +233,10 @@ interface AssetStats {
 | 
				
			|||||||
  peg_out_amount: number;
 | 
					  peg_out_amount: number;
 | 
				
			||||||
  burn_count: number;
 | 
					  burn_count: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface Utxo {
 | 
				
			||||||
 | 
					  txid: string;
 | 
				
			||||||
 | 
					  vout: number;
 | 
				
			||||||
 | 
					  value: number;
 | 
				
			||||||
 | 
					  status: Status;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,7 +13,8 @@ class GuardService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  trackerGuard(route: Route, segments: UrlSegment[]): boolean {
 | 
					  trackerGuard(route: Route, segments: UrlSegment[]): boolean {
 | 
				
			||||||
    const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
 | 
					    const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
 | 
				
			||||||
    return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98;
 | 
					    const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
 | 
				
			||||||
 | 
					    return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { Injectable } from '@angular/core';
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
import { HttpClient, HttpParams } from '@angular/common/http';
 | 
					import { HttpClient, HttpParams } from '@angular/common/http';
 | 
				
			||||||
import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
 | 
					import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs';
 | 
				
			||||||
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface';
 | 
					import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface';
 | 
				
			||||||
import { StateService } from './state.service';
 | 
					import { StateService } from './state.service';
 | 
				
			||||||
import { BlockExtended } from '../interfaces/node-api.interface';
 | 
					import { BlockExtended } from '../interfaces/node-api.interface';
 | 
				
			||||||
import { calcScriptHash$ } from '../bitcoin.utils';
 | 
					import { calcScriptHash$ } from '../bitcoin.utils';
 | 
				
			||||||
@ -166,6 +166,16 @@ export class ElectrsApiService {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAddressUtxos$(address: string): Observable<Utxo[]> {
 | 
				
			||||||
 | 
					    return this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getScriptHashUtxos$(script: string): Observable<Utxo[]> {
 | 
				
			||||||
 | 
					    return from(calcScriptHash$(script)).pipe(
 | 
				
			||||||
 | 
					      switchMap(scriptHash => this.httpClient.get<Utxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAsset$(assetId: string): Observable<Asset> {
 | 
					  getAsset$(assetId: string): Observable<Asset> {
 | 
				
			||||||
    return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
 | 
					    return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@ export class EtaService {
 | 
				
			|||||||
    return combineLatest([
 | 
					    return combineLatest([
 | 
				
			||||||
      this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
 | 
					      this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
 | 
				
			||||||
      this.stateService.difficultyAdjustment$,
 | 
					      this.stateService.difficultyAdjustment$,
 | 
				
			||||||
      miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'),
 | 
					      miningStats ? of(miningStats) : this.miningService.getMiningStats('1m'),
 | 
				
			||||||
    ]).pipe(
 | 
					    ]).pipe(
 | 
				
			||||||
      map(([mempoolPosition, da, miningStats]) => {
 | 
					      map(([mempoolPosition, da, miningStats]) => {
 | 
				
			||||||
        if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
 | 
					        if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
 | 
				
			||||||
@ -166,7 +166,7 @@ export class EtaService {
 | 
				
			|||||||
        pools[pool.poolUniqueId] = pool;
 | 
					        pools[pool.poolUniqueId] = pool;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
 | 
					      const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
 | 
				
			||||||
      const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
 | 
					      const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId]?.lastEstimatedHashrate || 0), 0);
 | 
				
			||||||
      const shares = [
 | 
					      const shares = [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          block: unacceleratedPosition.block,
 | 
					          block: unacceleratedPosition.block,
 | 
				
			||||||
@ -174,7 +174,7 @@ export class EtaService {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        ...accelerationPositions.map(pos => ({
 | 
					        ...accelerationPositions.map(pos => ({
 | 
				
			||||||
          block: pos.block,
 | 
					          block: pos.block,
 | 
				
			||||||
          hashrateShare: ((pools[pos.poolId].lastEstimatedHashrate) / miningStats.lastEstimatedHashrate)
 | 
					          hashrateShare: ((pools[pos.poolId]?.lastEstimatedHashrate || 0) / miningStats.lastEstimatedHashrate)
 | 
				
			||||||
        }))
 | 
					        }))
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
      return this.calculateETAFromShares(shares, da);
 | 
					      return this.calculateETAFromShares(shares, da);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
 | 
					import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
 | 
				
			||||||
import { TransactionStripped } from "../interfaces/node-api.interface";
 | 
					import { TransactionStripped } from "../interfaces/node-api.interface";
 | 
				
			||||||
 | 
					import { AmountShortenerPipe } from "./pipes/amount-shortener.pipe";
 | 
				
			||||||
 | 
					const amountShortenerPipe = new AmountShortenerPipe();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isMobile(): boolean {
 | 
					export function isMobile(): boolean {
 | 
				
			||||||
  return (window.innerWidth <= 767.98);
 | 
					  return (window.innerWidth <= 767.98);
 | 
				
			||||||
@ -184,6 +186,33 @@ export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCom
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string {
 | 
				
			||||||
 | 
					  let prefix = '';
 | 
				
			||||||
 | 
					  switch (network) {
 | 
				
			||||||
 | 
					    case 'liquid':
 | 
				
			||||||
 | 
					      prefix = 'L';
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'liquidtestnet':
 | 
				
			||||||
 | 
					      prefix = 'tL';
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'testnet':
 | 
				
			||||||
 | 
					    case 'testnet4':
 | 
				
			||||||
 | 
					      prefix = 't';
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 'signet':
 | 
				
			||||||
 | 
					      prefix = 's';
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) {
 | 
				
			||||||
 | 
					    return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    if (prefix.length) {
 | 
				
			||||||
 | 
					      prefix += '-';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return `${amountShortenerPipe.transform(value)} ${prefix}sats`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function insecureRandomUUID(): string {
 | 
					export function insecureRandomUUID(): string {
 | 
				
			||||||
  const hexDigits = '0123456789abcdef';
 | 
					  const hexDigits = '0123456789abcdef';
 | 
				
			||||||
  const uuidLengths = [8, 4, 4, 4, 12];
 | 
					  const uuidLengths = [8, 4, 4, 4, 12];
 | 
				
			||||||
 | 
				
			|||||||
@ -13,8 +13,13 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        @if (!enterpriseInfo?.footer_img) {
 | 
					        @if (!enterpriseInfo?.footer_img) {
 | 
				
			||||||
          <p class="explore-tagline-mobile">
 | 
					          <p class="explore-tagline-mobile">
 | 
				
			||||||
 | 
					            @if (officialMempoolSpace) {
 | 
				
			||||||
              <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
 | 
					              <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
 | 
				
			||||||
              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>  
 | 
					              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>  
 | 
				
			||||||
 | 
					            } @else {
 | 
				
			||||||
 | 
					              <ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
 | 
				
			||||||
 | 
					              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">™</ng-template>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        <div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
 | 
					        <div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
 | 
				
			||||||
@ -52,8 +57,13 @@
 | 
				
			|||||||
            <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
 | 
					            <span *ngIf="!user" i18n="shared.sign-in" class="nowrap">Sign In</span>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
          <p class="explore-tagline-desktop">
 | 
					          <p class="explore-tagline-desktop">
 | 
				
			||||||
 | 
					            @if (officialMempoolSpace) {
 | 
				
			||||||
              <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
 | 
					              <ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
 | 
				
			||||||
              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>  
 | 
					              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">®</ng-template>  
 | 
				
			||||||
 | 
					            } @else {
 | 
				
			||||||
 | 
					              <ng-container i18n="shared.be-your-own-explorer">Be your own explorer</ng-container>
 | 
				
			||||||
 | 
					              <ng-template [ngIf]="locale.substr(0, 2) === 'en'">™</ng-template>
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user