Skip to content

Commit 6d79010

Browse files
committed
Completed transfer command (finished basic CLI functionality)
1 parent d27ac97 commit 6d79010

File tree

6 files changed

+222
-60
lines changed

6 files changed

+222
-60
lines changed

crates/cli-client/src/cli/basic.rs

Lines changed: 206 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ impl Cli {
3434
.next()
3535
.and_then(|r| match r {
3636
coin_store::UtxoQueryResult::Found(entries, _) => entries.into_iter().next(),
37-
coin_store::UtxoQueryResult::InsufficientValue(_, _) | coin_store::UtxoQueryResult::Empty => {
37+
coin_store::UtxoQueryResult::InsufficientValue(_, _) => {
38+
eprintln!("No single UTXO large enough. Try using 'merge' command first.");
3839
None
3940
}
41+
coin_store::UtxoQueryResult::Empty => None,
4042
})
4143
.ok_or_else(|| Error::Config("No native UTXO found".to_string()))?;
4244

@@ -105,9 +107,11 @@ impl Cli {
105107
.next()
106108
.and_then(|r| match r {
107109
coin_store::UtxoQueryResult::Found(entries, _) => Some(entries),
108-
coin_store::UtxoQueryResult::InsufficientValue(_, _) | coin_store::UtxoQueryResult::Empty => {
109-
None
110+
coin_store::UtxoQueryResult::InsufficientValue(entries, _) => {
111+
eprintln!("Only found {} UTXOs for merge.", entries.len());
112+
Some(entries)
110113
}
114+
coin_store::UtxoQueryResult::Empty => None,
111115
})
112116
.ok_or_else(|| Error::Config(format!("No UTXOs found for asset {target_asset}")))?;
113117

@@ -163,7 +167,14 @@ impl Cli {
163167
.next()
164168
.and_then(|r| match r {
165169
coin_store::UtxoQueryResult::Found(entries, _) => entries.into_iter().next(),
166-
_ => None,
170+
coin_store::UtxoQueryResult::InsufficientValue(entries, _) => {
171+
let available: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
172+
eprintln!(
173+
"Insufficient LBTC for fee: have {available} sats, need {fee} sats. Try using 'merge' command first."
174+
);
175+
None
176+
}
177+
coin_store::UtxoQueryResult::Empty => None,
167178
})
168179
.ok_or_else(|| Error::Config(format!("No LBTC UTXO found to pay fee of {fee} sats")))?;
169180

@@ -234,22 +245,171 @@ impl Cli {
234245
}
235246
}
236247
}
237-
BasicCommand::TransferNative {
238-
to: _,
239-
amount: _,
240-
fee: _,
241-
broadcast: _,
242-
} => {
243-
todo!()
244-
}
245-
BasicCommand::TransferAsset {
246-
asset_id: _,
247-
to: _,
248-
amount: _,
249-
fee: _,
250-
broadcast: _,
248+
BasicCommand::Transfer {
249+
asset_id,
250+
to,
251+
amount,
252+
fee,
253+
broadcast,
251254
} => {
252-
todo!()
255+
let wallet = self.get_wallet(&config).await?;
256+
let script_pubkey = wallet.signer().p2pk_address(config.address_params())?.script_pubkey();
257+
258+
let target_asset = asset_id.unwrap_or(*LIQUID_TESTNET_BITCOIN_ASSET);
259+
let is_native = target_asset == *LIQUID_TESTNET_BITCOIN_ASSET;
260+
261+
let required_amount = if is_native { *amount + *fee } else { *amount };
262+
263+
let asset_filter = coin_store::UtxoFilter::new()
264+
.asset_id(target_asset)
265+
.script_pubkey(script_pubkey.clone())
266+
.required_value(required_amount);
267+
268+
let results: Vec<coin_store::UtxoQueryResult> =
269+
<_ as UtxoStore>::query_utxos(wallet.store(), &[asset_filter]).await?;
270+
271+
let entries: Vec<_> = results
272+
.into_iter()
273+
.next()
274+
.and_then(|r| match r {
275+
coin_store::UtxoQueryResult::Found(entries, _) => Some(entries),
276+
coin_store::UtxoQueryResult::InsufficientValue(entries, _) => {
277+
let available: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
278+
eprintln!(
279+
"Insufficient funds: have {available} sats, need {required_amount} sats. Try using 'merge' command first."
280+
);
281+
None
282+
}
283+
coin_store::UtxoQueryResult::Empty => None,
284+
})
285+
.ok_or_else(|| Error::Config(format!("No UTXOs found for asset {target_asset}")))?;
286+
287+
let total_asset_value: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
288+
let mut pst = PartiallySignedTransaction::new_v2();
289+
290+
let mut utxos: Vec<TxOut> = entries
291+
.iter()
292+
.map(|e| {
293+
let mut input = Input::from_prevout(*e.outpoint());
294+
input.witness_utxo = Some(e.txout().clone());
295+
pst.add_input(input);
296+
e.txout().clone()
297+
})
298+
.collect();
299+
300+
if is_native {
301+
pst.add_output(Output::new_explicit(
302+
to.script_pubkey(),
303+
*amount,
304+
*LIQUID_TESTNET_BITCOIN_ASSET,
305+
None,
306+
));
307+
308+
let change = total_asset_value
309+
.checked_sub(*amount + *fee)
310+
.ok_or_else(|| Error::Config("Fee + amount exceeds total UTXO value".to_string()))?;
311+
312+
if change > 0 {
313+
pst.add_output(Output::new_explicit(
314+
script_pubkey,
315+
change,
316+
*LIQUID_TESTNET_BITCOIN_ASSET,
317+
None,
318+
));
319+
}
320+
321+
println!("Transferring {amount} sats LBTC to {to}");
322+
} else {
323+
let fee_filter = coin_store::UtxoFilter::new()
324+
.asset_id(*LIQUID_TESTNET_BITCOIN_ASSET)
325+
.script_pubkey(script_pubkey.clone())
326+
.required_value(*fee);
327+
328+
let fee_results: Vec<coin_store::UtxoQueryResult> =
329+
<_ as UtxoStore>::query_utxos(wallet.store(), &[fee_filter]).await?;
330+
331+
let fee_entry = fee_results
332+
.into_iter()
333+
.next()
334+
.and_then(|r| match r {
335+
coin_store::UtxoQueryResult::Found(entries, _) => entries.into_iter().next(),
336+
coin_store::UtxoQueryResult::InsufficientValue(entries, _) => {
337+
let available: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
338+
eprintln!(
339+
"Insufficient LBTC for fee: have {available} sats, need {fee} sats. Try using 'merge' command first."
340+
);
341+
None
342+
}
343+
coin_store::UtxoQueryResult::Empty => None,
344+
})
345+
.ok_or_else(|| Error::Config(format!("No LBTC UTXO found to pay fee of {fee} sats")))?;
346+
347+
let Some(fee_input_value) = fee_entry.value() else {
348+
return Err(Error::Config("Unexpected confidential value".to_string()));
349+
};
350+
351+
let mut fee_input = Input::from_prevout(*fee_entry.outpoint());
352+
fee_input.witness_utxo = Some(fee_entry.txout().clone());
353+
pst.add_input(fee_input);
354+
utxos.push(fee_entry.txout().clone());
355+
356+
pst.add_output(Output::new_explicit(to.script_pubkey(), *amount, target_asset, None));
357+
358+
let asset_change = total_asset_value - *amount;
359+
if asset_change > 0 {
360+
pst.add_output(Output::new_explicit(
361+
script_pubkey.clone(),
362+
asset_change,
363+
target_asset,
364+
None,
365+
));
366+
}
367+
368+
if fee_input_value > *fee {
369+
pst.add_output(Output::new_explicit(
370+
script_pubkey,
371+
fee_input_value - *fee,
372+
*LIQUID_TESTNET_BITCOIN_ASSET,
373+
None,
374+
));
375+
}
376+
377+
println!("Transferring {amount} units of asset {target_asset} to {to}");
378+
}
379+
380+
pst.add_output(Output::from_txout(TxOut::new_fee(*fee, *LIQUID_TESTNET_BITCOIN_ASSET)));
381+
382+
let mut tx = pst.extract_tx()?;
383+
384+
for (i, _) in utxos.iter().enumerate() {
385+
let signature =
386+
wallet
387+
.signer()
388+
.sign_p2pk(&tx, &utxos, i, config.address_params(), *LIQUID_TESTNET_GENESIS)?;
389+
390+
tx = finalize_p2pk_transaction(
391+
tx,
392+
&utxos,
393+
&wallet.signer().public_key(),
394+
&signature,
395+
i,
396+
config.address_params(),
397+
*LIQUID_TESTNET_GENESIS,
398+
)?;
399+
}
400+
401+
match broadcast {
402+
false => {
403+
println!("{}", tx.serialize().to_lower_hex_string());
404+
}
405+
true => {
406+
cli_helper::explorer::broadcast_tx(&tx).await?;
407+
408+
println!("Broadcasted: {}", tx.txid());
409+
410+
wallet.store().insert_transaction(&tx, HashMap::default()).await?;
411+
}
412+
}
253413
}
254414
BasicCommand::IssueAsset { amount, fee, broadcast } => {
255415
let wallet = self.get_wallet(&config).await?;
@@ -267,7 +427,14 @@ impl Cli {
267427
.next()
268428
.and_then(|r| match r {
269429
coin_store::UtxoQueryResult::Found(entries, _) => entries.into_iter().next(),
270-
_ => None,
430+
coin_store::UtxoQueryResult::InsufficientValue(entries, _) => {
431+
let available: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
432+
eprintln!(
433+
"Insufficient LBTC for fee: have {available} sats, need {fee} sats. Try using 'merge' command first."
434+
);
435+
None
436+
}
437+
coin_store::UtxoQueryResult::Empty => None,
271438
})
272439
.ok_or_else(|| Error::Config(format!("No LBTC UTXO found to pay fee of {fee} sats")))?;
273440

@@ -345,8 +512,9 @@ impl Cli {
345512
.into_iter()
346513
.next()
347514
.and_then(|r| match r {
348-
coin_store::UtxoQueryResult::Found(entries, _) => entries.into_iter().next(),
349-
_ => None,
515+
coin_store::UtxoQueryResult::Found(entries, _)
516+
| coin_store::UtxoQueryResult::InsufficientValue(entries, _) => entries.into_iter().next(),
517+
coin_store::UtxoQueryResult::Empty => None,
350518
})
351519
.ok_or_else(|| Error::Config(format!("No UTXO found for asset {asset_id}")))?;
352520

@@ -374,19 +542,29 @@ impl Cli {
374542

375543
let results = <_ as UtxoStore>::query_utxos(wallet.store(), &[token_filter, fee_filter]).await?;
376544

377-
let UtxoQueryResult::Found(entries, _) = &results[0] else {
378-
return Err(Error::Config(format!("No reissuance token UTXO found for {token_id}")));
545+
let token_entry = match &results[0] {
546+
UtxoQueryResult::Found(entries, _) => &entries[0],
547+
UtxoQueryResult::InsufficientValue(entries, _) if !entries.is_empty() => &entries[0],
548+
_ => return Err(Error::Config(format!("No reissuance token UTXO found for {token_id}"))),
379549
};
380-
let token_entry = &entries[0];
381550

382551
let token_secrets = token_entry
383552
.secrets()
384553
.ok_or_else(|| Error::Config("Reissuance token must be confidential".to_string()))?;
385554

386-
let UtxoQueryResult::Found(entries, _) = &results[1] else {
387-
return Err(Error::Config(format!("No fee UTXO found for {token_id}")));
555+
let fee_entry = match &results[1] {
556+
UtxoQueryResult::Found(entries, _) => &entries[0],
557+
UtxoQueryResult::InsufficientValue(entries, _) => {
558+
let available: u64 = entries.iter().filter_map(coin_store::UtxoEntry::value).sum();
559+
eprintln!(
560+
"Insufficient LBTC for fee: have {available} sats, need {fee} sats. Try using 'merge' command first."
561+
);
562+
return Err(Error::Config(format!("No LBTC UTXO found to pay fee of {fee} sats")));
563+
}
564+
UtxoQueryResult::Empty => {
565+
return Err(Error::Config(format!("No LBTC UTXO found to pay fee of {fee} sats")));
566+
}
388567
};
389-
let fee_entry = &entries[0];
390568

391569
let token_utxo = (*token_entry.outpoint(), token_entry.txout().clone());
392570
let fee_utxo = (*fee_entry.outpoint(), fee_entry.txout().clone());

crates/cli-client/src/cli/commands.rs

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,15 @@ pub enum HelperCommand {
9999

100100
#[derive(Debug, Subcommand)]
101101
pub enum BasicCommand {
102-
/// Transfer LBTC to a recipient
103-
TransferNative {
102+
/// Transfer an asset to a recipient
103+
Transfer {
104+
/// Asset ID (defaults to native LBTC if not specified)
105+
#[arg(long)]
106+
asset_id: Option<AssetId>,
104107
/// Recipient address
105108
#[arg(long)]
106109
to: Address,
107-
/// Amount to send in satoshis
110+
/// Amount to send
108111
#[arg(long)]
109112
amount: u64,
110113
/// Fee amount in satoshis
@@ -144,25 +147,6 @@ pub enum BasicCommand {
144147
broadcast: bool,
145148
},
146149

147-
/// Transfer an asset to a recipient
148-
TransferAsset {
149-
/// Asset id
150-
#[arg(long)]
151-
asset_id: AssetId,
152-
/// Recipient address
153-
#[arg(long)]
154-
to: Address,
155-
/// Amount to send
156-
#[arg(long)]
157-
amount: u64,
158-
/// Fee amount in satoshis
159-
#[arg(long)]
160-
fee: u64,
161-
/// Broadcast transaction
162-
#[arg(long)]
163-
broadcast: bool,
164-
},
165-
166150
/// Issue a new asset
167151
IssueAsset {
168152
/// Amount to issue

crates/cli-client/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(dead_code)]
2+
13
use std::path::{Path, PathBuf};
24
use std::time::Duration;
35

crates/cli-client/src/wallet.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(dead_code)]
2+
13
use std::path::Path;
24

35
use coin_store::Store;

crates/coin-store/src/executor.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,14 +515,13 @@ mod tests {
515515
use std::fs;
516516

517517
use contracts::bytes32_tr_storage::{
518-
get_bytes32_tr_compiled_program, taproot_spend_info, unspendable_internal_key,
519-
BYTES32_TR_STORAGE_SOURCE,
518+
BYTES32_TR_STORAGE_SOURCE, get_bytes32_tr_compiled_program, taproot_spend_info, unspendable_internal_key,
520519
};
521520
use contracts::sdk::taproot_pubkey_gen::TaprootPubkeyGen;
522521
use simplicityhl::elements::confidential::{Asset, Nonce, Value};
523522
use simplicityhl::elements::{AddressParams, AssetId, Script, TxOutWitness};
524-
use simplicityhl::simplicity::bitcoin::key::Parity;
525523
use simplicityhl::simplicity::bitcoin::PublicKey;
524+
use simplicityhl::simplicity::bitcoin::key::Parity;
526525

527526
fn make_explicit_txout(asset_id: AssetId, value: u64) -> TxOut {
528527
TxOut {
@@ -552,7 +551,7 @@ mod tests {
552551
);
553552

554553
let seed = vec![42u8; 32];
555-
let xonly = simplicityhl::elements::schnorr::XOnlyPublicKey::from(spend_info.internal_key());
554+
let xonly = spend_info.internal_key();
556555
let pubkey = PublicKey::from(xonly.public_key(Parity::Even));
557556

558557
TaprootPubkeyGen { seed, pubkey, address }
@@ -781,9 +780,7 @@ mod tests {
781780
.await;
782781
assert!(result.is_ok());
783782

784-
let result = store
785-
.add_contract(BYTES32_TR_STORAGE_SOURCE, arguments, tpg2)
786-
.await;
783+
let result = store.add_contract(BYTES32_TR_STORAGE_SOURCE, arguments, tpg2).await;
787784
assert!(result.is_ok());
788785

789786
let _ = fs::remove_file(path);
@@ -811,8 +808,7 @@ mod tests {
811808

812809
store.insert(outpoint, txout, None).await.unwrap();
813810

814-
let program =
815-
simplicityhl::CompiledProgram::new(BYTES32_TR_STORAGE_SOURCE, arguments, false).unwrap();
811+
let program = simplicityhl::CompiledProgram::new(BYTES32_TR_STORAGE_SOURCE, arguments, false).unwrap();
816812
let cmr = program.commit().cmr();
817813

818814
let filter = UtxoFilter::new().cmr(cmr);

0 commit comments

Comments
 (0)