99
1010import java .io .BufferedReader ;
1111import java .io .InputStreamReader ;
12+ import java .math .BigDecimal ;
13+ import java .time .Duration ;
14+ import java .util .ArrayList ;
15+ import java .util .List ;
1216import java .util .stream .Collectors ;
1317
1418/**
15- * BitcoinBase — wraps a local bitcoind instance.
19+ * BitcoinBase — controlled wrapper around a local bitcoind instance.
1620 *
17- * RPC config mirrors /bitcoin/bash/btc24-query.sh:
18- * port 2222
19- * authentication: Bitcoin Core cookie authentication (no password in source)
20- * network regtest
21- * wallet "United States"
22- *
23- * All mutating operations (start, stop, load/unload wallet, send) persist a
24- * trade/action record to the MySQL N21 instance via db.N21Store.storeBitcoinTrade().
25- *
26- * @author Max Rupplin
27- * @date June 08 2026
21+ * RPC authentication is delegated to Bitcoin Core cookie authentication.
22+ * The server never accepts arbitrary bitcoin-cli method names and never places
23+ * RPC credentials in source code or command arguments.
2824 */
2925public class BitcoinBase
3026{
3127 protected String hash = "0xDA717018470E213F" ;
32-
3328 protected NitroWebExpress .Aspect ASPECT ;
3429
35- // ── RPC constants (from btc24-query.sh) ──────────────────────────────────
36- protected static final String BITCOIN_CLI = "bitcoin-cli" ;
37- protected static final String BITCOIND = "bitcoind" ;
38- protected static final String RPC_PORT = "2222" ;
39- protected static final String NETWORK = "-regtest" ;
40- protected static final String WALLET_NAME = "United States" ;
41-
42- // ── Shared RPC flag array (prepended to every bitcoin-cli call) ───────────
43- private static final String [] RPC_FLAGS = {
44- NETWORK ,
45- "-rpcport=" + RPC_PORT ,
46- };
30+ protected static final String BITCOIN_CLI = "bitcoin-cli" ;
31+ protected static final String BITCOIND = "bitcoind" ;
32+ protected static final String RPC_PORT = configuredRpcPort ();
33+ protected static final String NETWORK = "-regtest" ;
34+ protected static final String WALLET_NAME = "United States" ;
35+ private static final Duration RPC_TIMEOUT = Duration .ofSeconds (30 );
4736
4837 protected MessageOrderer bitcoin_message_orderer = new MessageOrderer (this );
4938
5039 public BitcoinBase (final NitroWebExpress .Aspect ASPECT )
5140 {
5241 this .ASPECT = ASPECT ;
53-
54- BitcoinAsiaAndTokyoDate JAPANDate = new BitcoinAsiaAndTokyoDate ();
55- BitcoinAmericaAndNewYorkDate ESTDate = new BitcoinAmericaAndNewYorkDate ();
42+ BitcoinAsiaAndTokyoDate JAPANDate = new BitcoinAsiaAndTokyoDate ();
43+ BitcoinAmericaAndNewYorkDate ESTDate = new BitcoinAmericaAndNewYorkDate ();
5644
5745 CommonRails .printSystemComponent (this , this .hashCode (),
5846 ". WebExpress Bitcoin >> opens in North Carolina on Date " + ESTDate .EST_Time + " . " );
@@ -62,154 +50,138 @@ public BitcoinBase(final NitroWebExpress.Aspect ASPECT)
6250 database .N21Store .createBitcoinTradesTable ();
6351 }
6452
65- // ── Daemon lifecycle ──────────────────────────────────────────────────────
66-
67- /** Start local bitcoind in regtest+daemon mode. */
6853 public String start_bitcoind ()
6954 {
70- String result = exec (new String []{ BITCOIND , NETWORK , "-daemon" ,
71- "-rpcport=" + RPC_PORT ,
72- });
55+ String result = exec (List .of (BITCOIND , NETWORK , "-daemon" , "-rpcport=" + RPC_PORT ), false );
7356 database .N21Store .storeBitcoinTrade ("start_bitcoind" , "" , "" , result );
7457 return result ;
7558 }
7659
77- /** Stop local bitcoind via RPC stop. */
7860 public String stop_bitcoind ()
7961 {
8062 String result = cli ("stop" );
8163 database .N21Store .storeBitcoinTrade ("stop_bitcoind" , "" , "" , result );
8264 return result ;
8365 }
8466
85- // ── Wallet management ─────────────────────────────────────────────────────
86-
8767 public String load_wallet ()
8868 {
69+ BitcoinRpcPolicy .requireWalletName (WALLET_NAME );
8970 String result = cli ("loadwallet" , WALLET_NAME );
9071 database .N21Store .storeBitcoinTrade ("load_wallet" , WALLET_NAME , "" , result );
9172 return result ;
9273 }
9374
9475 public String unload_wallet ()
9576 {
77+ BitcoinRpcPolicy .requireWalletName (WALLET_NAME );
9678 String result = cli ("unloadwallet" , WALLET_NAME );
9779 database .N21Store .storeBitcoinTrade ("unload_wallet" , WALLET_NAME , "" , result );
9880 return result ;
9981 }
10082
10183 public String create_wallet (final String name )
10284 {
85+ BitcoinRpcPolicy .requireWalletName (name );
10386 String result = cli ("createwallet" , name );
10487 database .N21Store .storeBitcoinTrade ("create_wallet" , name , "" , result );
10588 return result ;
10689 }
10790
108- /** Returns raw JSON from getwalletinfo for the default wallet. */
109- public String get_wallet_info ()
110- {
111- return walletCli ("getwalletinfo" );
112- }
113-
114- /** Returns raw balance string for the default wallet. */
115- public String get_balance ()
116- {
117- return walletCli ("getbalance" );
118- }
119-
120- /** Returns a new address for the default wallet. */
121- public String get_new_address ()
122- {
123- return walletCli ("getnewaddress" );
124- }
125-
126- // ── Node status ───────────────────────────────────────────────────────────
127-
128- public String get_blockchain_info ()
129- {
130- return cli ("getblockchaininfo" );
131- }
132-
133- public String get_block_count ()
134- {
135- return cli ("getblockcount" );
136- }
137-
138- // ── Trade / send ──────────────────────────────────────────────────────────
91+ public String get_wallet_info () { return walletCli ("getwalletinfo" ); }
92+ public String get_balance () { return walletCli ("getbalance" ); }
93+ public String get_new_address () { return walletCli ("getnewaddress" ); }
94+ public String get_blockchain_info () { return cli ("getblockchaininfo" ); }
95+ public String get_block_count () { return cli ("getblockcount" ); }
13996
14097 /**
141- * Send BTC from the default wallet to a destination address.
142- * Records the trade to MySQL regardless of outcome.
143- *
144- * @param toAddress destination Bitcoin address
145- * @param amount amount in BTC (e.g. "0.001")
146- * @return txid on success, error string on failure
98+ * Broadcast a transaction only after strict address and amount validation.
99+ * Bitcoin Core remains the authority for transaction acceptance.
147100 */
148101 public String send (final String toAddress , final String amount )
149102 {
150- String result = walletCli ("sendtoaddress" , toAddress , amount );
151- database .N21Store .storeBitcoinTrade ("send" , WALLET_NAME , toAddress + " " + amount + " BTC" , result );
103+ BitcoinRpcPolicy .requireAddress (toAddress );
104+ long satoshis = BitcoinRpcPolicy .requireSatoshis (amount );
105+ String normalizedAmount = BigDecimal .valueOf (satoshis , 8 ).toPlainString ();
106+ String result = walletCli ("sendtoaddress" , toAddress , normalizedAmount );
107+ database .N21Store .storeBitcoinTrade ("send" , WALLET_NAME ,
108+ "address=" + toAddress + " satoshis=" + satoshis , result );
152109 return result ;
153110 }
154111
155- // ── Message pass-through ──────────────────────────────────────────────────
156-
157112 public void send_message (final StringBuffer BUFFER ) {}
158- public void send_message (final String MESSAGE ) {}
113+ public void send_message (final String MESSAGE ) {}
159114
160- // ── Process helpers ───────────────────────────────────────────────────────
161-
162- /**
163- * Run bitcoin-cli with the shared RPC flags, no wallet suffix.
164- * Additional args are appended after the RPC flags.
165- */
166115 protected String cli (final String ... args )
167116 {
168- String [] cmd = buildCmd (false , args );
169- return exec (cmd );
117+ if (args .length == 0 ) throw new IllegalArgumentException ("Bitcoin RPC method is required" );
118+ BitcoinRpcPolicy .requireAllowed (args [0 ]);
119+ return exec (buildCmd (false , args ), true );
170120 }
171121
172- /**
173- * Run bitcoin-cli with -rpcwallet=WALLET_NAME prepended to args.
174- */
175122 protected String walletCli (final String ... args )
176123 {
177- String [] cmd = buildCmd (true , args );
178- return exec (cmd );
124+ if (args .length == 0 ) throw new IllegalArgumentException ("Bitcoin RPC method is required" );
125+ BitcoinRpcPolicy .requireAllowed (args [0 ]);
126+ return exec (buildCmd (true , args ), true );
179127 }
180128
181- private String [] buildCmd (final boolean withWallet , final String ... args )
129+ private List < String > buildCmd (final boolean withWallet , final String ... args )
182130 {
183- int base = 1 + RPC_FLAGS .length + (withWallet ? 1 : 0 );
184- String [] cmd = new String [base + args .length ];
185- cmd [0 ] = BITCOIN_CLI ;
186- System .arraycopy (RPC_FLAGS , 0 , cmd , 1 , RPC_FLAGS .length );
187- int off = 1 + RPC_FLAGS .length ;
188- if (withWallet ) { cmd [off ] = "-rpcwallet=" + WALLET_NAME ; off ++; }
189- System .arraycopy (args , 0 , cmd , off , args .length );
131+ List <String > cmd = new ArrayList <>();
132+ cmd .add (BITCOIN_CLI );
133+ cmd .add (NETWORK );
134+ cmd .add ("-rpcport=" + RPC_PORT );
135+ if (withWallet ) cmd .add ("-rpcwallet=" + WALLET_NAME );
136+ for (String arg : args ) cmd .add (arg );
190137 return cmd ;
191138 }
192139
193- /** Execute a command, capture stdout+stderr, return combined output. */
194- private String exec (final String [] cmd )
140+ private String exec (final List <String > cmd , final boolean rpc )
195141 {
196142 try
197143 {
198- Process p = Runtime .getRuntime ().exec (cmd );
199- String out = new BufferedReader (new InputStreamReader (p .getInputStream ()))
200- .lines ().collect (Collectors .joining ("\n " ));
201- String err = new BufferedReader (new InputStreamReader (p .getErrorStream ()))
202- .lines ().collect (Collectors .joining ("\n " ));
203- p .waitFor ();
204- String result = out .isBlank () ? err : out ;
144+ ProcessBuilder builder = new ProcessBuilder (cmd );
145+ builder .redirectErrorStream (true );
146+ Process process = builder .start ();
147+
148+ String result ;
149+ try (BufferedReader reader = new BufferedReader (new InputStreamReader (process .getInputStream ())))
150+ {
151+ result = reader .lines ().collect (Collectors .joining ("\n " ));
152+ }
153+
154+ if (!process .waitFor (RPC_TIMEOUT .toMillis (), java .util .concurrent .TimeUnit .MILLISECONDS ))
155+ {
156+ process .destroyForcibly ();
157+ return "ERROR: Bitcoin process timed out" ;
158+ }
159+
160+ int exit = process .exitValue ();
161+ String method = cmd .size () > 3 ? cmd .get (cmd .size () - (rpc ? Math .min (1 , cmd .size () - 1 ) : 1 )) : "process" ;
205162 CommonRails .printSystemComponent (this , this .hashCode (),
206- ". BitcoinBase >> " + cmd [ 0 ] + " " + (cmd .length > 1 ? cmd [ cmd . length - 1 ] : "" ) + " >> exit=" + p . exitValue ( ) + " ." );
207- return result ;
163+ ". BitcoinBase >> controlled RPC invocation exit= " + exit + " method= " + (cmd .size () > 3 ? cmd . get ( 3 ) : method ) + " ." );
164+ return result == null ? "" : result ;
208165 }
209166 catch (Exception e )
210167 {
211168 ExceptionHandler .dispatch (e );
212- return "ERROR: " + e .getMessage ();
169+ return "ERROR: Bitcoin RPC operation failed" ;
170+ }
171+ }
172+
173+ private static String configuredRpcPort ()
174+ {
175+ String value = System .getenv ().getOrDefault ("BITCOIN_RPC_PORT" , "2222" );
176+ try
177+ {
178+ int port = Integer .parseInt (value );
179+ if (port < 1 || port > 65535 ) throw new NumberFormatException ();
180+ return Integer .toString (port );
181+ }
182+ catch (NumberFormatException e )
183+ {
184+ return "2222" ;
213185 }
214186 }
215187}
0 commit comments