@@ -12,7 +12,7 @@ use std::path::PathBuf;
1212
1313use clap:: { CommandFactory , Parser , Subcommand } ;
1414use clap_complete:: { generate, Shell } ;
15- use hex_conservative:: DisplayHex ;
15+ use hex_conservative:: { DisplayHex , FromHex } ;
1616use ldk_server_client:: client:: LdkServerClient ;
1717use ldk_server_client:: config:: {
1818 get_default_config_path, load_config, resolve_api_key, resolve_base_url, resolve_cert_path,
@@ -45,8 +45,8 @@ use ldk_server_client::ldk_server_grpc::api::{
4545 UpdateChannelConfigResponse , VerifySignatureRequest , VerifySignatureResponse ,
4646} ;
4747use ldk_server_client:: ldk_server_grpc:: types:: {
48- bolt11_invoice_description, Bolt11InvoiceDescription , ChannelConfig , PageToken ,
49- RouteParametersConfig ,
48+ bolt11_invoice_description, Bolt11InvoiceDescription , ChannelConfig , CustomTlvRecord ,
49+ PageToken , RouteParametersConfig ,
5050} ;
5151use ldk_server_client:: {
5252 DEFAULT_EXPIRY_SECS , DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF , DEFAULT_MAX_PATH_COUNT ,
@@ -314,6 +314,12 @@ enum Commands {
314314 help = "Maximum share of a channel's total capacity to send over a channel, as a power of 1/2 (default: 2)"
315315 ) ]
316316 max_channel_saturation_power_of_half : Option < u32 > ,
317+ #[ arg(
318+ long = "custom-tlv" ,
319+ value_parser = parse_custom_tlv,
320+ help = "Custom TLV record to attach, format: <type_num>:<hex_value>. Repeatable. type_num must be >= 65536."
321+ ) ]
322+ custom_tlvs : Vec < ( u64 , Vec < u8 > ) > ,
317323 } ,
318324 #[ command(
319325 about = "Pay a BIP 21 URI, BIP 353 Human-Readable Name, BOLT11 invoice, or BOLT12 offer"
@@ -810,6 +816,7 @@ async fn main() {
810816 max_total_cltv_expiry_delta,
811817 max_path_count,
812818 max_channel_saturation_power_of_half,
819+ custom_tlvs,
813820 } => {
814821 let amount_msat = amount. to_msat ( ) ;
815822 let max_total_routing_fee_msat = max_total_routing_fee. map ( |a| a. to_msat ( ) ) ;
@@ -822,12 +829,18 @@ async fn main() {
822829 . unwrap_or ( DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF ) ,
823830 } ;
824831
832+ let proto_custom_tlvs: Vec < _ > = custom_tlvs
833+ . into_iter ( )
834+ . map ( |( type_num, value) | CustomTlvRecord { type_num, value : value. into ( ) } )
835+ . collect ( ) ;
836+
825837 handle_response_result :: < _ , SpontaneousSendResponse > (
826838 client
827839 . spontaneous_send ( SpontaneousSendRequest {
828840 amount_msat,
829841 node_id,
830842 route_parameters : Some ( route_parameters) ,
843+ custom_tlvs : proto_custom_tlvs,
831844 } )
832845 . await ,
833846 ) ;
@@ -1249,6 +1262,19 @@ fn parse_page_token(token_str: &str) -> Result<PageToken, LdkServerError> {
12491262 Ok ( PageToken { token : parts[ 0 ] . to_string ( ) , index } )
12501263}
12511264
1265+ fn parse_custom_tlv ( s : & str ) -> Result < ( u64 , Vec < u8 > ) , String > {
1266+ let ( type_str, hex_str) =
1267+ s. split_once ( ':' ) . ok_or_else ( || format ! ( "expected <type_num>:<hex_value>, got '{s}'" ) ) ?;
1268+ let type_num: u64 =
1269+ type_str. parse ( ) . map_err ( |e| format ! ( "invalid type number '{type_str}': {e}" ) ) ?;
1270+ if type_num < 65536 {
1271+ return Err ( format ! ( "type number must be >= 65536, got {type_num}" ) ) ;
1272+ }
1273+ let value =
1274+ Vec :: < u8 > :: from_hex ( hex_str) . map_err ( |e| format ! ( "invalid hex value '{hex_str}': {e}" ) ) ?;
1275+ Ok ( ( type_num, value) )
1276+ }
1277+
12521278fn handle_error_msg ( msg : String ) -> ! {
12531279 eprintln ! ( "Error: {}" , sanitize_for_terminal( msg) ) ;
12541280 std:: process:: exit ( 1 ) ;
@@ -1265,3 +1291,33 @@ fn handle_error(e: LdkServerError) -> ! {
12651291 eprintln ! ( "Error ({}): {}" , error_type, e. message) ;
12661292 std:: process:: exit ( 1 ) ; // Exit with status code 1 on error.
12671293}
1294+
1295+ #[ cfg( test) ]
1296+ mod tests {
1297+ use super :: * ;
1298+
1299+ #[ test]
1300+ fn parse_custom_tlv_accepts_valid_record ( ) {
1301+ let ( type_num, value) = parse_custom_tlv ( "65537:deadbeef" ) . unwrap ( ) ;
1302+ assert_eq ! ( type_num, 65537 ) ;
1303+ assert_eq ! ( value, vec![ 0xde , 0xad , 0xbe , 0xef ] ) ;
1304+ }
1305+
1306+ #[ test]
1307+ fn parse_custom_tlv_rejects_missing_separator ( ) {
1308+ let err = parse_custom_tlv ( "65537" ) . unwrap_err ( ) ;
1309+ assert ! ( err. contains( "expected <type_num>:<hex_value>" ) ) ;
1310+ }
1311+
1312+ #[ test]
1313+ fn parse_custom_tlv_rejects_reserved_type ( ) {
1314+ let err = parse_custom_tlv ( "65535:00" ) . unwrap_err ( ) ;
1315+ assert ! ( err. contains( "type number must be >= 65536" ) ) ;
1316+ }
1317+
1318+ #[ test]
1319+ fn parse_custom_tlv_rejects_invalid_hex ( ) {
1320+ let err = parse_custom_tlv ( "65537:not-hex" ) . unwrap_err ( ) ;
1321+ assert ! ( err. contains( "invalid hex value" ) ) ;
1322+ }
1323+ }
0 commit comments