2026-05-24 17:56:57 +00:00
use blockchain ::blocks ::loans ::UnsignedLoanContractTransaction ;
2026-06-27 16:28:13 +00:00
use blockchain ::common ::asset_names ::{
padded_loan_collateral_input_or_base , padded_normalized_asset_input ,
} ;
2026-06-04 15:06:51 +00:00
use blockchain ::common ::cli_prompts ::{ prompt_hidden_nonempty , prompt_visible , prompt_wallet_path } ;
2026-06-23 17:24:35 +00:00
use blockchain ::common ::network_paths_and_settings ::block_extension_and_paths ;
2026-05-24 17:56:57 +00:00
use blockchain ::common ::types ::LENDER_FEE ;
2026-06-04 15:06:51 +00:00
2026-05-24 17:56:57 +00:00
use blockchain ::json ;
2026-06-04 15:06:51 +00:00
use blockchain ::standalone_tools ::vanity_resolver ::resolve_wallet_address_input ;
2026-05-24 17:56:57 +00:00
use blockchain ::wallets ::structures ::Wallet ;
use blockchain ::File ;
use blockchain ::{ create_dir_all , AsyncWriteExt } ;
2026-06-27 16:28:13 +00:00
use blockchain ::{ NaiveDate , NaiveTime , TimeZone , Utc } ;
2026-05-24 17:56:57 +00:00
2026-06-23 17:24:35 +00:00
fn normalize_asset_or_base ( input : & str ) -> Option < String > {
let base_coin = block_extension_and_paths ( ) . 1 ;
if input . trim ( ) . eq_ignore_ascii_case ( base_coin . trim ( ) ) {
Some ( base_coin )
} else {
padded_normalized_asset_input ( input . trim ( ) )
}
2026-05-24 17:56:57 +00:00
}
fn display_fee ( value : u64 ) -> f64 {
// Fees are stored as atomic units and displayed as whole-coin decimals for the CLI.
value as f64 / 100_000_000.0
}
fn parse_start_date ( input : & str ) -> Result < u32 , String > {
2026-06-27 16:28:13 +00:00
// Loan schedules use UTC dates so both parties and every node derive the same due dates.
2026-05-24 17:56:57 +00:00
let date = NaiveDate ::parse_from_str ( input . trim ( ) , " %Y-%m-%d " )
. map_err ( | _ | " Please enter the date in YYYY-MM-DD format. " . to_string ( ) ) ? ;
let datetime = date . and_time ( NaiveTime ::from_hms_opt ( 0 , 0 , 0 ) . unwrap ( ) ) ;
2026-06-27 16:28:13 +00:00
let timestamp = Utc . from_utc_datetime ( & datetime ) . timestamp ( ) ;
2026-05-24 17:56:57 +00:00
if timestamp < 0 | | timestamp > u32 ::MAX as i64 {
return Err ( " Loan start date is out of range. " . to_string ( ) ) ;
}
Ok ( timestamp as u32 )
}
2026-06-04 15:06:51 +00:00
async fn load_signing_wallet ( ) -> Option < Wallet > {
let wallet_path = prompt_wallet_path ( ) . await ;
let decryption_key = prompt_hidden_nonempty (
" What is your wallet decryption key? " ,
" Wallet key cannot be empty. Please try again. " ,
)
. await ;
match Wallet ::try_obtain_wallet ( decryption_key , Some ( & wallet_path ) ) . await {
Ok ( wallet ) = > Some ( wallet ) ,
Err ( err ) = > {
eprintln! ( " Wallet decryption failed: {err} " ) ;
None
}
}
}
2026-05-24 17:56:57 +00:00
#[ tokio::main ]
async fn main ( ) {
// Loan contracts use transaction type 7 and are first signed by the lender.
let txtype = 7 ;
// Coin/token and collateral names are padded to fixed-width transaction fields.
let loan_coin_name = prompt_visible ( " What coin or token will you be lending out? " ) . await ;
2026-06-23 17:24:35 +00:00
let Some ( loan_coin ) = normalize_asset_or_base ( & loan_coin_name ) else {
println! ( " Loan asset must be the base coin or normalize to 3-15 alphanumeric characters. " ) ;
return ;
} ;
2026-05-24 17:56:57 +00:00
let loan_amount_string = prompt_visible ( " How many coins or tokens will you be lending? " ) . await ;
let loan_amount_f64 : f64 = loan_amount_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid amount " ) ;
let loan_amount = ( loan_amount_f64 * 100_000_000.0 ) . round ( ) as u64 ;
// Payment cadence is stored as a compact one-letter code.
let payment_period =
prompt_visible ( " Would you like each payment period to be daily, weekly, or monthly? " )
. await ;
let payment_period = match payment_period . trim ( ) . to_lowercase ( ) . as_str ( ) {
" daily " | " d " = > " d " . to_string ( ) ,
" weekly " | " w " = > " w " . to_string ( ) ,
" monthly " | " m " = > " m " . to_string ( ) ,
_ = > {
println! ( " Payment period must be daily, weekly, or monthly. " ) ;
return ;
}
} ;
let payment_number_string =
prompt_visible ( " How many payments do you expect until the loan is considered paid? " ) . await ;
let payment_number : u8 = payment_number_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid number " ) ;
let payment_amount_string = prompt_visible ( " What should be the amount of each payment? " ) . await ;
let payment_amount_f64 : f64 = payment_amount_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid amount " ) ;
let payment_amount = ( payment_amount_f64 * 100_000_000.0 ) . round ( ) as u64 ;
let grace_period_string =
prompt_visible ( " How many payments can be missed before you will claim the collateral? " )
. await ;
let grace_period : u8 = grace_period_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid number " ) ;
let max_late_value_string = prompt_visible (
" How far behind in value can the borrower be before you will claim the collateral? " ,
)
. await ;
let max_late_value_f64 : f64 = max_late_value_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid amount " ) ;
let max_late_value = ( max_late_value_f64 * 100_000_000.0 ) . round ( ) as u64 ;
// Collateral can be a coin, token, or NFT name; NFT collateral uses amount 1.
let collateral_name =
prompt_visible ( " What collateral coin, token, or NFT will you be accepting for this loan? " )
. await ;
2026-06-27 16:28:13 +00:00
let base_coin = block_extension_and_paths ( ) . 1 ;
let Some ( collateral ) = padded_loan_collateral_input_or_base ( collateral_name . trim ( ) , & base_coin )
else {
println! (
" Collateral must be the base coin, a 3-15 character asset, or a numbered NFT ending in _xxxxx. "
) ;
2026-06-23 17:24:35 +00:00
return ;
} ;
2026-05-24 17:56:57 +00:00
let collateral_amount_string = prompt_visible ( " How many collateral coins or tokens will you be accepting for this loan (enter 1 for NFT collateral)? " ) . await ;
let collateral_amount_f64 : f64 = collateral_amount_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid amount " ) ;
let collateral_amount = ( collateral_amount_f64 * 100_000_000.0 ) . round ( ) as u64 ;
let start_date =
prompt_visible ( " What is the date this loan should begin? (YYYY-MM-DD): " ) . await ;
let timestamp = match parse_start_date ( & start_date ) {
Ok ( timestamp ) = > timestamp ,
Err ( err ) = > {
println! ( " {err} " ) ;
return ;
}
} ;
2026-06-04 15:06:51 +00:00
// Load the lender wallet before resolving vanity inputs; remote vanity lookup needs a signed handshake.
let wallet = match load_signing_wallet ( ) . await {
Some ( wallet ) = > wallet ,
None = > return ,
} ;
2026-05-24 17:56:57 +00:00
// Resolve the borrower input before writing it into the loan offer.
let borrower = prompt_visible ( " What is the wallet address of the borrower? " ) . await ;
2026-06-04 15:06:51 +00:00
let borrower = match resolve_wallet_address_input ( borrower . trim ( ) , & wallet ) . await {
2026-05-24 17:56:57 +00:00
Ok ( address ) = > address ,
2026-06-04 15:06:51 +00:00
Err ( err ) = > {
println! ( " borrower wallet invalid: {err} " ) ;
2026-05-24 17:56:57 +00:00
return ;
}
} ;
let txfee_prompt = format! (
" Enter the txfee you will be paying for this transaction (minimum: {:.8}) " ,
display_fee ( LENDER_FEE )
) ;
let txfee_string = prompt_visible ( & format! ( " {txfee_prompt} : " ) ) . await ;
let txfee_f64 : f64 = txfee_string
. trim ( )
. parse ( )
. expect ( " Please enter a valid fee " ) ;
let txfee = ( txfee_f64 * 100_000_000.0 ) . round ( ) as u64 ;
let private_key = & wallet . saved . private_key ;
let lender = & wallet . saved . short_address ;
// The lender must sign with a valid current-network short address.
if ! Wallet ::short_address_validation ( lender . trim_matches ( '"' ) ) {
println! ( " lender wallet invalid: {lender} " ) ;
return ;
}
// Build the unsigned loan so both lender and borrower sign the exact same hash.
let unsigned_loan = UnsignedLoanContractTransaction ::new (
txtype ,
timestamp ,
& loan_coin ,
loan_amount ,
lender . trim_matches ( '"' ) ,
& collateral ,
collateral_amount ,
& borrower ,
& payment_period ,
payment_number ,
payment_amount ,
grace_period ,
max_late_value ,
txfee ,
)
. await ;
// This first signature is the lender's offer; the borrower adds signature2 later.
let ( hash , signature1 ) = match unsigned_loan . hash_and_sign ( private_key ) . await {
Ok ( value ) = > value ,
Err ( err ) = > {
eprintln! ( " Failed to sign loan contract: {err} " ) ;
return ;
}
} ;
// Save the partially signed loan JSON for the borrower to review and sign.
let output = json! ( {
" txtype " : txtype ,
" timestamp " : timestamp ,
" loan_coin " : loan_coin ,
" loan_amount " : loan_amount ,
" lender " : lender . trim_matches ( '"' ) ,
" collateral " : collateral ,
" collateral_amount " : collateral_amount ,
" borrower " : borrower ,
" payment_period " : payment_period ,
" payment_number " : payment_number ,
" payment_amount " : payment_amount ,
" grace_period " : grace_period ,
" max_late_value " : max_late_value ,
" txfee " : txfee ,
" hash " : hash ,
" signature1 " : signature1 ,
} ) ;
let output_str = serde_json ::to_string_pretty ( & output ) . expect ( " Failed to serialize JSON " ) ;
// Transaction creation tools all write into ./transactions using the transaction hash.
let dir_path = " ./transactions " ;
if let Err ( e ) = create_dir_all ( & dir_path ) . await {
eprintln! ( " Failed to create directory: {e} " ) ;
return ;
}
let file_path = format! (
" {}/{}.json " ,
dir_path ,
output [ " hash " ] . as_str ( ) . unwrap_or_default ( )
) ;
let mut file = File ::create ( & file_path )
. await
. expect ( " Failed to create file " ) ;
if let Err ( e ) = file . write_all ( output_str . as_bytes ( ) ) . await {
eprintln! ( " Failed to write file: {e} " ) ;
return ;
}
println! ( " transaction: {output_str} " ) ;
}