Skip to content

Commit 413d9c8

Browse files
committed
Harden Bitcoin wallet session accounting and trade recording
1 parent 5b078a6 commit 413d9c8

1 file changed

Lines changed: 169 additions & 100 deletions

File tree

source/bitcoin/module/BitcoinWalletSession.java

Lines changed: 169 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,37 @@
33
import connections.Connection;
44
import 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
*/
2326
public 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

Comments
 (0)