Skip to content

Commit e189842

Browse files
committed
Bitcoin: add centralized RPC and monetary validation policy
1 parent c1ed7bd commit e189842

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package bitcoin.base;
2+
3+
import java.math.BigDecimal;
4+
import java.math.RoundingMode;
5+
import java.util.Set;
6+
import java.util.regex.Pattern;
7+
8+
/**
9+
* Central validation policy for Bitcoin RPC operations exposed by the server.
10+
*
11+
* The policy intentionally permits only the RPC methods used by the application
12+
* and validates monetary inputs before they reach bitcoin-cli.
13+
*/
14+
public final class BitcoinRpcPolicy
15+
{
16+
private static final Set<String> ALLOWED_RPC_METHODS = Set.of(
17+
"stop",
18+
"loadwallet",
19+
"unloadwallet",
20+
"createwallet",
21+
"getwalletinfo",
22+
"getbalance",
23+
"getnewaddress",
24+
"getblockchaininfo",
25+
"getblockcount",
26+
"sendtoaddress"
27+
);
28+
29+
private static final Pattern WALLET_NAME = Pattern.compile("[A-Za-z0-9._ -]{1,128}");
30+
private static final Pattern BITCOIN_ADDRESS = Pattern.compile("(?:[123][1-9A-HJ-NP-Za-km-z]{25,90}|bc1[ac-hj-np-z02-9]{11,87}|tb1[ac-hj-np-z02-9]{11,87}|bcrt1[ac-hj-np-z02-9]{11,87})");
31+
32+
private BitcoinRpcPolicy() {}
33+
34+
public static boolean isAllowed(final String method)
35+
{
36+
return method != null && ALLOWED_RPC_METHODS.contains(method);
37+
}
38+
39+
public static void requireAllowed(final String method)
40+
{
41+
if (!isAllowed(method))
42+
throw new IllegalArgumentException("Bitcoin RPC method is not permitted: " + method);
43+
}
44+
45+
public static String requireWalletName(final String name)
46+
{
47+
if (name == null || !WALLET_NAME.matcher(name).matches())
48+
throw new IllegalArgumentException("Invalid Bitcoin wallet name");
49+
return name;
50+
}
51+
52+
public static String requireAddress(final String address)
53+
{
54+
if (address == null || !BITCOIN_ADDRESS.matcher(address).matches())
55+
throw new IllegalArgumentException("Invalid Bitcoin address format");
56+
return address;
57+
}
58+
59+
/** Parse BTC without floating-point conversion and convert exactly to satoshis. */
60+
public static long requireSatoshis(final String amount)
61+
{
62+
if (amount == null || amount.isBlank())
63+
throw new IllegalArgumentException("BTC amount is required");
64+
65+
final BigDecimal value;
66+
try
67+
{
68+
value = new BigDecimal(amount.trim());
69+
}
70+
catch (NumberFormatException e)
71+
{
72+
throw new IllegalArgumentException("Invalid BTC amount", e);
73+
}
74+
75+
if (value.signum() <= 0 || value.scale() > 8)
76+
throw new IllegalArgumentException("BTC amount must be positive and use at most 8 decimal places");
77+
78+
final BigDecimal satoshis = value.movePointRight(8).setScale(0, RoundingMode.UNNECESSARY);
79+
if (satoshis.compareTo(BigDecimal.valueOf(Long.MAX_VALUE)) > 0)
80+
throw new IllegalArgumentException("BTC amount is too large");
81+
return satoshis.longValueExact();
82+
}
83+
}

0 commit comments

Comments
 (0)