Merge pull request #5080 from mempool/nymkappa/faucet-polish
[faucet] polish code and UX - Move Twitter login component into FOSS
This commit is contained in:
		
						commit
						2ce1cc24b9
					
				@ -4,43 +4,54 @@
 | 
				
			|||||||
    <h1 i18n="testnet4.faucet">Testnet4 Faucet</h1>
 | 
					    <h1 i18n="testnet4.faucet">Testnet4 Faucet</h1>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @if (error) {
 | 
					 | 
				
			||||||
    <div class="alert alert-danger">
 | 
					 | 
				
			||||||
      @switch (error) {
 | 
					 | 
				
			||||||
        @case ('faucet_too_soon') {
 | 
					 | 
				
			||||||
          Too many requests! Try again later.
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @case ('faucet_maximum_reached') {
 | 
					 | 
				
			||||||
          You have exceeded your testnet4 allowance. Try again later.
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @case ('faucet_not_available') {
 | 
					 | 
				
			||||||
          The faucet is not available right now. Try again later.
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @default {
 | 
					 | 
				
			||||||
          Sorry, something went wrong! Try again later.
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div class="faucet-container text-center">
 | 
					  <div class="faucet-container text-center">
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
    @if (txid) {
 | 
					    @if (txid) {
 | 
				
			||||||
      <div class="alert alert-mempool d-block text-center">
 | 
					      <div class="alert alert-success w-100 text-truncate">
 | 
				
			||||||
        <a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid }}</a>
 | 
					        <fa-icon [icon]="['fas', 'circle-check']"></fa-icon>
 | 
				
			||||||
 | 
					        Sent!
 | 
				
			||||||
 | 
					        <a class="text-primary" [href]="'/testnet4/txid/' + txid">{{ txid }}</a>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    } @else if (loading) {
 | 
					    }
 | 
				
			||||||
      <p>Waiting for faucet...</p>
 | 
					    @else if (loading) {
 | 
				
			||||||
 | 
					      <p>Loading faucet...</p>
 | 
				
			||||||
      <div class="spinner-border text-light"></div>
 | 
					      <div class="spinner-border text-light"></div>
 | 
				
			||||||
    } @else {
 | 
					    } @else if (!user) {
 | 
				
			||||||
      <form [formGroup]="faucetForm" class="formGroup" (submit)="requestCoins()">
 | 
					      <!-- User not logged in -->
 | 
				
			||||||
 | 
					      <div class="alert alert-mempool d-block text-center w-100">
 | 
				
			||||||
 | 
					        <div class="d-inline align-middle">
 | 
				
			||||||
 | 
					          <span>To limit abuse, </span>
 | 
				
			||||||
 | 
					          <a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">authenticate </a>
 | 
				
			||||||
 | 
					          <span class="mr-2">or</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    @else if (error === 'not_available') {
 | 
				
			||||||
 | 
					      <!-- User logged in but not a paid user or did not link its Twitter account -->
 | 
				
			||||||
 | 
					      <div class="alert alert-mempool d-block text-center w-100">
 | 
				
			||||||
 | 
					        <div class="d-inline align-middle">
 | 
				
			||||||
 | 
					          <span class="mb-2 mr-2">To limit abuse</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @else if (error) {
 | 
				
			||||||
 | 
					      <!-- User can request -->
 | 
				
			||||||
 | 
					      <app-mempool-error class="w-100" [error]="error"></app-mempool-error>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @if (!loading) {
 | 
				
			||||||
 | 
					      <form [formGroup]="faucetForm" class="formGroup" (submit)="requestCoins()" [style]="(error || !this.user) ? 'opacity: 0.3; pointer-events: none' : ''">
 | 
				
			||||||
        <div class="row">
 | 
					        <div class="row">
 | 
				
			||||||
          <div class="col">
 | 
					          <div class="col">
 | 
				
			||||||
            <div class="form-group">
 | 
					            <div class="form-group mb-0">
 | 
				
			||||||
              <div class="input-group input-group-lg mb-2">
 | 
					              <div class="input-group input-group-lg">
 | 
				
			||||||
                <div class="input-group-prepend">
 | 
					                <div class="input-group-prepend">
 | 
				
			||||||
                  <span class="input-group-text" i18n="amount-sats">Amount (sats)</span>
 | 
					                  <span class="input-group-text" i18n="amount-sats">Amount (sats)</span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <input type="number" class="form-control" formControlName="satoshis" id="satoshis">
 | 
					                <input type="number" class="form-control" [class]="{invalid: invalidAmount}" formControlName="satoshis" id="satoshis">
 | 
				
			||||||
                <div class="button-group">
 | 
					                <div class="button-group">
 | 
				
			||||||
                  <button type="button" class="btn btn-secondary" (click)="setAmount(5000)">5k</button>
 | 
					                  <button type="button" class="btn btn-secondary" (click)="setAmount(5000)">5k</button>
 | 
				
			||||||
                  <button type="button" class="btn btn-secondary ml-2" (click)="setAmount(50000)">50k</button>
 | 
					                  <button type="button" class="btn btn-secondary ml-2" (click)="setAmount(50000)">50k</button>
 | 
				
			||||||
@ -49,43 +60,32 @@
 | 
				
			|||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="text-danger text-left" *ngIf="invalidAmount">
 | 
					              <div class="text-danger text-left" *ngIf="invalidAmount">
 | 
				
			||||||
                <div *ngIf="amount?.errors?.['required']">Amount is required</div>
 | 
					                <div *ngIf="amount?.errors?.['required']">Amount is required</div>
 | 
				
			||||||
                <div *ngIf="status?.user_requests && amount?.errors?.['min']">Minimum is {{ amount?.errors?.['min'].min }}</div>
 | 
					                <div *ngIf="amount?.errors?.['min']">Minimum is {{ amount?.errors?.['min'].min | number }} tSats</div>
 | 
				
			||||||
                <div *ngIf="status?.user_requests && amount?.errors?.['max']">Maximum is {{ amount?.errors?.['max'].max }}</div>
 | 
					                <div *ngIf="amount?.errors?.['max']">Maximum is {{ amount?.errors?.['max'].max | number }} tSats</div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="input-group input-group-lg mb-2">
 | 
					              <div class="input-group input-group-lg mt-2">
 | 
				
			||||||
                <div class="input-group-prepend">
 | 
					                <div class="input-group-prepend">
 | 
				
			||||||
                  <span class="input-group-text" i18n="address">Address</span>
 | 
					                  <span class="input-group-text" i18n="address">Address</span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <input type="address" class="form-control" formControlName="address" id="address" placeholder="tb1q...">
 | 
					                <input type="text" class="form-control" [class]="{invalid: invalidAddress}" formControlName="address" id="address" placeholder="tb1q...">
 | 
				
			||||||
                <button type="submit" class="btn btn-primary submit-button" [disabled]="!status?.access || !faucetForm.valid || !faucetForm.get('address')?.dirty" i18n="testnet4.request-coins">Request Testnet4 Coins</button>
 | 
					                <button type="submit" class="btn btn-primary submit-button" [disabled]="!faucetForm.valid || !faucetForm.get('address')?.dirty" i18n="testnet4.request-coins">Request Testnet4 Coins</button>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="text-danger text-left" *ngIf="invalidAddress">
 | 
					              <div class="text-danger text-left" *ngIf="invalidAddress">
 | 
				
			||||||
                @if (address?.errors?.['required']) {
 | 
					                <div *ngIf="address?.errors?.['required']">Address is required</div>
 | 
				
			||||||
                  <div>Address is required</div>
 | 
					                <div *ngIf="address?.errors?.['pattern']">Must be a valid testnet4 address</div>
 | 
				
			||||||
                } @else {
 | 
					                <div *ngIf="address?.errors?.['forbiddenAddress']">You cannot use this address</div>
 | 
				
			||||||
                  <div>Must be a valid testnet4 address</div>
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
              <div class="text-danger text-left" *ngIf="status && !status.user_requests">
 | 
					 | 
				
			||||||
                <div>Too many requests! Try again later.</div>
 | 
					 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
      @if (!user) {
 | 
					 | 
				
			||||||
        <div class="alert alert-mempool d-block">
 | 
					 | 
				
			||||||
          To limit abuse, please <a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">log in</a> or <a routerLink="/signup" [queryParams]="{'redirectTo': '/testnet4/faucet'}">sign up</a> and link your Twitter account to use the faucet.
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      } @else if (!status?.access) {
 | 
					 | 
				
			||||||
        <div class="alert alert-mempool d-block">
 | 
					 | 
				
			||||||
          To use this feature, please <a routerLink="/services/account/settings">link your Twitter account</a>.
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    <br>
 | 
					
 | 
				
			||||||
    <div *ngIf="status?.address">
 | 
					    <!-- Send back coins -->
 | 
				
			||||||
      If you no longer need your testnet4 coins, please consider sending them back to <a [routerLink]="['/address/' | relativeUrl, status.address]">{{ status.address }}</a> to replenish the faucet.
 | 
					    @if (status?.address) {  
 | 
				
			||||||
    </div>
 | 
					      <div class="mt-2 alert alert-info w-100">If you no longer need your testnet4 coins, please consider <a class="text-primary" [routerLink]="['/address/' | relativeUrl, status.address]"><u>sending them back</u></a> to replenish the faucet.</div>
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
@ -23,6 +23,9 @@
 | 
				
			|||||||
  .submit-button, .button-group, .button-group .btn {
 | 
					  .submit-button, .button-group, .button-group .btn {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  .submit-button:disabled {
 | 
				
			||||||
 | 
					    pointer-events: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  #satoshis::after {
 | 
					  #satoshis::after {
 | 
				
			||||||
    content: 'sats';
 | 
					    content: 'sats';
 | 
				
			||||||
@ -42,3 +45,8 @@
 | 
				
			|||||||
  max-width: 800px;
 | 
					  max-width: 800px;
 | 
				
			||||||
  margin: auto;
 | 
					  margin: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.invalid {
 | 
				
			||||||
 | 
					  border-width: 1px;
 | 
				
			||||||
 | 
					  border-color: var(--red);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,13 @@
 | 
				
			|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
 | 
					import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
 | 
				
			||||||
import { FormBuilder, FormGroup, ValidationErrors, Validators } from "@angular/forms";
 | 
					import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
 | 
				
			||||||
import { StorageService } from '../../services/storage.service';
 | 
					import { Subscription } from "rxjs";
 | 
				
			||||||
import { ServicesApiServices } from '../../services/services-api.service';
 | 
					import { StorageService } from "../../services/storage.service";
 | 
				
			||||||
import { AudioService } from '../../services/audio.service';
 | 
					import { ServicesApiServices } from "../../services/services-api.service";
 | 
				
			||||||
import { StateService } from '../../services/state.service';
 | 
					 | 
				
			||||||
import { Subscription, tap } from "rxjs";
 | 
					 | 
				
			||||||
import { HttpErrorResponse } from "@angular/common/http";
 | 
					 | 
				
			||||||
import { getRegex } from "../../shared/regex.utils";
 | 
					import { getRegex } from "../../shared/regex.utils";
 | 
				
			||||||
 | 
					import { StateService } from "../../services/state.service";
 | 
				
			||||||
import { WebsocketService } from "../../services/websocket.service";
 | 
					import { WebsocketService } from "../../services/websocket.service";
 | 
				
			||||||
 | 
					import { AudioService } from "../../services/audio.service";
 | 
				
			||||||
 | 
					import { HttpErrorResponse } from "@angular/common/http";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-faucet',
 | 
					  selector: 'app-faucet',
 | 
				
			||||||
@ -15,52 +15,91 @@ import { WebsocketService } from "../../services/websocket.service";
 | 
				
			|||||||
  styleUrls: ['./faucet.component.scss']
 | 
					  styleUrls: ['./faucet.component.scss']
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class FaucetComponent implements OnInit, OnDestroy {
 | 
					export class FaucetComponent implements OnInit, OnDestroy {
 | 
				
			||||||
  user: any;
 | 
					  loading = true;
 | 
				
			||||||
  loading: boolean = true;
 | 
					  error: string = '';
 | 
				
			||||||
 | 
					  user: any = undefined;
 | 
				
			||||||
 | 
					  txid: string = '';
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					  faucetStatusSubscription: Subscription;
 | 
				
			||||||
  status: {
 | 
					  status: {
 | 
				
			||||||
    address?: string,
 | 
					    min: number; // minimum amount to request at once (in sats)
 | 
				
			||||||
    access: boolean
 | 
					    max: number; // maximum amount to request at once
 | 
				
			||||||
    min: number,
 | 
					    address?: string; // faucet address
 | 
				
			||||||
    user_max: number,
 | 
					    code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon';
 | 
				
			||||||
    user_requests: number,
 | 
					 | 
				
			||||||
  } | null = null;
 | 
					  } | null = null;
 | 
				
			||||||
  error = '';
 | 
					 | 
				
			||||||
  faucetForm: FormGroup;
 | 
					  faucetForm: FormGroup;
 | 
				
			||||||
  txid = '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mempoolPositionSubscription: Subscription;
 | 
					  mempoolPositionSubscription: Subscription;
 | 
				
			||||||
  confirmationSubscription: Subscription;
 | 
					  confirmationSubscription: Subscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private stateService: StateService,
 | 
					    private cd: ChangeDetectorRef,
 | 
				
			||||||
    private storageService: StorageService,
 | 
					    private storageService: StorageService,
 | 
				
			||||||
    private servicesApiService: ServicesApiServices,
 | 
					    private servicesApiService: ServicesApiServices,
 | 
				
			||||||
    private websocketService: WebsocketService,
 | 
					 | 
				
			||||||
    private audioService: AudioService,
 | 
					 | 
				
			||||||
    private formBuilder: FormBuilder,
 | 
					    private formBuilder: FormBuilder,
 | 
				
			||||||
 | 
					    private stateService: StateService,
 | 
				
			||||||
 | 
					    private websocketService: WebsocketService,
 | 
				
			||||||
 | 
					    private audioService: AudioService
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.faucetForm = this.formBuilder.group({
 | 
				
			||||||
 | 
					      'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]],
 | 
				
			||||||
 | 
					      'satoshis': [0, [Validators.required, Validators.min(0), Validators.max(0)]]
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngOnDestroy() {
 | 
				
			||||||
 | 
					    this.stateService.markBlock$.next({});
 | 
				
			||||||
 | 
					    this.websocketService.stopTrackingTransaction();
 | 
				
			||||||
 | 
					    if (this.mempoolPositionSubscription) {
 | 
				
			||||||
 | 
					      this.mempoolPositionSubscription.unsubscribe();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.confirmationSubscription) {
 | 
				
			||||||
 | 
					      this.confirmationSubscription.unsubscribe();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngOnInit() {
 | 
				
			||||||
    this.user = this.storageService.getAuth()?.user ?? null;
 | 
					    this.user = this.storageService.getAuth()?.user ?? null;
 | 
				
			||||||
    this.initForm(5000, 500000);
 | 
					    if (!this.user) {
 | 
				
			||||||
    if (this.user) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        this.servicesApiService.getFaucetStatus$().subscribe(status => {
 | 
					 | 
				
			||||||
          this.status = status;
 | 
					 | 
				
			||||||
          this.initForm(this.status.min, this.status.user_max);
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        if (e?.status !== 403) {
 | 
					 | 
				
			||||||
          this.error = 'faucet_not_available';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } finally {
 | 
					 | 
				
			||||||
        this.loading = false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this.loading = false;
 | 
					      this.loading = false;
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Setup form
 | 
				
			||||||
 | 
					    this.faucetStatusSubscription = this.servicesApiService.getFaucetStatus$().subscribe({
 | 
				
			||||||
 | 
					      next: (status) => {
 | 
				
			||||||
 | 
					        if (!status) {
 | 
				
			||||||
 | 
					          this.error = 'internal_server_error';
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.status = status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const notFaucetAddressValidator = (faucetAddress: string): ValidatorFn => {
 | 
				
			||||||
 | 
					          return (control: AbstractControl): ValidationErrors | null => {
 | 
				
			||||||
 | 
					            const forbidden = control.value === faucetAddress;
 | 
				
			||||||
 | 
					            return forbidden ? { forbiddenAddress: { value: control.value } } : null;
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.faucetForm = this.formBuilder.group({
 | 
				
			||||||
 | 
					          'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), notFaucetAddressValidator(this.status.address)]],
 | 
				
			||||||
 | 
					          'satoshis': [this.status.min, [Validators.required, Validators.min(this.status.min), Validators.max(this.status.max)]]
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.status.code !== 'ok') {
 | 
				
			||||||
 | 
					          this.error = this.status.code;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loading = false;
 | 
				
			||||||
 | 
					        this.cd.markForCheck();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      error: (response) => {
 | 
				
			||||||
 | 
					        this.loading = false;
 | 
				
			||||||
 | 
					        this.error = response.error;
 | 
				
			||||||
 | 
					        this.cd.markForCheck();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Track transaction
 | 
				
			||||||
    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
					    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
				
			||||||
    this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
 | 
					    this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => {
 | 
				
			||||||
      if (txPosition && txPosition.txid === this.txid) {
 | 
					      if (txPosition && txPosition.txid === this.txid) {
 | 
				
			||||||
@ -78,18 +117,22 @@ export class FaucetComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  initForm(min: number, max: number): void {
 | 
					  requestCoins(): void {
 | 
				
			||||||
    this.faucetForm = this.formBuilder.group({
 | 
					    this.error = null;
 | 
				
			||||||
      'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]],
 | 
					    this.txid = '';
 | 
				
			||||||
      'satoshis': ['', [Validators.required, Validators.min(min), Validators.max(max)]]
 | 
					    this.stateService.markBlock$.next({});
 | 
				
			||||||
    }, { validators: (formGroup): ValidationErrors | null => {
 | 
					    this.servicesApiService.requestTestnet4Coins$(this.faucetForm.get('address')?.value, parseInt(this.faucetForm.get('satoshis')?.value))
 | 
				
			||||||
      if (this.status && !this.status?.user_requests) {
 | 
					    .subscribe({
 | 
				
			||||||
        return { customError: 'You have used the faucet too many times already! Come back later.'}
 | 
					      next: ((response) => {
 | 
				
			||||||
      }
 | 
					        this.txid = response.txid;
 | 
				
			||||||
      return null;
 | 
					        this.websocketService.startTrackTransaction(this.txid);
 | 
				
			||||||
    }});
 | 
					        this.audioService.playSound('cha-ching');
 | 
				
			||||||
    this.faucetForm.get('satoshis').setValue(min);
 | 
					        this.cd.markForCheck();
 | 
				
			||||||
    this.loading = false;
 | 
					      }),
 | 
				
			||||||
 | 
					      error: (response: HttpErrorResponse) => {
 | 
				
			||||||
 | 
					        this.error = response.error;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setAmount(value: number): void {
 | 
					  setAmount(value: number): void {
 | 
				
			||||||
@ -98,39 +141,13 @@ export class FaucetComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  requestCoins(): void {
 | 
					 | 
				
			||||||
    this.error = null;
 | 
					 | 
				
			||||||
    this.stateService.markBlock$.next({});
 | 
					 | 
				
			||||||
    this.servicesApiService.requestTestnet4Coins$(this.faucetForm.get('address')?.value, parseInt(this.faucetForm.get('satoshis')?.value))
 | 
					 | 
				
			||||||
    .subscribe({
 | 
					 | 
				
			||||||
      next: ((response) => {
 | 
					 | 
				
			||||||
        this.txid = response.txid;
 | 
					 | 
				
			||||||
        this.websocketService.startTrackTransaction(this.txid);
 | 
					 | 
				
			||||||
        this.audioService.playSound('cha-ching');
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
      error: (response: HttpErrorResponse) => {
 | 
					 | 
				
			||||||
        this.error = response.error;
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ngOnDestroy(): void {
 | 
					 | 
				
			||||||
    this.stateService.markBlock$.next({});
 | 
					 | 
				
			||||||
    this.websocketService.stopTrackingTransaction();
 | 
					 | 
				
			||||||
    if (this.mempoolPositionSubscription) {
 | 
					 | 
				
			||||||
      this.mempoolPositionSubscription.unsubscribe();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (this.confirmationSubscription) {
 | 
					 | 
				
			||||||
      this.confirmationSubscription.unsubscribe();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  get amount() { return this.faucetForm.get('satoshis')!; }
 | 
					  get amount() { return this.faucetForm.get('satoshis')!; }
 | 
				
			||||||
  get address() { return this.faucetForm.get('address')!; }
 | 
					 | 
				
			||||||
  get invalidAmount() {
 | 
					  get invalidAmount() {
 | 
				
			||||||
    const amount = this.faucetForm.get('satoshis')!;
 | 
					    const amount = this.faucetForm.get('satoshis')!;
 | 
				
			||||||
    return amount?.invalid && (amount.dirty || amount.touched)
 | 
					    return amount?.invalid && (amount.dirty || amount.touched)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get address() { return this.faucetForm.get('address')!; }
 | 
				
			||||||
  get invalidAddress() {
 | 
					  get invalidAddress() {
 | 
				
			||||||
    const address = this.faucetForm.get('address')!;
 | 
					    const address = this.faucetForm.get('address')!;
 | 
				
			||||||
    return address?.invalid && (address.dirty || address.touched)
 | 
					    return address?.invalid && (address.dirty || address.touched)
 | 
				
			||||||
 | 
				
			|||||||
@ -102,7 +102,7 @@
 | 
				
			|||||||
      <li class="nav-item" routerLinkActive="active" id="btn-graphs">
 | 
					      <li class="nav-item" routerLinkActive="active" id="btn-graphs">
 | 
				
			||||||
        <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
 | 
					        <a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
      <li class="nav-item" routerLinkActive="active" id="btn-faucet" *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.network === 'testnet4'">
 | 
					      <li class="nav-item" routerLinkActive="active" id="btn-faucet" *ngIf="stateService.isMempoolSpaceBuild && stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.network === 'testnet4'">
 | 
				
			||||||
        <a class="nav-link" [routerLink]="['/faucet' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'faucet-drip']" [fixedWidth]="true" i18n-title="master-page.faucet" title="Faucet"></fa-icon></a>
 | 
					        <a class="nav-link" [routerLink]="['/faucet' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'faucet-drip']" [fixedWidth]="true" i18n-title="master-page.faucet" title="Faucet"></fa-icon></a>
 | 
				
			||||||
      </li>
 | 
					      </li>
 | 
				
			||||||
      <li class="nav-item" routerLinkActive="active" id="btn-docs">
 | 
					      <li class="nav-item" routerLinkActive="active" id="btn-docs">
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<a href="#" (click)="twitterLogin()"
 | 
				
			||||||
 | 
					  [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')"
 | 
				
			||||||
 | 
					  style="background-color: #1DA1F2" [style]="width ? 'width: ' + width : ''">
 | 
				
			||||||
 | 
					  <img src="./resources/twitter.svg" height="25" style="padding: 2px" [alt]="buttonString + ' with Twitter'" />
 | 
				
			||||||
 | 
					  <span class="ml-2 text-light align-middle">{{ buttonString }}</span>
 | 
				
			||||||
 | 
					</a>
 | 
				
			||||||
@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					import { Component, EventEmitter, Input, Output } from '@angular/core';
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-twitter-login',
 | 
				
			||||||
 | 
					  templateUrl: './twitter-login.component.html',
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class TwitterLogin {
 | 
				
			||||||
 | 
					  @Input() width: string | null = null;
 | 
				
			||||||
 | 
					  @Input() customClass: string | null = null;
 | 
				
			||||||
 | 
					  @Input() buttonString: string= 'unset';
 | 
				
			||||||
 | 
					  @Input() redirectTo: string | null = null;
 | 
				
			||||||
 | 
					  @Output() clicked = new EventEmitter<boolean>();
 | 
				
			||||||
 | 
					  @Input() disabled: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  twitterLogin() {
 | 
				
			||||||
 | 
					    this.clicked.emit(true);
 | 
				
			||||||
 | 
					    if (this.redirectTo) {
 | 
				
			||||||
 | 
					      location.replace(`/api/v1/services/auth/login/twitter?redirectTo=${encodeURIComponent(this.redirectTo)}`);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      location.replace(`/api/v1/services/auth/login/twitter?redirectTo=${location.href}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -105,19 +105,21 @@ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
 | 
				
			|||||||
    data: { networks: ['bitcoin', 'liquid'] },
 | 
					    data: { networks: ['bitcoin', 'liquid'] },
 | 
				
			||||||
    component: ServerStatusComponent
 | 
					    component: ServerStatusComponent
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  routes[0].children.push({
 | 
					  if (window['isMempoolSpaceBuild']) {
 | 
				
			||||||
    path: 'faucet',
 | 
					    routes[0].children.push({
 | 
				
			||||||
    canActivate: [(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
 | 
					      path: 'faucet',
 | 
				
			||||||
      return state.url.startsWith('/testnet4/');
 | 
					      canActivate: [(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
 | 
				
			||||||
    }],
 | 
					        return state.url.startsWith('/testnet4/');
 | 
				
			||||||
    component: StartComponent,
 | 
					      }],
 | 
				
			||||||
    data: { preload: true, networkSpecific: true },
 | 
					      component: StartComponent,
 | 
				
			||||||
    children: [{
 | 
					      data: { preload: true, networkSpecific: true },
 | 
				
			||||||
      path: '',
 | 
					      children: [{
 | 
				
			||||||
      data: { networks: ['bitcoin'] },
 | 
					        path: '',
 | 
				
			||||||
      component: FaucetComponent,
 | 
					        data: { networks: ['bitcoin'] },
 | 
				
			||||||
    }]
 | 
					        component: FaucetComponent,
 | 
				
			||||||
  })
 | 
					      }]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
 | 
				
			|||||||
@ -161,7 +161,7 @@ export class ServicesApiServices {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getFaucetStatus$() {
 | 
					  getFaucetStatus$() {
 | 
				
			||||||
    return this.httpClient.get<{ address?: string, access: boolean, min: number, user_max: number, user_requests: number }>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' });
 | 
					    return this.httpClient.get<{ address?: string, min: number, max: number, code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'}>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  requestTestnet4Coins$(address: string, sats: number) {
 | 
					  requestTestnet4Coins$(address: string, sats: number) {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { Component, Input, OnInit } from "@angular/core";
 | 
				
			|||||||
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
 | 
					import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MempoolErrors = {
 | 
					const MempoolErrors = {
 | 
				
			||||||
 | 
					  'bad_request': `Your request was not valid. Please try again.`,
 | 
				
			||||||
  'internal_server_error': `Something went wrong, please try again later`,
 | 
					  'internal_server_error': `Something went wrong, please try again later`,
 | 
				
			||||||
  'acceleration_duplicated': `This transaction has already been accelerated.`,
 | 
					  'acceleration_duplicated': `This transaction has already been accelerated.`,
 | 
				
			||||||
  'acceleration_outbid': `Your fee delta is too low.`,
 | 
					  'acceleration_outbid': `Your fee delta is too low.`,
 | 
				
			||||||
@ -22,6 +23,12 @@ const MempoolErrors = {
 | 
				
			|||||||
  'waitlisted': `You are currently on the wait list. You will get notified once you are granted access.`,  
 | 
					  'waitlisted': `You are currently on the wait list. You will get notified once you are granted access.`,  
 | 
				
			||||||
  'not_whitelisted_by_any_pool': `You are not whitelisted by any mining pool`,
 | 
					  'not_whitelisted_by_any_pool': `You are not whitelisted by any mining pool`,
 | 
				
			||||||
  'unauthorized': `You are not authorized to do this`,
 | 
					  'unauthorized': `You are not authorized to do this`,
 | 
				
			||||||
 | 
					  'faucet_too_soon': `You cannot request any more coins right now. Try again later.`,
 | 
				
			||||||
 | 
					  'faucet_not_available': `The faucet is not available right now. Try again later.`,
 | 
				
			||||||
 | 
					  'faucet_maximum_reached': `You are not allowed to request more coins`,
 | 
				
			||||||
 | 
					  'faucet_address_not_allowed': `You cannot use this address`,
 | 
				
			||||||
 | 
					  'faucet_below_minimum': `Requested amount is too small`,
 | 
				
			||||||
 | 
					  'faucet_above_maximum': `Requested amount is too high`,
 | 
				
			||||||
} as { [error: string]: string };
 | 
					} as { [error: string]: string };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isMempoolError(error: string) {
 | 
					export function isMempoolError(error: string) {
 | 
				
			||||||
 | 
				
			|||||||
@ -115,6 +115,7 @@ import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe';
 | 
				
			|||||||
import { HttpErrorComponent } from '../shared/components/http-error/http-error.component';
 | 
					import { HttpErrorComponent } from '../shared/components/http-error/http-error.component';
 | 
				
			||||||
import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component';
 | 
					import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component';
 | 
				
			||||||
import { FaucetComponent } from '../components/faucet/faucet.component';
 | 
					import { FaucetComponent } from '../components/faucet/faucet.component';
 | 
				
			||||||
 | 
					import { TwitterLogin } from '../components/twitter-login/twitter-login.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
 | 
					import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -230,6 +231,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
				
			|||||||
    HttpErrorComponent,
 | 
					    HttpErrorComponent,
 | 
				
			||||||
    TwitterWidgetComponent,
 | 
					    TwitterWidgetComponent,
 | 
				
			||||||
    FaucetComponent,
 | 
					    FaucetComponent,
 | 
				
			||||||
 | 
					    TwitterLogin,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    CommonModule,
 | 
					    CommonModule,
 | 
				
			||||||
@ -359,6 +361,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
				
			|||||||
    PendingStatsComponent,
 | 
					    PendingStatsComponent,
 | 
				
			||||||
    HttpErrorComponent,
 | 
					    HttpErrorComponent,
 | 
				
			||||||
    TwitterWidgetComponent,
 | 
					    TwitterWidgetComponent,
 | 
				
			||||||
 | 
					    TwitterLogin,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MempoolBlockOverviewComponent,
 | 
					    MempoolBlockOverviewComponent,
 | 
				
			||||||
    ClockchainComponent,
 | 
					    ClockchainComponent,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user