diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 1030087b1..b75112708 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -4,43 +4,54 @@

Testnet4 Faucet

- @if (error) { -
- @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. - } - } -
- } -
+ @if (txid) { -
- {{ txid }} +
+ + Sent! + {{ txid }}
- } @else if (loading) { -

Waiting for faucet...

+ } + @else if (loading) { +

Loading faucet...

- } @else { -
+ } @else if (!user) { + +
+
+ To limit abuse,  + authenticate  + or +
+ +
+ } + @else if (error === 'not_available') { + +
+
+ To limit abuse +
+ +
+ } + + @else if (error) { + + + } + + @if (!loading) { +
-
-
+
+
Amount (sats)
- +
@@ -49,43 +60,32 @@
Amount is required
-
Minimum is {{ amount?.errors?.['min'].min }}
-
Maximum is {{ amount?.errors?.['max'].max }}
+
Minimum is {{ amount?.errors?.['min'].min | number }} tSats
+
Maximum is {{ amount?.errors?.['max'].max | number }} tSats
-
+
Address
- - + +
- @if (address?.errors?.['required']) { -
Address is required
- } @else { -
Must be a valid testnet4 address
- } -
-
-
Too many requests! Try again later.
+
Address is required
+
Must be a valid testnet4 address
+
You cannot use this address
- @if (!user) { -
- To limit abuse, please log in or sign up and link your Twitter account to use the faucet. -
- } @else if (!status?.access) { -
- To use this feature, please link your Twitter account. -
- } } -
-
- If you no longer need your testnet4 coins, please consider sending them back to {{ status.address }} to replenish the faucet. -
+ + + @if (status?.address) { +
If you no longer need your testnet4 coins, please consider sending them back to replenish the faucet.
+ } +
-
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/faucet/faucet.component.scss b/frontend/src/app/components/faucet/faucet.component.scss index 084168ca4..d611f5a23 100644 --- a/frontend/src/app/components/faucet/faucet.component.scss +++ b/frontend/src/app/components/faucet/faucet.component.scss @@ -23,6 +23,9 @@ .submit-button, .button-group, .button-group .btn { flex-grow: 1; } + .submit-button:disabled { + pointer-events: none; + } #satoshis::after { content: 'sats'; @@ -41,4 +44,9 @@ width: 100%; max-width: 800px; margin: auto; -} \ No newline at end of file +} + +.invalid { + border-width: 1px; + border-color: var(--red); +} diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 98d7a0c57..bfb485d0e 100644 --- a/frontend/src/app/components/faucet/faucet.component.ts +++ b/frontend/src/app/components/faucet/faucet.component.ts @@ -1,13 +1,13 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, FormGroup, ValidationErrors, Validators } from "@angular/forms"; -import { StorageService } from '../../services/storage.service'; -import { ServicesApiServices } from '../../services/services-api.service'; -import { AudioService } from '../../services/audio.service'; -import { StateService } from '../../services/state.service'; -import { Subscription, tap } from "rxjs"; -import { HttpErrorResponse } from "@angular/common/http"; +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', @@ -15,52 +15,91 @@ import { WebsocketService } from "../../services/websocket.service"; styleUrls: ['./faucet.component.scss'] }) export class FaucetComponent implements OnInit, OnDestroy { - user: any; - loading: boolean = true; + loading = true; + error: string = ''; + user: any = undefined; + txid: string = ''; + + faucetStatusSubscription: Subscription; status: { - address?: string, - access: boolean - min: number, - user_max: number, - user_requests: number, + 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; - error = ''; faucetForm: FormGroup; - txid = ''; mempoolPositionSubscription: Subscription; confirmationSubscription: Subscription; constructor( - private stateService: StateService, + private cd: ChangeDetectorRef, private storageService: StorageService, private servicesApiService: ServicesApiServices, - private websocketService: WebsocketService, - private audioService: AudioService, 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.initForm(5000, 500000); - 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 { + if (!this.user) { 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.mempoolPositionSubscription = this.stateService.mempoolTxPosition$.subscribe(txPosition => { if (txPosition && txPosition.txid === this.txid) { @@ -78,18 +117,22 @@ export class FaucetComponent implements OnInit, OnDestroy { }); } - initForm(min: number, max: number): void { - this.faucetForm = this.formBuilder.group({ - 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4'))]], - 'satoshis': ['', [Validators.required, Validators.min(min), Validators.max(max)]] - }, { validators: (formGroup): ValidationErrors | null => { - if (this.status && !this.status?.user_requests) { - return { customError: 'You have used the faucet too many times already! Come back later.'} - } - return null; - }}); - this.faucetForm.get('satoshis').setValue(min); - this.loading = false; + requestCoins(): void { + 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.cd.markForCheck(); + }), + error: (response: HttpErrorResponse) => { + this.error = response.error; + }, + }); } 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 address() { return this.faucetForm.get('address')!; } 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) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 86c653deb..737f8ab42 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -102,7 +102,7 @@ -