import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core"; import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms"; import { Subscription } from "rxjs"; import { StorageService } from "../../services/storage.service"; import { ServicesApiServices } from "../../services/services-api.service"; import { getRegex } from "../../shared/regex.utils"; import { StateService } from "../../services/state.service"; import { WebsocketService } from "../../services/websocket.service"; import { AudioService } from "../../services/audio.service"; import { HttpErrorResponse } from "@angular/common/http"; @Component({ selector: 'app-faucet', templateUrl: './faucet.component.html', styleUrls: ['./faucet.component.scss'] }) export class FaucetComponent implements OnInit, OnDestroy { loading = true; error: string = ''; user: any = undefined; txid: string = ''; faucetStatusSubscription: Subscription; status: { min: number; // minimum amount to request at once (in sats) max: number; // maximum amount to request at once address?: string; // faucet address code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'; } | null = null; faucetForm: FormGroup; mempoolPositionSubscription: Subscription; confirmationSubscription: Subscription; constructor( private cd: ChangeDetectorRef, private storageService: StorageService, private servicesApiService: ServicesApiServices, private formBuilder: FormBuilder, private stateService: StateService, private websocketService: WebsocketService, private audioService: AudioService ) { this.initForm(5000, 500_000, null); } 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; if (!this.user) { this.loading = false; return; } // Setup form this.updateFaucetStatus(); // Track transaction this.websocketService.want(['blocks', 'mempool-blocks']); this.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { if (txPosition && txPosition.txid === this.txid) { this.stateService.markBlock$.next({ txid: txPosition.txid, mempoolPosition: txPosition.position, }); } }); this.confirmationSubscription = this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => { if (txConfirmed && txConfirmed === this.txid) { this.stateService.markBlock$.next({ blockHeight: block.height }); } }); } updateFaucetStatus(): void { this.servicesApiService.getFaucetStatus$().subscribe({ next: (status) => { if (!status) { this.error = 'internal_server_error'; return; } this.status = status; if (this.status.code !== 'ok') { this.error = this.status.code; this.updateForm(this.status.min ?? 5000, this.status.max ?? 500_000, this.status.address); return; } // update the form with the proper validation parameters this.updateForm(this.status.min, this.status.max, this.status.address); }, error: (response) => { this.loading = false; this.error = response.error; this.cd.markForCheck(); } }); } requestCoins(): void { if (this.isDisabled()) { return; } this.error = null; this.txid = ''; 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'); this.updateFaucetStatus(); this.cd.markForCheck(); }), error: (response: HttpErrorResponse) => { this.error = response.error; }, }); } isDisabled(): boolean { return !(this.user && this.status?.code === 'ok' && !this.error); } getNotFaucetAddressValidator(faucetAddress: string): ValidatorFn { return faucetAddress ? (control: AbstractControl): ValidationErrors | null => { const forbidden = control.value === faucetAddress; return forbidden ? { forbiddenAddress: { value: control.value } } : null; }: () => null; } initForm(min: number, max: number, faucetAddress: string): void { this.faucetForm = this.formBuilder.group({ 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]], 'satoshis': [min, [Validators.required, Validators.min(min), Validators.max(max)]] }); this.loading = false; this.cd.markForCheck(); } updateForm(min, max, faucetAddress: string): void { if (!this.faucetForm) { this.initForm(min, max, faucetAddress); } else { this.faucetForm.get('address').setValidators([Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]); this.faucetForm.get('satoshis').setValidators([Validators.required, Validators.min(min), Validators.max(max)]); this.faucetForm.get('satoshis').setValue(Math.max(min, this.faucetForm.get('satoshis').value)); this.faucetForm.get('satoshis').updateValueAndValidity(); this.faucetForm.get('satoshis').markAsDirty(); } } setAmount(value: number): void { if (this.faucetForm) { this.faucetForm.get('satoshis').setValue(value); this.faucetForm.get('satoshis').updateValueAndValidity(); this.faucetForm.get('satoshis').markAsDirty(); } } get amount() { return this.faucetForm.get('satoshis')!; } get invalidAmount() { const amount = this.faucetForm.get('satoshis')!; return amount?.invalid && (amount.dirty || amount.touched) } get address() { return this.faucetForm.get('address')!; } get invalidAddress() { const address = this.faucetForm.get('address')!; return address?.invalid && (address.dirty || address.touched) } }