\ 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 @@
-
+
diff --git a/frontend/src/app/components/twitter-login/twitter-login.component.html b/frontend/src/app/components/twitter-login/twitter-login.component.html
new file mode 100644
index 000000000..6ff40bd50
--- /dev/null
+++ b/frontend/src/app/components/twitter-login/twitter-login.component.html
@@ -0,0 +1,6 @@
+
+
+ {{ buttonString }}
+
diff --git a/frontend/src/app/components/twitter-login/twitter-login.component.ts b/frontend/src/app/components/twitter-login/twitter-login.component.ts
new file mode 100644
index 000000000..17583b00e
--- /dev/null
+++ b/frontend/src/app/components/twitter-login/twitter-login.component.ts
@@ -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();
+ @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;
+ }
+}
diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts
index 68a902e56..012d2fa43 100644
--- a/frontend/src/app/master-page.module.ts
+++ b/frontend/src/app/master-page.module.ts
@@ -105,19 +105,21 @@ if (window['__env']?.OFFICIAL_MEMPOOL_SPACE) {
data: { networks: ['bitcoin', 'liquid'] },
component: ServerStatusComponent
});
- routes[0].children.push({
- path: 'faucet',
- canActivate: [(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
- return state.url.startsWith('/testnet4/');
- }],
- component: StartComponent,
- data: { preload: true, networkSpecific: true },
- children: [{
- path: '',
- data: { networks: ['bitcoin'] },
- component: FaucetComponent,
- }]
- })
+ if (window['isMempoolSpaceBuild']) {
+ routes[0].children.push({
+ path: 'faucet',
+ canActivate: [(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
+ return state.url.startsWith('/testnet4/');
+ }],
+ component: StartComponent,
+ data: { preload: true, networkSpecific: true },
+ children: [{
+ path: '',
+ data: { networks: ['bitcoin'] },
+ component: FaucetComponent,
+ }]
+ })
+ }
}
@NgModule({
diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts
index aec4be089..bdc6d18c2 100644
--- a/frontend/src/app/services/services-api.service.ts
+++ b/frontend/src/app/services/services-api.service.ts
@@ -161,7 +161,7 @@ export class ServicesApiServices {
}
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) {
diff --git a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts
index 07b96427d..e60c7c524 100644
--- a/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts
+++ b/frontend/src/app/shared/components/mempool-error/mempool-error.component.ts
@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
const MempoolErrors = {
+ 'bad_request': `Your request was not valid. Please try again.`,
'internal_server_error': `Something went wrong, please try again later`,
'acceleration_duplicated': `This transaction has already been accelerated.`,
'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.`,
'not_whitelisted_by_any_pool': `You are not whitelisted by any mining pool`,
'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 };
export function isMempoolError(error: string) {
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 89d62b375..777ca7180 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -115,6 +115,7 @@ import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe';
import { HttpErrorComponent } from '../shared/components/http-error/http-error.component';
import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.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';
@@ -230,6 +231,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
HttpErrorComponent,
TwitterWidgetComponent,
FaucetComponent,
+ TwitterLogin,
],
imports: [
CommonModule,
@@ -359,6 +361,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
PendingStatsComponent,
HttpErrorComponent,
TwitterWidgetComponent,
+ TwitterLogin,
MempoolBlockOverviewComponent,
ClockchainComponent,