33import connections .Connection ;
44import national .NationalFinanceID ;
55
6- import java .sql .*;
7- import java .util .ArrayList ;
8- import java .util .List ;
6+ import java .math .BigDecimal ;
7+ import java .math .RoundingMode ;
8+ import java .sql .PreparedStatement ;
9+ import java .sql .ResultSet ;
10+ import java .sql .Statement ;
911
1012/**
11- * BitcoinWalletSession — handles telnet commands for browsing/selecting/trading BTC wallets.
13+ * BitcoinWalletSession — handles telnet commands for browsing/selecting BTC wallets.
1214 *
1315 * Commands:
1416 * bitcoin — show available versions (24-30)
15- * bitcoin <version> — list wallets for that version
16- * set wallet.name <name> — select wallet for session (persists to DB)
17+ * bitcoin <version> — list wallet metadata for that version
18+ * set wallet.name <name> — select wallet for session
1719 * unset wallet.name — deselect wallet
18- * trade btc <amount> — trade BTC from selected wallet (recorded in trades table)
20+ * trade btc <amount> — record a trade event; DOES NOT submit a blockchain transaction
21+ * show wallet — show the selected wallet
1922 *
20- * Original wallet data in bitcoin_wallets_v{N} is NEVER modified .
21- * Trades are recorded in bitcoin_trades_v{N} tables .
23+ * Financial balances are never inferred from wallet filenames or file sizes .
24+ * Bitcoin Core RPC is the authoritative source for an actual wallet balance .
2225 */
2326public class BitcoinWalletSession
2427{
25- private static final String AUTHOR = "Max Ruppln - Clear 21 Branch US Military" ;
28+ private static final int MIN_VERSION = 24 ;
29+ private static final int MAX_VERSION = 30 ;
30+ private static final long SATOSHIS_PER_BTC = 100_000_000L ;
31+ private static final int MAX_TRADE_SCALE = 8 ;
2632
2733 /** Handle a bitcoin-related command. Returns response string. */
2834 public static String handle (String cmd , Connection conn , NationalFinanceID nfid )
2935 {
36+ if (cmd == null ) return null ;
3037 String lower = cmd .trim ().toLowerCase ();
3138
3239 if (lower .equals ("bitcoin" ))
@@ -42,12 +49,12 @@ else if (lower.startsWith("trade btc "))
4249 else if (lower .equals ("show wallet" ))
4350 return showWallet (conn );
4451
45- return null ; // not a bitcoin command
52+ return null ;
4653 }
4754
48- /** Check if input is a bitcoin command. */
4955 public static boolean isBitcoinCommand (String cmd )
5056 {
57+ if (cmd == null ) return false ;
5158 String l = cmd .trim ().toLowerCase ();
5259 return l .equals ("bitcoin" ) || l .startsWith ("bitcoin " ) ||
5360 l .startsWith ("set wallet.name " ) || l .equals ("unset wallet.name" ) ||
@@ -63,75 +70,78 @@ private static String listVersions(Connection conn)
6370 java .sql .Connection db = database .N21DataSource .get ();
6471 if (db == null ) return " [DB unavailable]" ;
6572
66- for (int v = 24 ; v <= 30 ; v ++)
73+ for (int v = MIN_VERSION ; v <= MAX_VERSION ; v ++)
6774 {
68- Statement st = db .createStatement ();
69- ResultSet rs = st .executeQuery ("SELECT COUNT(*) as c, IFNULL(SUM(btc_value),0) as btc FROM bitcoin_wallets_v" + v );
70- if (rs .next ())
71- sb .append (" v" ).append (v ).append (" — " ).append (rs .getInt ("c" )).append (" wallets, " ).append (rs .getLong ("btc" )).append (" BTC\r \n " );
72- rs .close (); st .close ();
75+ String table = walletTable (v );
76+ try (Statement st = db .createStatement ();
77+ ResultSet rs = st .executeQuery ("SELECT COUNT(*) AS c FROM " + table ))
78+ {
79+ if (rs .next ())
80+ sb .append (" v" ).append (v ).append (" — " ).append (rs .getInt ("c" )).append (" wallet records\r \n " );
81+ }
7382 }
7483 sb .append ("\r \n Usage: bitcoin <version> (e.g. bitcoin 24)" );
7584 if (conn .btcWallet != null )
7685 sb .append ("\r \n Active wallet: " ).append (conn .btcWallet ).append (" (v" ).append (conn .btcVersion ).append (")" );
7786 }
78- catch (Exception e ) { return " [Error querying wallets ]" ; }
87+ catch (Exception e ) { return " [Error querying wallet metadata ]" ; }
7988 return sb .toString ();
8089 }
8190
8291 private static String listWallets (String versionStr , Connection conn )
8392 {
84- int version ;
93+ final int version ;
8594 try { version = Integer .parseInt (versionStr ); }
8695 catch (NumberFormatException e ) { return " Usage: bitcoin <24|25|26|27|28|29|30>" ; }
87- if (version < 24 || version > 30 ) return " Invalid version. Use 24–30." ;
96+ if (! validVersion ( version ) ) return " Invalid version. Use 24–30." ;
8897
8998 StringBuilder sb = new StringBuilder ();
90- sb .append ("\r \n Wallets — v" ).append (version ).append ("\r \n ─────────────────────────\r \n " );
99+ sb .append ("\r \n Wallet Metadata — v" ).append (version ).append ("\r \n ─────────────────────────\r \n " );
91100 try
92101 {
93102 java .sql .Connection db = database .N21DataSource .get ();
94- Statement st = db .createStatement ();
95- ResultSet rs = st .executeQuery (
96- "SELECT wallet_name, file_size_bytes, btc_value, usd_value FROM bitcoin_wallets_v" + version +
97- " ORDER BY btc_value DESC LIMIT 25" );
98- int i = 1 ;
99- while (rs .next ())
103+ if (db == null ) return " [DB unavailable]" ;
104+ String table = walletTable (version );
105+ String query = "SELECT wallet_name, file_size_bytes FROM " + table + " ORDER BY wallet_name LIMIT 25" ;
106+ try (Statement st = db .createStatement (); ResultSet rs = st .executeQuery (query ))
100107 {
101- sb .append (String .format (" %2d. %-30s %,12d bytes %,8d BTC\r \n " ,
102- i ++, rs .getString ("wallet_name" ), rs .getLong ("file_size_bytes" ), rs .getLong ("btc_value" )));
108+ int i = 1 ;
109+ while (rs .next ())
110+ {
111+ sb .append (String .format (" %2d. %-30s %,12d bytes\r \n " ,
112+ i ++, rs .getString ("wallet_name" ), rs .getLong ("file_size_bytes" )));
113+ }
103114 }
104- rs .close (); st .close ();
115+ sb .append ("\r \n NOTE: file size is metadata, not a BTC balance." );
116+ sb .append ("\r \n Use authenticated Bitcoin Core RPC for authoritative balances." );
105117 sb .append ("\r \n Use: set wallet.name <name> to select a wallet." );
106-
107- // Remember version selection in session
108118 conn .btcVersion = version ;
109119 }
110- catch (Exception e ) { return " [Error listing wallets ]" ; }
120+ catch (Exception e ) { return " [Error listing wallet metadata ]" ; }
111121 return sb .toString ();
112122 }
113123
114124 private static String setWallet (String name , Connection conn , NationalFinanceID nfid )
115125 {
116126 if (conn .btcVersion == 0 ) return " Select a version first: bitcoin <24-30>" ;
117- if (name .isEmpty ()) return " Usage: set wallet.name <wallet_name>" ;
127+ if (name .isEmpty () || name . length () > 512 ) return " Usage: set wallet.name <wallet_name>" ;
118128
119- // Verify wallet exists
120129 try
121130 {
122131 java .sql .Connection db = database .N21DataSource .get ();
123- PreparedStatement ps = db .prepareStatement (
124- "SELECT wallet_name FROM bitcoin_wallets_v" + conn .btcVersion + " WHERE wallet_name = ?" );
125- ps .setString (1 , name );
126- ResultSet rs = ps .executeQuery ();
127- if (!rs .next ()) { rs .close (); ps .close (); return " Wallet '" + name + "' not found in v" + conn .btcVersion + "." ; }
128- rs .close (); ps .close ();
132+ if (db == null ) return " [DB unavailable]" ;
133+ String table = walletTable (conn .btcVersion );
134+ try (PreparedStatement ps = db .prepareStatement ("SELECT wallet_name FROM " + table + " WHERE wallet_name = ?" ))
135+ {
136+ ps .setString (1 , name );
137+ try (ResultSet rs = ps .executeQuery ())
138+ {
139+ if (!rs .next ()) return " Wallet '" + name + "' not found in v" + conn .btcVersion + "." ;
140+ }
141+ }
129142
130143 conn .btcWallet = name ;
131-
132- // Persist session to DB
133144 saveSession (nfid .nationalId , conn .btcVersion , name );
134-
135145 return " ✔ Wallet set: " + name + " (v" + conn .btcVersion + ")" ;
136146 }
137147 catch (Exception e ) { return " [Error setting wallet]" ; }
@@ -151,49 +161,107 @@ private static String showWallet(Connection conn)
151161 return " Active wallet: " + conn .btcWallet + " (v" + conn .btcVersion + ")" ;
152162 }
153163
164+ /**
165+ * Records a trade event only. This method never broadcasts or submits a transaction.
166+ * Amounts are stored as exact satoshis and optional fiat valuation is operator supplied.
167+ */
154168 private static String tradeBtc (String amountStr , Connection conn , NationalFinanceID nfid )
155169 {
156170 if (conn .btcWallet == null ) return " No wallet selected. Use: set wallet.name <name>" ;
171+ if (amountStr .isEmpty ()) return " Usage: trade btc <amount>" ;
157172
158- long amount ;
159- try { amount = Long .parseLong (amountStr ); }
160- catch (NumberFormatException e ) { return " Usage: trade btc <amount>" ; }
161- if (amount <= 0 ) return " Amount must be positive." ;
173+ final long satoshis ;
174+ try
175+ {
176+ BigDecimal btc = new BigDecimal (amountStr ).setScale (MAX_TRADE_SCALE , RoundingMode .UNNECESSARY );
177+ if (btc .signum () <= 0 ) return " Amount must be positive." ;
178+ BigDecimal satoshiDecimal = btc .movePointRight (MAX_TRADE_SCALE );
179+ if (satoshiDecimal .compareTo (BigDecimal .valueOf (Long .MAX_VALUE )) > 0 )
180+ return " Amount is too large." ;
181+ satoshis = satoshiDecimal .longValueExact ();
182+ if (satoshis <= 0 ) return " Amount must be positive." ;
183+ }
184+ catch (ArithmeticException | NumberFormatException e )
185+ {
186+ return " Amount must be a positive BTC decimal with at most 8 decimal places." ;
187+ }
162188
163189 try
164190 {
165191 java .sql .Connection db = database .N21DataSource .get ();
192+ if (db == null ) return " [DB unavailable]" ;
193+
194+ String table = tradeTable (conn .btcVersion );
195+ try (Statement st = db .createStatement ())
196+ {
197+ st .executeUpdate (
198+ "CREATE TABLE IF NOT EXISTS " + table + " (" +
199+ " id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," +
200+ " national_id BIGINT UNSIGNED NOT NULL," +
201+ " wallet_name VARCHAR(512) NOT NULL," +
202+ " amount_satoshis BIGINT UNSIGNED NOT NULL," +
203+ " btc_price_usd DECIMAL(38,8) NULL," +
204+ " usd_value DECIMAL(38,8) NULL," +
205+ " trade_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," +
206+ " event_state VARCHAR(32) NOT NULL DEFAULT 'RECORDED'," +
207+ " author VARCHAR(256) NOT NULL" +
208+ ") ENGINE=InnoDB" );
209+ }
210+
211+ BigDecimal price = readOptionalPrice ();
212+ BigDecimal usd = price == null ? null : BigDecimal .valueOf (satoshis )
213+ .divide (BigDecimal .valueOf (SATOSHIS_PER_BTC ), 8 , RoundingMode .HALF_UP )
214+ .multiply (price ).setScale (8 , RoundingMode .HALF_UP );
215+
216+ String sql = "INSERT INTO " + table +
217+ " (national_id, wallet_name, amount_satoshis, btc_price_usd, usd_value, event_state, author) VALUES (?,?,?,?,?,?,?)" ;
218+ try (PreparedStatement ps = db .prepareStatement (sql ))
219+ {
220+ ps .setLong (1 , nfid .nationalId );
221+ ps .setString (2 , conn .btcWallet );
222+ ps .setLong (3 , satoshis );
223+ if (price == null ) ps .setNull (4 , java .sql .Types .DECIMAL ); else ps .setBigDecimal (4 , price );
224+ if (usd == null ) ps .setNull (5 , java .sql .Types .DECIMAL ); else ps .setBigDecimal (5 , usd );
225+ ps .setString (6 , "RECORDED" );
226+ ps .setString (7 , "JWSTF BitcoinWalletSession" );
227+ ps .executeUpdate ();
228+ }
229+
230+ String btc = BigDecimal .valueOf (satoshis ).movePointLeft (MAX_TRADE_SCALE ).stripTrailingZeros ().toPlainString ();
231+ return price == null
232+ ? " ✔ Trade event recorded: " + btc + " BTC (" + satoshis + " satoshis). No blockchain transaction was submitted."
233+ : " ✔ Trade event recorded: " + btc + " BTC (" + satoshis + " satoshis), valuation $" + usd .toPlainString () + ". No blockchain transaction was submitted." ;
234+ }
235+ catch (Exception e ) { return " [Error recording trade event]" ; }
236+ }
166237
167- // Create trades table if not exists
168- String tradesTable = "bitcoin_trades_v" + conn .btcVersion ;
169- Statement st = db .createStatement ();
170- st .executeUpdate (
171- "CREATE TABLE IF NOT EXISTS " + tradesTable + " (" +
172- " id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY," +
173- " national_id BIGINT UNSIGNED NOT NULL," +
174- " wallet_name VARCHAR(512) NOT NULL," +
175- " btc_amount BIGINT NOT NULL," +
176- " usd_value DOUBLE NOT NULL," +
177- " trade_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP," +
178- " author VARCHAR(256) NOT NULL" +
179- ") ENGINE=InnoDB" );
180- st .close ();
181-
182- double usd = amount * 20_000_000_000_000.0 ;
183-
184- PreparedStatement ps = db .prepareStatement (
185- "INSERT INTO " + tradesTable + " (national_id, wallet_name, btc_amount, usd_value, author) VALUES (?,?,?,?,?)" );
186- ps .setLong (1 , nfid .nationalId );
187- ps .setString (2 , conn .btcWallet );
188- ps .setLong (3 , amount );
189- ps .setDouble (4 , usd );
190- ps .setString (5 , AUTHOR );
191- ps .executeUpdate ();
192- ps .close ();
193-
194- return " ✔ Trade recorded: " + amount + " BTC from " + conn .btcWallet + " (v" + conn .btcVersion + ") = $" + String .format ("%.2e" , usd ) + " USD" ;
238+ private static BigDecimal readOptionalPrice ()
239+ {
240+ String value = System .getenv ("BTC_PRICE_USD" );
241+ if (value == null || value .trim ().isEmpty ()) return null ;
242+ try
243+ {
244+ BigDecimal price = new BigDecimal (value .trim ()).setScale (8 , RoundingMode .HALF_UP );
245+ return price .signum () >= 0 ? price : null ;
195246 }
196- catch (Exception e ) { return " [Error recording trade: " + e .getMessage () + "]" ; }
247+ catch (NumberFormatException e ) { return null ; }
248+ }
249+
250+ private static boolean validVersion (int version )
251+ {
252+ return version >= MIN_VERSION && version <= MAX_VERSION ;
253+ }
254+
255+ private static String walletTable (int version )
256+ {
257+ if (!validVersion (version )) throw new IllegalArgumentException ("Unsupported Bitcoin version" );
258+ return "bitcoin_wallets_v" + version ;
259+ }
260+
261+ private static String tradeTable (int version )
262+ {
263+ if (!validVersion (version )) throw new IllegalArgumentException ("Unsupported Bitcoin version" );
264+ return "bitcoin_trade_events_v" + version ;
197265 }
198266
199267 private static void saveSession (long nationalId , int version , String wallet )
@@ -202,25 +270,25 @@ private static void saveSession(long nationalId, int version, String wallet)
202270 {
203271 java .sql .Connection db = database .N21DataSource .get ();
204272 if (db == null ) return ;
205-
206- Statement st = db .createStatement ();
207- st .executeUpdate (
208- "CREATE TABLE IF NOT EXISTS bitcoin_wallet_sessions (" +
209- " national_id BIGINT UNSIGNED PRIMARY KEY," +
210- " btc_version INT NOT NULL," +
211- " wallet_name VARCHAR(512) NOT NULL," +
212- " updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
213- ") ENGINE=InnoDB" );
214- st .close ();
215-
216- PreparedStatement ps = db .prepareStatement (
273+ try (Statement st = db .createStatement ())
274+ {
275+ st .executeUpdate (
276+ "CREATE TABLE IF NOT EXISTS bitcoin_wallet_sessions (" +
277+ " national_id BIGINT UNSIGNED PRIMARY KEY," +
278+ " btc_version INT NOT NULL," +
279+ " wallet_name VARCHAR(512) NOT NULL," +
280+ " updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
281+ ") ENGINE=InnoDB" );
282+ }
283+ try (PreparedStatement ps = db .prepareStatement (
217284 "INSERT INTO bitcoin_wallet_sessions (national_id, btc_version, wallet_name) VALUES (?,?,?) " +
218- "ON DUPLICATE KEY UPDATE btc_version=VALUES(btc_version), wallet_name=VALUES(wallet_name)" );
219- ps .setLong (1 , nationalId );
220- ps .setInt (2 , version );
221- ps .setString (3 , wallet );
222- ps .executeUpdate ();
223- ps .close ();
285+ "ON DUPLICATE KEY UPDATE btc_version=VALUES(btc_version), wallet_name=VALUES(wallet_name)" ))
286+ {
287+ ps .setLong (1 , nationalId );
288+ ps .setInt (2 , version );
289+ ps .setString (3 , wallet );
290+ ps .executeUpdate ();
291+ }
224292 }
225293 catch (Exception ignored ) {}
226294 }
@@ -231,10 +299,11 @@ private static void clearSession(long nationalId)
231299 {
232300 java .sql .Connection db = database .N21DataSource .get ();
233301 if (db == null ) return ;
234- PreparedStatement ps = db .prepareStatement ("DELETE FROM bitcoin_wallet_sessions WHERE national_id=?" );
235- ps .setLong (1 , nationalId );
236- ps .executeUpdate ();
237- ps .close ();
302+ try (PreparedStatement ps = db .prepareStatement ("DELETE FROM bitcoin_wallet_sessions WHERE national_id=?" ))
303+ {
304+ ps .setLong (1 , nationalId );
305+ ps .executeUpdate ();
306+ }
238307 }
239308 catch (Exception ignored ) {}
240309 }
0 commit comments