234 lines
8.1 KiB
Rust
234 lines
8.1 KiB
Rust
|
|
use blockchain::common::binary_conversions::binary_to_string;
|
||
|
|
use blockchain::common::cli_prompts::prompt_hidden_nonempty;
|
||
|
|
use blockchain::common::network_startup::get_connections;
|
||
|
|
use blockchain::encode;
|
||
|
|
use blockchain::env;
|
||
|
|
use blockchain::json;
|
||
|
|
use blockchain::records::memory::response_channels::generate_uid;
|
||
|
|
use blockchain::standalone_tools::connections::handshake;
|
||
|
|
use blockchain::wallets::structures::Wallet;
|
||
|
|
use blockchain::{Map, Value};
|
||
|
|
|
||
|
|
const NFT_NAME_BYTES: usize = 15;
|
||
|
|
const SERIES_BYTES: usize = 4;
|
||
|
|
const GENESIS_HASH_BYTES: usize = 32;
|
||
|
|
const CREATOR_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
|
||
|
|
const IPFS_BYTES: usize = 100;
|
||
|
|
const HOLDER_BYTES: usize = Wallet::SHORT_ADDRESS_BYTES_LENGTH;
|
||
|
|
const HISTORY_COUNT_BYTES: usize = 4;
|
||
|
|
const HISTORY_ENTRY_SIZE: usize =
|
||
|
|
32 + 4 + 1 + 1 + (2 * Wallet::SHORT_ADDRESS_BYTES_LENGTH) + 15 + 4 + 8;
|
||
|
|
const NFT_LOOKUP_FIXED_BYTES: usize = NFT_NAME_BYTES
|
||
|
|
+ SERIES_BYTES
|
||
|
|
+ GENESIS_HASH_BYTES
|
||
|
|
+ CREATOR_BYTES
|
||
|
|
+ IPFS_BYTES
|
||
|
|
+ HOLDER_BYTES
|
||
|
|
+ HISTORY_COUNT_BYTES;
|
||
|
|
|
||
|
|
fn action_name(action: u8) -> &'static str {
|
||
|
|
// History action bytes are stored compactly on the wire and expanded for display.
|
||
|
|
match action {
|
||
|
|
1 => "create_nft",
|
||
|
|
2 => "transfer",
|
||
|
|
3 => "swap",
|
||
|
|
4 => "loan_locked",
|
||
|
|
5 => "loan_payment",
|
||
|
|
6 => "collateral_claimed",
|
||
|
|
7 => "loan_issued",
|
||
|
|
_ => "unknown",
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn decode_wallet(bytes: &[u8]) -> String {
|
||
|
|
// Empty wallet fields are encoded as all zeroes; otherwise decode a short address.
|
||
|
|
if bytes.iter().all(|b| *b == 0) {
|
||
|
|
String::new()
|
||
|
|
} else {
|
||
|
|
Wallet::bytes_to_short_address(bytes).unwrap_or_default()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn decode_history_entry(entry: &[u8]) -> Option<Value> {
|
||
|
|
// Each history row has a fixed binary size so bad lengths mean an invalid response.
|
||
|
|
if entry.len() != HISTORY_ENTRY_SIZE {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode the history fields in the same order the RPC command writes them.
|
||
|
|
let txid = encode(&entry[0..32]);
|
||
|
|
let block = u32::from_le_bytes(entry[32..36].try_into().ok()?);
|
||
|
|
let txtype = entry[36];
|
||
|
|
let action = entry[37];
|
||
|
|
let from = decode_wallet(&entry[38..60]);
|
||
|
|
let to = decode_wallet(&entry[60..82]);
|
||
|
|
let received_asset = binary_to_string(entry[82..97].to_vec()).trim().to_string();
|
||
|
|
let received_series = u32::from_le_bytes(entry[97..101].try_into().ok()?);
|
||
|
|
let received_value_raw = u64::from_le_bytes(entry[101..109].try_into().ok()?);
|
||
|
|
let received_value = received_value_raw as f64 / 100_000_000.0;
|
||
|
|
|
||
|
|
// Omit empty optional fields so the CLI output stays readable.
|
||
|
|
let mut obj = Map::new();
|
||
|
|
obj.insert("txid".to_string(), json!(txid));
|
||
|
|
obj.insert("block".to_string(), json!(block));
|
||
|
|
obj.insert("txtype".to_string(), json!(txtype));
|
||
|
|
obj.insert("action".to_string(), json!(action_name(action)));
|
||
|
|
|
||
|
|
if !from.is_empty() {
|
||
|
|
obj.insert("from".to_string(), json!(from));
|
||
|
|
}
|
||
|
|
if !to.is_empty() {
|
||
|
|
obj.insert("to".to_string(), json!(to));
|
||
|
|
}
|
||
|
|
if !received_asset.is_empty() {
|
||
|
|
obj.insert("received_asset".to_string(), json!(received_asset));
|
||
|
|
if received_series > 0 {
|
||
|
|
obj.insert("received_series".to_string(), json!(received_series));
|
||
|
|
}
|
||
|
|
obj.insert("received_value".to_string(), json!(received_value));
|
||
|
|
}
|
||
|
|
|
||
|
|
Some(Value::Object(obj))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn decode_nft_lookup(response: &[u8]) -> Option<String> {
|
||
|
|
// The fixed NFT fields must exist before any history entries can be decoded.
|
||
|
|
if response.len() < NFT_LOOKUP_FIXED_BYTES {
|
||
|
|
return None;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decode the fixed-width NFT metadata fields in wire order.
|
||
|
|
let mut cursor = 0usize;
|
||
|
|
let nft_name = binary_to_string(response[cursor..cursor + NFT_NAME_BYTES].to_vec())
|
||
|
|
.trim()
|
||
|
|
.to_string();
|
||
|
|
cursor += NFT_NAME_BYTES;
|
||
|
|
|
||
|
|
let series = u32::from_le_bytes(response[cursor..cursor + SERIES_BYTES].try_into().ok()?);
|
||
|
|
cursor += SERIES_BYTES;
|
||
|
|
|
||
|
|
let genesis = encode(&response[cursor..cursor + GENESIS_HASH_BYTES]);
|
||
|
|
cursor += GENESIS_HASH_BYTES;
|
||
|
|
|
||
|
|
let creator = Wallet::bytes_to_short_address(&response[cursor..cursor + CREATOR_BYTES])?;
|
||
|
|
cursor += CREATOR_BYTES;
|
||
|
|
|
||
|
|
let ipfs_cid = binary_to_string(response[cursor..cursor + IPFS_BYTES].to_vec())
|
||
|
|
.trim()
|
||
|
|
.to_string();
|
||
|
|
cursor += IPFS_BYTES;
|
||
|
|
|
||
|
|
let current_holder = decode_wallet(&response[cursor..cursor + HOLDER_BYTES]);
|
||
|
|
cursor += HOLDER_BYTES;
|
||
|
|
|
||
|
|
let history_count = u32::from_le_bytes(
|
||
|
|
response[cursor..cursor + HISTORY_COUNT_BYTES]
|
||
|
|
.try_into()
|
||
|
|
.ok()?,
|
||
|
|
);
|
||
|
|
cursor += HISTORY_COUNT_BYTES;
|
||
|
|
|
||
|
|
// Any remaining bytes are fixed-size history entries.
|
||
|
|
let mut history = Vec::new();
|
||
|
|
let mut offset = cursor;
|
||
|
|
while offset + HISTORY_ENTRY_SIZE <= response.len() {
|
||
|
|
if let Some(entry) = decode_history_entry(&response[offset..offset + HISTORY_ENTRY_SIZE]) {
|
||
|
|
history.push(entry);
|
||
|
|
}
|
||
|
|
offset += HISTORY_ENTRY_SIZE;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert the binary record into user-facing JSON.
|
||
|
|
let mut output = Map::new();
|
||
|
|
output.insert("nft_name".to_string(), json!(nft_name));
|
||
|
|
output.insert("series".to_string(), json!(series));
|
||
|
|
if series == 0 {
|
||
|
|
output.insert("ipfs".to_string(), json!(format!("ipfs://{ipfs_cid}")));
|
||
|
|
} else {
|
||
|
|
output.insert(
|
||
|
|
"ipfs".to_string(),
|
||
|
|
json!(format!("ipfs://{ipfs_cid}/{series}.json")),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if series > 0 {
|
||
|
|
output.insert(
|
||
|
|
"asset_name".to_string(),
|
||
|
|
json!(format!("{nft_name}_{series}")),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
output.insert("genesis".to_string(), json!(genesis));
|
||
|
|
output.insert("creator".to_string(), json!(creator));
|
||
|
|
output.insert("current_holder".to_string(), json!(current_holder));
|
||
|
|
output.insert("history_count".to_string(), json!(history_count));
|
||
|
|
output.insert("history".to_string(), Value::Array(history));
|
||
|
|
|
||
|
|
serde_json::to_string_pretty(&Value::Object(output)).ok()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::main]
|
||
|
|
async fn main() {
|
||
|
|
// Command 36 asks a peer for NFT metadata by name and series/item number.
|
||
|
|
let hashmap_key = generate_uid();
|
||
|
|
let rpc_command = 36;
|
||
|
|
|
||
|
|
// Item number 0 means a 1/1 NFT; nonzero values identify a series item.
|
||
|
|
let args: Vec<String> = env::args().collect();
|
||
|
|
if args.len() != 3 {
|
||
|
|
println!("Usage: ./lookup_nft <nft_name> <item_number>");
|
||
|
|
println!("Use item_number 0 for a 1/1 NFT, or the specific item number for a series NFT.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let nft_name = args[1].clone();
|
||
|
|
let item_number = args[2].clone();
|
||
|
|
let encryption_key = prompt_hidden_nonempty(
|
||
|
|
"What is your wallet decryption key? ",
|
||
|
|
"Wallet key cannot be empty. Please try again.",
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
// sending_request expects command 36 lookup input as "name|series".
|
||
|
|
let payload = format!("{nft_name}|{item_number}");
|
||
|
|
|
||
|
|
// Try each configured peer until one returns a parsable NFT record or text error.
|
||
|
|
let connections = get_connections().await;
|
||
|
|
let mut connected = false;
|
||
|
|
|
||
|
|
for conn in connections {
|
||
|
|
if connected {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
let socket_address = conn.parse().expect("Failed to parse the socket address");
|
||
|
|
let result = handshake::connect_and_handshake(
|
||
|
|
socket_address,
|
||
|
|
payload.clone(),
|
||
|
|
rpc_command,
|
||
|
|
handshake::HandshakeWallet::WalletKey(encryption_key.clone()),
|
||
|
|
hashmap_key,
|
||
|
|
)
|
||
|
|
.await;
|
||
|
|
|
||
|
|
match result {
|
||
|
|
Ok(response) => {
|
||
|
|
// Successful NFT lookups are binary records; errors are returned as text.
|
||
|
|
if let Some(output) = decode_nft_lookup(&response) {
|
||
|
|
println!("{output}");
|
||
|
|
connected = true;
|
||
|
|
} else {
|
||
|
|
let response_text = String::from_utf8_lossy(&response);
|
||
|
|
let trimmed = response_text.trim();
|
||
|
|
if !trimmed.is_empty() {
|
||
|
|
println!("{trimmed}");
|
||
|
|
connected = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
Err(_) => connected = false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !connected {
|
||
|
|
eprintln!("failed to connect");
|
||
|
|
}
|
||
|
|
}
|