Skip to content

Commit f14bfa4

Browse files
committed
faucet: validate address and show readable dispense errors
1 parent 51f4185 commit f14bfa4

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

faucet/app/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ import type { GetTransactionResponse } from "@/gen/bitcoin/bitcoind/v1alpha/bitc
1717
import { FaucetService } from "@/gen/faucet/v1/faucet_pb";
1818
import { timestampToDate } from "@/lib/api";
1919
import { 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

2228
export 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
}

faucet/lib/errors.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}

faucet/lib/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff 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 = /^(bc|tb|bcrt)1[a-z0-9]{6,87}$/i;
24+
const base58 = /^[123mn][1-9A-HJ-NP-Za-km-z]{24,38}$/;
25+
return bech32.test(addr) || base58.test(addr);
26+
}
27+
1728
export function exampleAddressForNetwork(): string {
1829
switch (instanceNetwork()) {
1930
case "signet":

0 commit comments

Comments
 (0)