-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcli.rs
More file actions
314 lines (294 loc) · 12.7 KB
/
Copy pathcli.rs
File metadata and controls
314 lines (294 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
use std::path::PathBuf;
use bitcoin::hex::FromHex;
use bitcoin::secp256k1::{rand::rngs::OsRng, Secp256k1, SecretKey, XOnlyPublicKey};
use clap::Parser;
use kernel_node::ext::DirnameExt;
use kernel_node::server_capnp::server;
use tokio::net::UnixStream;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
use wallet::io::FileExt;
use wallet::silentpayments::{SilentPaymentKeysFile, SpendKey};
const DEFAULT_DATA_DIR: &str = "~/.kernel-node/";
#[derive(clap::Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[command(flatten)]
opts: Opts,
#[command(subcommand)]
commands: Commands,
}
#[derive(Debug, Clone, clap::Args)]
struct Opts {
/// Path to the data directory.
#[arg(long, short)]
datadir: Option<String>,
}
#[derive(Debug, Clone, clap::Subcommand)]
enum Commands {
/// Echo a message to yourself.
Echo(Echo),
/// Terminate the server.
Stop,
/// Chain commands.
#[command(subcommand)]
Chain(ChainCmd),
/// Wallet commands.
#[command(subcommand)]
Wallet(WalletCmd),
}
#[derive(Debug, Clone, clap::Subcommand)]
enum ChainCmd {
/// Show the current chain tip.
Tip,
}
#[derive(Debug, Clone, clap::Args)]
struct Echo {
/// The message to echo.
message: String,
}
#[derive(Debug, Clone, clap::Subcommand)]
enum WalletCmd {
/// Generate fresh scan and spend keys for receiving silent payments.
///
/// By default, prints the scan key, spend private key, and spend
/// x-only public key as hex on stdout. With `--out <path>`, writes
/// the secrets to a binary file and prints only the spend public
/// key on stderr. WARNING: anyone with the scan key and spend
/// private key can spend received funds.
GenerateKeys {
/// Write the keys to this binary file. Must not already exist.
#[arg(long)]
out: Option<PathBuf>,
},
/// Import BIP-352 silent payment keys to enable scanning for incoming payments.
///
/// Both keys are hex-encoded. The scan key derives the ECDH shared secret with
/// each transaction; the spend pubkey identifies the receiver.
ImportKeys {
/// 32-byte scan private key as hex.
scan_key: String,
/// 32-byte x-only spend public key as hex.
spend_key: String,
},
/// Print scan_key, spend_priv and spend_pub keys to stdout from file.
PrintKeysFromKeysFile { path: PathBuf },
/// Show wallet balance.
Balance,
/// Show wallet transaction history.
History,
/// Show the silent payment address the wallet is scanning for.
Receive,
/// Broadcast a raw transaction to the network.
BroadcastRawTx {
/// Hex-encoded raw transaction.
tx: String,
},
/// Send to a silent payment address or a bitcoin address.
SendToAddress {
/// The recipient silent payment address or bitcoin address.
address: String,
/// Amount to send, in satoshis.
amount_sat: u64,
/// Fee rate, in satoshis per virtual byte.
fee_rate_sat_per_vb: f64,
/// The maximum feerate in sats/vB at which transaction building may
/// use more inputs than strictly necessary so that the wallet's UTXO
/// pool can be reduced (default: 10 sats/vB). Long term fee rate,
/// in satoshis per virtual byte.
///
/// Setting the consolidate fee rate helps the coin-selection
/// algorithm know if to use more UTXOs or less when building a
/// transaction. That is, if the current fee rate is high
/// (fee rate > consolidate fee rate), then consume less inputs making
/// the transaction cheaper. Likewise, if the current fee rate is low,
/// use more inputs and consolidate the UTXO set to be fewer.
consolidate_fee_rate_sat_per_vb: Option<f64>,
},
}
fn generate_keys() -> (SecretKey, SecretKey, XOnlyPublicKey) {
let secp = Secp256k1::new();
let scan_priv = SecretKey::new(&mut OsRng);
let spend_priv = SecretKey::new(&mut OsRng);
let (spend_xonly, _) = spend_priv.public_key(&secp).x_only_public_key();
(scan_priv, spend_priv, spend_xonly)
}
async fn connect_server(datadir_path: &str) -> server::Client {
let sock_file = datadir_path.to_owned() + "/node.sock";
let stream = UnixStream::connect(&sock_file)
.await
.expect("Could not connect to node.sock. Is `node` running?");
let (reader, writer) = stream.into_split();
let buf_reader = futures::io::BufReader::new(reader.compat());
let buf_writer = futures::io::BufWriter::new(writer.compat_write());
let network = capnp_rpc::twoparty::VatNetwork::new(
buf_reader,
buf_writer,
capnp_rpc::rpc_twoparty_capnp::Side::Client,
Default::default(),
);
let mut rpc_system = capnp_rpc::RpcSystem::new(Box::new(network), None);
let client: server::Client = rpc_system.bootstrap(capnp_rpc::rpc_twoparty_capnp::Side::Server);
tokio::task::spawn_local(rpc_system);
client
}
fn main() {
let cli = Args::parse();
if let Commands::Wallet(WalletCmd::GenerateKeys { out }) = &cli.commands {
let (scan_priv, spend_priv, spend_pub) = generate_keys();
match out {
Some(path) => {
let file = SilentPaymentKeysFile::new(scan_priv, SpendKey::Secret(spend_priv));
file.save(path).expect("failed to write keys file");
println!("Wrote silent payment keys to {}", path.display());
println!("spend_pub={}", spend_pub);
}
None => {
eprintln!("WARNING: scan_key and spend_priv must be kept secret — anyone with them can spend received funds.");
println!("scan_key={}", scan_priv.display_secret());
println!("spend_priv={}", spend_priv.display_secret());
println!("spend_pub={}", spend_pub);
}
}
return;
}
if let Commands::Wallet(WalletCmd::PrintKeysFromKeysFile { path }) = &cli.commands {
let read = SilentPaymentKeysFile::load(path)
.expect("file path provided should be readable as a silent payments keys file");
let spend_pub = read.spend_xonly();
eprintln!("spend_pub={}", spend_pub);
let scan_priv = read.scan_key();
eprintln!("scan_key={}", scan_priv.display_secret());
return;
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let datadir_path = cli.opts.datadir.unwrap_or(DEFAULT_DATA_DIR.data_dir());
rt.block_on(tokio::task::LocalSet::new().run_until(async move {
let client = connect_server(&datadir_path).await;
match cli.commands {
Commands::Echo(echo) => {
let mut echo_req = client.echo_request();
println!("Sending... {}", echo.message);
echo_req.get().set_msg(echo.message);
let result = echo_req.send().promise.await.unwrap();
let result = result
.get()
.unwrap()
.get_reply()
.unwrap()
.to_string()
.unwrap();
println!("{result}");
}
Commands::Stop => {
let shutdown_req = client.shutdown_request();
shutdown_req.send().promise.await.unwrap();
println!("Kernel node stopping...");
}
Commands::Chain(cmd) => {
let chain_response = client.make_chain_request().send().promise.await.unwrap();
let client = chain_response.get().unwrap().get_chain().unwrap();
match cmd {
ChainCmd::Tip => {
let req = client.get_tip_request();
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let height = r.get_height();
let hash = r.get_hash().unwrap().to_string().unwrap();
println!("height={height} hash={hash}");
}
}
}
Commands::Wallet(cmd) => {
let wallet_response = client.make_wallet_request().send().promise.await.unwrap();
let client = wallet_response.get().unwrap().get_wallet().unwrap();
match cmd {
WalletCmd::GenerateKeys { .. } => unreachable!("handled before runtime"),
WalletCmd::PrintKeysFromKeysFile { .. } => {
unreachable!("handled before runtime")
}
WalletCmd::ImportKeys {
scan_key,
spend_key,
} => {
let scan_bytes =
Vec::<u8>::from_hex(&scan_key).expect("scan_key must be valid hex");
let spend_bytes =
Vec::<u8>::from_hex(&spend_key).expect("spend_key must be valid hex");
let mut req = client.import_keys_request();
req.get().set_scan_key(&scan_bytes);
req.get().set_spend_key(&spend_bytes);
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let msg = r.get_message().unwrap().to_string().unwrap();
println!("{}", msg);
}
WalletCmd::Balance => {
let req = client.get_balance_request();
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
println!(
"Balance: {} sats | scan height: {} | UTXOs: {}",
r.get_sats(),
r.get_scan_height(),
r.get_utxo_count(),
);
}
WalletCmd::History => {
let req = client.get_history_request();
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let entries = r.get_entries().unwrap().to_string().unwrap();
if entries.is_empty() {
println!("No history yet.");
} else {
println!("{}", entries);
}
}
WalletCmd::Receive => {
let req = client.receive_request();
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let address = r.get_address().unwrap().to_string().unwrap();
println!("{}", address);
}
WalletCmd::BroadcastRawTx { tx } => {
let raw_bytes = Vec::<u8>::from_hex(&tx).expect("tx must be valid hex");
let mut req = client.broadcast_raw_tx_request();
req.get().set_tx(&raw_bytes);
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let txid = r.get_txid().unwrap().to_string().unwrap();
println!("{}", txid);
}
WalletCmd::SendToAddress {
address,
amount_sat,
fee_rate_sat_per_vb,
consolidate_fee_rate_sat_per_vb,
} => {
let mut req = client.send_to_address_request();
req.get().set_address(&address);
req.get().set_amount_sat(amount_sat);
req.get().set_fee_rate_sat_per_vb(fee_rate_sat_per_vb);
if let Some(consolidate_fee_rate) = consolidate_fee_rate_sat_per_vb {
req.get()
.set_consolidate_fee_rate_sat_per_vb(consolidate_fee_rate);
}
let result = req.send().promise.await.unwrap();
let r = result.get().unwrap();
let message = r.get_message().unwrap().to_string().unwrap();
if r.get_ok() {
println!("{}", message);
} else {
eprintln!("{}", message);
std::process::exit(1);
}
}
}
}
}
}))
}