File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -17,7 +17,13 @@ import type { GetTransactionResponse } from "@/gen/bitcoin/bitcoind/v1alpha/bitc
1717import { FaucetService } from "@/gen/faucet/v1/faucet_pb" ;
1818import { timestampToDate } from "@/lib/api" ;
1919import { clientTransport } from "@/lib/client-api" ;
20- import { blockExplorerUrl , exampleAddressForNetwork } from "@/lib/utils" ;
20+ import { dispenseErrorMessage } from "@/lib/errors" ;
21+ import {
22+ blockExplorerUrl ,
23+ exampleAddressForNetwork ,
24+ instanceNetwork ,
25+ isPlausibleAddress ,
26+ } from "@/lib/utils" ;
2127
2228export default function Home ( ) {
2329 const [ address , setAddress ] = useState ( "" ) ;
@@ -35,6 +41,11 @@ export default function Home() {
3541 const handleDispense = async ( ) => {
3642 if ( ! address || ! amount ) return ;
3743
44+ if ( ! isPlausibleAddress ( address ) ) {
45+ setError ( `That doesn't look like a valid ${ instanceNetwork ( ) } address.` ) ;
46+ return ;
47+ }
48+
3849 setLoading ( true ) ;
3950 setError ( null ) ;
4051 setTxid ( null ) ;
@@ -45,7 +56,7 @@ export default function Home() {
4556 setTxid ( response . txid ) ;
4657 fetchClaims ( ) ; // Refresh claims list
4758 } catch ( err ) {
48- setError ( `Failed to dispense coins: ${ err } ` ) ;
59+ setError ( dispenseErrorMessage ( err ) ) ;
4960 } finally {
5061 setLoading ( false ) ;
5162 }
Original file line number Diff line number Diff line change 1+ import { Code , ConnectError } from "@connectrpc/connect" ;
2+ import { instanceNetwork } from "@/lib/utils" ;
3+
4+ // Turns the backend's raw dispense errors into something a user can act on.
5+ // The Bitcoin Core node stays the source of truth, this just makes its
6+ // messages readable instead of dumping "[internal] dispense coins: -5: ...".
7+ export function dispenseErrorMessage ( err : unknown ) : string {
8+ const connectErr = ConnectError . from ( err ) ;
9+ const raw = connectErr . rawMessage . toLowerCase ( ) ;
10+
11+ if ( raw . includes ( "address" ) || raw . includes ( "checksum" ) ) {
12+ return `That doesn't look like a valid ${ instanceNetwork ( ) } address. Double-check it and try again.` ;
13+ }
14+ if ( raw . includes ( "amount" ) ) {
15+ return "Amount must be greater than 0 and at most 5 BTC." ;
16+ }
17+ if ( connectErr . code === Code . ResourceExhausted || raw . includes ( "insufficient" ) ) {
18+ return "The faucet is temporarily out of funds or rate-limited. Please try again later." ;
19+ }
20+ return "Could not dispense coins. Please try again." ;
21+ }
Original file line number Diff line number Diff line change @@ -14,6 +14,17 @@ export function instanceNetwork(): string {
1414 return network ;
1515}
1616
17+ // Loose sanity check to catch empty or obviously typo'd input before we hit
18+ // the node. Intentionally does not verify the checksum or network, the faucet's
19+ // Bitcoin Core node is the source of truth, so this stays permissive to avoid
20+ // rejecting a valid address.
21+ export function isPlausibleAddress ( address : string ) : boolean {
22+ const addr = address . trim ( ) ;
23+ const bech32 = / ^ ( b c | t b | b c r t ) 1 [ a - z 0 - 9 ] { 6 , 87 } $ / i;
24+ const base58 = / ^ [ 1 2 3 m n ] [ 1 - 9 A - H J - N P - Z a - k m - z ] { 24 , 38 } $ / ;
25+ return bech32 . test ( addr ) || base58 . test ( addr ) ;
26+ }
27+
1728export function exampleAddressForNetwork ( ) : string {
1829 switch ( instanceNetwork ( ) ) {
1930 case "signet" :
You can’t perform that action at this time.
0 commit comments