@@ -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 ( ) ) ;
0 commit comments