2026-05-24 17:56:57 +00:00
|
|
|
use blockchain::blocks::transfer::{TransferTransaction, UnsignedTransferTransaction};
|
2026-06-23 17:24:35 +00:00
|
|
|
use blockchain::common::asset_names::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-05-24 17:56:57 +00:00
|
|
|
use blockchain::common::network_paths_and_settings::block_extension_and_paths;
|
2026-06-04 15:06:51 +00:00
|
|
|
|
2026-05-24 17:56:57 +00:00
|
|
|
use blockchain::common::types::{NON_BASE_TRANSFER_MIN_FEE, TRANSFER_FEE};
|
|
|
|
|
use blockchain::env;
|
|
|
|
|
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::Utc;
|
|
|
|
|
use blockchain::{create_dir_all, AsyncWriteExt};
|
|
|
|
|
|
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 {
|
|
|
|
|
value as f64 / 100_000_000.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() {
|
|
|
|
|
let minimum_transfer_fee_percent = TRANSFER_FEE * 100.0;
|
|
|
|
|
|
|
|
|
|
// set type and timestamp
|
|
|
|
|
let txtype = 2;
|
|
|
|
|
let timestamp = Utc::now().timestamp() as u32;
|
|
|
|
|
|
|
|
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
|
|
|
|
|
|
if args.len() > 1 && args.len() != 6 {
|
|
|
|
|
println!("Usage: ./create_transfer_tx <coin> <value> <receiver> <txfee> <nft_series>");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let coin_name = if args.len() > 1 {
|
|
|
|
|
args[1].clone()
|
|
|
|
|
} else {
|
|
|
|
|
prompt_visible("Please enter the coin or token you wish to transfer: ").await
|
|
|
|
|
};
|
2026-06-23 17:24:35 +00:00
|
|
|
let Some(coin) = normalize_asset_or_base(&coin_name) else {
|
2026-06-27 16:28:13 +00:00
|
|
|
println!(
|
|
|
|
|
"Transfer asset must be the base coin or normalize to 3-15 alphanumeric characters."
|
|
|
|
|
);
|
2026-06-23 17:24:35 +00:00
|
|
|
return;
|
|
|
|
|
};
|
2026-06-27 16:28:13 +00:00
|
|
|
let is_base_coin = coin
|
|
|
|
|
.trim()
|
|
|
|
|
.eq_ignore_ascii_case(block_extension_and_paths().1.trim());
|
2026-05-24 17:56:57 +00:00
|
|
|
|
|
|
|
|
let value_string = if args.len() > 2 {
|
|
|
|
|
args[2].clone()
|
|
|
|
|
} else {
|
|
|
|
|
prompt_visible("Please enter the amount of coins or tokens to send: (eg. 23.4): ").await
|
|
|
|
|
};
|
|
|
|
|
let value_f32: f32 = value_string
|
|
|
|
|
.trim()
|
|
|
|
|
.parse()
|
|
|
|
|
.expect("Please enter a valid amount.");
|
|
|
|
|
let value = ((value_f32 as f64) * 100_000_000.0).round() as u64;
|
|
|
|
|
|
2026-06-04 15:06:51 +00:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
let wallet = match Wallet::try_obtain_wallet(decryption_key, Some(&wallet_path)).await {
|
|
|
|
|
Ok(wallet) => wallet,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
eprintln!("Wallet decryption failed: {err}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-24 17:56:57 +00:00
|
|
|
let receiver_input = if args.len() > 3 {
|
|
|
|
|
args[3].clone()
|
|
|
|
|
} else {
|
|
|
|
|
prompt_visible("Please enter the receiver wallet address: ").await
|
|
|
|
|
};
|
2026-06-04 15:06:51 +00:00
|
|
|
let receiver = match resolve_wallet_address_input(&receiver_input, &wallet).await {
|
2026-05-24 17:56:57 +00:00
|
|
|
Ok(address) => address,
|
2026-06-02 23:05:48 +00:00
|
|
|
Err(err) => {
|
|
|
|
|
let trimmed_receiver = receiver_input.trim();
|
|
|
|
|
let escaped_receiver = receiver_input
|
|
|
|
|
.chars()
|
|
|
|
|
.flat_map(char::escape_default)
|
|
|
|
|
.collect::<String>();
|
|
|
|
|
let escaped_trimmed_receiver = trimmed_receiver
|
|
|
|
|
.chars()
|
|
|
|
|
.flat_map(char::escape_default)
|
|
|
|
|
.collect::<String>();
|
|
|
|
|
let dot_count = trimmed_receiver.matches('.').count();
|
|
|
|
|
let (payload_chars, payload_bytes, suffix, payload_is_hex, payload_is_ascii) =
|
|
|
|
|
match trimmed_receiver.rsplit_once('.') {
|
|
|
|
|
Some((payload, suffix)) => (
|
|
|
|
|
payload.chars().count(),
|
|
|
|
|
payload.len(),
|
|
|
|
|
suffix,
|
|
|
|
|
payload.chars().all(|ch| ch.is_ascii_hexdigit()),
|
|
|
|
|
payload.is_ascii(),
|
|
|
|
|
),
|
|
|
|
|
None => (0, 0, "<missing>", false, trimmed_receiver.is_ascii()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
println!("receiver wallet is not valid: {err}");
|
|
|
|
|
println!(
|
|
|
|
|
"receiver diagnostics: raw_chars={} raw_bytes={} trimmed_chars={} trimmed_bytes={} dot_count={} suffix={} payload_chars={} payload_bytes={} payload_hex={} payload_ascii={} raw_ascii={} raw_control_chars={}",
|
|
|
|
|
receiver_input.chars().count(),
|
|
|
|
|
receiver_input.len(),
|
|
|
|
|
trimmed_receiver.chars().count(),
|
|
|
|
|
trimmed_receiver.len(),
|
|
|
|
|
dot_count,
|
|
|
|
|
suffix,
|
|
|
|
|
payload_chars,
|
|
|
|
|
payload_bytes,
|
|
|
|
|
payload_is_hex,
|
|
|
|
|
payload_is_ascii,
|
|
|
|
|
receiver_input.is_ascii(),
|
|
|
|
|
receiver_input.chars().any(char::is_control)
|
|
|
|
|
);
|
|
|
|
|
println!("receiver escaped raw: {escaped_receiver}");
|
|
|
|
|
println!("receiver escaped trimmed: {escaped_trimmed_receiver}");
|
2026-05-24 17:56:57 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let txfee_string = if args.len() > 4 {
|
|
|
|
|
args[4].clone()
|
|
|
|
|
} else {
|
|
|
|
|
let prompt = if is_base_coin {
|
|
|
|
|
format!(
|
|
|
|
|
"Please enter the amount for the fee: (eg. 1.09, minimum fee {minimum_transfer_fee_percent:.0}% of transaction):"
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
format!(
|
|
|
|
|
"Please enter the amount for the fee: (eg. 0.0001, minimum fee {:.8} for tokens/NFTs):",
|
|
|
|
|
display_fee(NON_BASE_TRANSFER_MIN_FEE)
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
prompt_visible(&format!("{prompt} ")).await
|
|
|
|
|
};
|
|
|
|
|
let txfee_f32: f32 = txfee_string
|
|
|
|
|
.trim()
|
|
|
|
|
.parse()
|
|
|
|
|
.expect("Please enter a valid fee.");
|
|
|
|
|
let txfee = ((txfee_f32 as f64) * 100_000_000.0).round() as u64;
|
|
|
|
|
|
|
|
|
|
let nft_series_string = if args.len() > 5 {
|
|
|
|
|
args[5].clone()
|
|
|
|
|
} else {
|
|
|
|
|
prompt_visible(
|
|
|
|
|
"Please enter the NFT series number to transfer, or 0 for coins/tokens/1of1 NFTs: ",
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
};
|
|
|
|
|
let nft_series: u32 = nft_series_string
|
|
|
|
|
.trim()
|
|
|
|
|
.parse()
|
|
|
|
|
.expect("Please enter a valid NFT series number.");
|
|
|
|
|
|
|
|
|
|
let private_key = &wallet.saved.private_key;
|
|
|
|
|
let short_address = &wallet.saved.short_address;
|
|
|
|
|
|
|
|
|
|
if !Wallet::short_address_validation(short_address.trim()) {
|
|
|
|
|
println!("sender wallet invalid: {short_address}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if short_address.trim() == receiver.trim() {
|
|
|
|
|
println!("You cannot send funds to yourself");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if nft_series > 0 && value != 100_000_000 {
|
|
|
|
|
println!("Series NFT transfers must have a value of exactly 1.0");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let unsigned_transfer = UnsignedTransferTransaction::new(
|
|
|
|
|
txtype,
|
|
|
|
|
timestamp,
|
|
|
|
|
value,
|
|
|
|
|
&coin,
|
|
|
|
|
nft_series,
|
|
|
|
|
short_address.trim(),
|
|
|
|
|
&receiver,
|
|
|
|
|
txfee,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let transfer = match TransferTransaction::new(unsigned_transfer, private_key).await {
|
|
|
|
|
Ok(tx) => tx,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
eprintln!("Failed to sign transfer transaction: {err}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let hashed_data = transfer.unsigned_transfer.hash().await;
|
|
|
|
|
let signature = transfer.signature.clone();
|
|
|
|
|
|
|
|
|
|
let output = json!({
|
|
|
|
|
"txtype": txtype,
|
|
|
|
|
"timestamp": timestamp,
|
|
|
|
|
"value": value,
|
|
|
|
|
"coin": coin,
|
|
|
|
|
"nft_series": nft_series,
|
|
|
|
|
"sender": short_address.trim(),
|
|
|
|
|
"receiver": receiver,
|
|
|
|
|
"txfee": txfee,
|
|
|
|
|
"hash": hashed_data,
|
|
|
|
|
"signature": signature,
|
|
|
|
|
});
|
|
|
|
|
let output_str = serde_json::to_string_pretty(&output).expect("Failed to serialize JSON");
|
|
|
|
|
|
|
|
|
|
// Define the directory path
|
|
|
|
|
let dir_path = "./transactions";
|
|
|
|
|
|
|
|
|
|
// Create the directory if it doesn't exist
|
|
|
|
|
if let Err(e) = create_dir_all(&dir_path).await {
|
|
|
|
|
eprintln!("Failed to create directory: {e}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define the file path
|
|
|
|
|
let file_path = format!("{dir_path}/{hashed_data}.json");
|
|
|
|
|
|
|
|
|
|
// Open the file for writing asynchronously
|
|
|
|
|
let mut file = File::create(&file_path)
|
|
|
|
|
.await
|
|
|
|
|
.expect("Failed to create file");
|
|
|
|
|
|
|
|
|
|
// Write the JSON string to the file asynchronously
|
|
|
|
|
if let Err(e) = file.write_all(output_str.as_bytes()).await {
|
|
|
|
|
eprintln!("Failed to write file: {e}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 16:05:30 +00:00
|
|
|
println!("{output_str}");
|
2026-05-24 17:56:57 +00:00
|
|
|
}
|