use crate::error::{Error, Result};
use crate::msg::pqc::{migrate_pqc_key_any, register_pqc_key_any};
use crate::pqc::ALGORITHM_DILITHIUM5;
use crate::query::QorClient;
use crate::tx::{
broadcast, build_hybrid_tx, send_messages, BroadcastMode, BuildHybridTxParams, BuiltTx, Fee,
Message, SendMessagesParams,
};
use serde_json::Value;
pub const PQC_KEY_STATUS_PRECOMPILE: &str = "0x0000000000000000000000000000000000000A02";
pub const DEFAULT_KEY_TYPE: &str = "hybrid";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PqcStatus {
pub registered: bool,
pub algorithm_id: Option<u8>,
pub pubkey: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnsureResult {
pub already_registered: bool,
pub tx_hash: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PqcDx {
pub sender: String,
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub pqc_public_key: Vec<u8>,
pub pqc_secret_key: Vec<u8>,
pub chain_id: String,
pub account_number: u64,
pub sequence: u64,
pub fee: Fee,
pub key_type: String,
pub rest_url: String,
pub mode: BroadcastMode,
pub qor: Option<QorClient>,
}
impl PqcDx {
pub async fn is_pqc_registered(&self, address: &str) -> Result<bool> {
Ok(self.get_pqc_status(address).await?.registered)
}
pub async fn get_pqc_status(&self, address: &str) -> Result<PqcStatus> {
let qor = self.require_qor("get_pqc_status")?;
let raw = qor.get_pqc_key_status(address).await?;
Ok(parse_pqc_status(&raw))
}
pub async fn ensure_pqc_registered(&self) -> Result<EnsureResult> {
if self.qor.is_some() && self.is_pqc_registered(&self.sender).await? {
return Ok(EnsureResult {
already_registered: true,
tx_hash: None,
});
}
let built = self.build_register()?;
let resp = broadcast(&self.rest_url, &built.tx_raw_bytes, self.mode).await?;
Ok(EnsureResult {
already_registered: false,
tx_hash: extract_tx_hash(&resp),
})
}
pub fn build_register(&self) -> Result<BuiltTx> {
let msg = register_pqc_key_any(
self.sender.clone(),
self.pqc_public_key.clone(),
self.public_key.clone(),
self.key_type(),
);
send_messages(SendMessagesParams {
private_key: self.private_key.clone(),
public_key: self.public_key.clone(),
messages: vec![msg],
chain_id: self.chain_id.clone(),
account_number: self.account_number,
sequence: self.sequence,
fee: self.fee.clone(),
memo: String::new(),
timeout_height: 0,
})
}
pub async fn migrate_to_hybrid(&self) -> Result<HybridSendPath> {
let ensured = self.ensure_pqc_registered().await?;
Ok(HybridSendPath {
already_registered: ensured.already_registered,
registration_tx_hash: ensured.tx_hash,
dx: self.clone(),
})
}
pub fn build_hybrid(&self, messages: Vec<Message>) -> Result<BuiltTx> {
build_hybrid_tx(BuildHybridTxParams {
private_key: self.private_key.clone(),
public_key: self.public_key.clone(),
pqc_secret_key: self.pqc_secret_key.clone(),
pqc_public_key: self.pqc_public_key.clone(),
messages,
fee: self.fee.clone(),
chain_id: self.chain_id.clone(),
account_number: self.account_number,
sequence: self.sequence,
memo: String::new(),
timeout_height: 0,
include_pqc_public_key: false,
})
}
pub async fn send_hybrid(&self, messages: Vec<Message>) -> Result<Value> {
let built = self.build_hybrid(messages)?;
broadcast(&self.rest_url, &built.tx_raw_bytes, self.mode).await
}
pub async fn migrate_pqc_key(&self, opts: &MigrateKeyOptions) -> Result<Value> {
let built = self.build_migrate_pqc_key(opts)?;
broadcast(&self.rest_url, &built.tx_raw_bytes, self.mode).await
}
pub fn build_migrate_pqc_key(&self, opts: &MigrateKeyOptions) -> Result<BuiltTx> {
let algorithm_id = if opts.new_algorithm_id == 0 {
ALGORITHM_DILITHIUM5
} else {
opts.new_algorithm_id
};
let msg = migrate_pqc_key_any(
self.sender.clone(),
opts.old_public_key.clone(),
opts.new_public_key.clone(),
algorithm_id,
opts.old_signature.clone(),
opts.new_signature.clone(),
);
send_messages(SendMessagesParams {
private_key: self.private_key.clone(),
public_key: self.public_key.clone(),
messages: vec![msg],
chain_id: self.chain_id.clone(),
account_number: self.account_number,
sequence: self.sequence,
fee: self.fee.clone(),
memo: String::new(),
timeout_height: 0,
})
}
fn key_type(&self) -> String {
if self.key_type.is_empty() {
DEFAULT_KEY_TYPE.to_string()
} else {
self.key_type.clone()
}
}
fn require_qor(&self, ctx: &str) -> Result<&QorClient> {
self.qor.as_ref().ok_or_else(|| {
Error::MissingEndpoint(format!("qor (pqc_dx.{ctx} requires a QorClient)"))
})
}
}
#[derive(Debug, Clone, Default)]
pub struct MigrateKeyOptions {
pub old_public_key: Vec<u8>,
pub new_public_key: Vec<u8>,
pub new_algorithm_id: u32,
pub old_signature: Vec<u8>,
pub new_signature: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct HybridSendPath {
pub already_registered: bool,
pub registration_tx_hash: Option<String>,
pub dx: PqcDx,
}
impl HybridSendPath {
pub fn build_hybrid(&self, messages: Vec<Message>) -> Result<BuiltTx> {
self.dx.build_hybrid(messages)
}
pub async fn send_hybrid(&self, messages: Vec<Message>) -> Result<Value> {
self.dx.send_hybrid(messages).await
}
}
fn as_bool(v: &Value) -> bool {
match v {
Value::Bool(b) => *b,
Value::Number(n) => n.as_i64().map(|i| i != 0).unwrap_or(false),
Value::String(s) => s == "true" || s == "1",
_ => false,
}
}
fn as_u8(v: &Value) -> Option<u8> {
match v {
Value::Number(n) => n.as_u64().and_then(|x| u8::try_from(x).ok()),
Value::String(s) => s.trim().parse::<u8>().ok(),
_ => None,
}
}
fn decode_pubkey(v: &Value) -> Option<Vec<u8>> {
match v {
Value::String(s) if s.is_empty() => None,
Value::String(s) => {
let trimmed = s.strip_prefix("0x").unwrap_or(s);
if let Ok(bytes) = hex::decode(trimmed) {
return Some(bytes);
}
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
BASE64.decode(s).ok()
}
Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items {
let b = it.as_u64().and_then(|x| u8::try_from(x).ok())?;
out.push(b);
}
Some(out)
}
_ => None,
}
}
fn parse_pqc_status(raw: &Value) -> PqcStatus {
let obj = match raw {
Value::Object(_) => raw,
_ => return PqcStatus::default(),
};
let registered = ["registered", "isRegistered", "is_registered"]
.iter()
.find_map(|k| obj.get(*k))
.map(as_bool)
.unwrap_or(false);
let algorithm_id = ["algorithmId", "algorithm_id"]
.iter()
.find_map(|k| obj.get(*k))
.and_then(as_u8);
let pubkey = ["pubkey", "publicKey", "public_key"]
.iter()
.find_map(|k| obj.get(*k))
.and_then(decode_pubkey);
PqcStatus {
registered,
algorithm_id,
pubkey,
}
}
fn extract_tx_hash(resp: &Value) -> Option<String> {
resp.get("tx_response")
.and_then(|r| r.get("txhash"))
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parse_status_registered_snake_case() {
let raw = json!({ "registered": true, "algorithm_id": 1, "public_key": "0xdeadbeef" });
let s = parse_pqc_status(&raw);
assert!(s.registered);
assert_eq!(s.algorithm_id, Some(1));
assert_eq!(s.pubkey, Some(vec![0xde, 0xad, 0xbe, 0xef]));
}
#[test]
fn parse_status_camel_case_and_string_fields() {
let raw = json!({ "isRegistered": "true", "algorithmId": "1" });
let s = parse_pqc_status(&raw);
assert!(s.registered);
assert_eq!(s.algorithm_id, Some(1));
assert_eq!(s.pubkey, None);
}
#[test]
fn parse_status_unregistered_and_unknown_shapes() {
assert!(!parse_pqc_status(&json!({ "registered": false })).registered);
assert_eq!(parse_pqc_status(&Value::Null), PqcStatus::default());
assert_eq!(parse_pqc_status(&json!("nope")), PqcStatus::default());
}
#[test]
fn pubkey_decodes_base64_and_array() {
let s = parse_pqc_status(&json!({ "registered": true, "pubkey": "AQID" }));
assert_eq!(s.pubkey, Some(vec![1, 2, 3]));
let s = parse_pqc_status(&json!({ "registered": true, "pubkey": [4, 5, 6] }));
assert_eq!(s.pubkey, Some(vec![4, 5, 6]));
}
#[test]
fn extract_tx_hash_reads_nested_field() {
assert_eq!(
extract_tx_hash(&json!({ "tx_response": { "txhash": "ABC123" } })),
Some("ABC123".to_string())
);
assert_eq!(extract_tx_hash(&json!({ "tx_response": { "txhash": "" } })), None);
assert_eq!(extract_tx_hash(&json!({})), None);
}
}