use crate::error::{Error, Result};
use crate::msg::crossvm::{cross_vm_call_any, CROSS_VM_CALL_RESPONSE};
use crate::proto::qorechain::crossvm::v1 as pb;
use crate::query::QorClient;
use crate::tx::{
broadcast, send_messages, BroadcastMode, BuiltTx, Coin as TxCoin, Fee, SendMessagesParams,
};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use cosmrs::proto::cosmos::base::v1beta1::Coin as ProtoCoin;
use prost::Message;
use serde_json::Value;
pub const VM_TYPE_EVM: &str = "evm";
pub const VM_TYPE_COSMWASM: &str = "cosmwasm";
pub const VM_TYPE_SVM: &str = "svm";
pub const VM_TYPES: &[&str] = &[VM_TYPE_EVM, VM_TYPE_COSMWASM, VM_TYPE_SVM];
#[derive(Debug, Clone)]
pub enum Payload {
Raw(Vec<u8>),
CosmWasm(Value),
}
impl Payload {
pub fn to_bytes(&self) -> Result<Vec<u8>> {
match self {
Payload::Raw(b) => Ok(b.clone()),
Payload::CosmWasm(v) => serde_json::to_vec(v)
.map_err(|e| Error::InvalidResponse(format!("serialize cosmwasm payload: {e}"))),
}
}
}
impl From<Vec<u8>> for Payload {
fn from(b: Vec<u8>) -> Self {
Payload::Raw(b)
}
}
impl From<Value> for Payload {
fn from(v: Value) -> Self {
Payload::CosmWasm(v)
}
}
#[derive(Debug, Clone)]
pub struct CallOptions {
pub source_vm: String,
pub target_vm: String,
pub target_contract: String,
pub payload: Payload,
pub funds: Vec<TxCoin>,
pub queue: bool,
}
impl CallOptions {
pub fn new(
target_vm: impl Into<String>,
target_contract: impl Into<String>,
payload: impl Into<Payload>,
) -> Self {
Self {
source_vm: VM_TYPE_EVM.to_string(),
target_vm: target_vm.into(),
target_contract: target_contract.into(),
payload: payload.into(),
funds: Vec::new(),
queue: false,
}
}
pub fn source_vm(mut self, vm: impl Into<String>) -> Self {
self.source_vm = vm.into();
self
}
pub fn funds(mut self, funds: Vec<TxCoin>) -> Self {
self.funds = funds;
self
}
pub fn queue(mut self, queue: bool) -> Self {
self.queue = queue;
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CrossVmCallResponse {
pub message_id: String,
pub executed: bool,
pub data: Vec<u8>,
pub gas_used: u64,
}
impl CrossVmCallResponse {
pub fn decode(bytes: &[u8]) -> Result<Self> {
let pb = pb::MsgCrossVmCallResponse::decode(bytes)
.map_err(|e| Error::InvalidResponse(format!("decode MsgCrossVMCallResponse: {e}")))?;
Ok(Self {
message_id: pb.message_id,
executed: pb.executed,
data: pb.data,
gas_used: pb.gas_used,
})
}
pub fn list_from_broadcast(resp: &Value) -> Result<Vec<Self>> {
let responses = resp
.get("tx_response")
.and_then(|r| r.get("msg_responses"))
.or_else(|| resp.get("msg_responses"))
.and_then(|m| m.as_array());
let Some(responses) = responses else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for entry in responses {
let type_url = entry
.get("type_url")
.or_else(|| entry.get("@type"))
.and_then(|t| t.as_str())
.unwrap_or_default();
if type_url != CROSS_VM_CALL_RESPONSE {
continue;
}
let value = entry.get("value").and_then(|v| v.as_str()).unwrap_or("");
let bytes = BASE64.decode(value).map_err(|e| {
Error::InvalidResponse(format!("decode MsgCrossVMCallResponse base64: {e}"))
})?;
out.push(Self::decode(&bytes)?);
}
Ok(out)
}
}
#[derive(Debug, Clone)]
pub struct CrossVm {
pub sender: String,
pub private_key: Vec<u8>,
pub public_key: Vec<u8>,
pub chain_id: String,
pub account_number: u64,
pub sequence: u64,
pub fee: Fee,
pub rest_url: String,
pub mode: BroadcastMode,
pub qor: Option<QorClient>,
}
impl CrossVm {
pub fn build_call(&self, opts: &CallOptions) -> Result<BuiltTx> {
self.build_atomic(std::slice::from_ref(opts))
}
pub async fn call(&self, opts: &CallOptions) -> Result<Value> {
let built = self.build_call(opts)?;
broadcast(&self.rest_url, &built.tx_raw_bytes, self.mode).await
}
pub async fn call_atomic(&self, opts: &[CallOptions]) -> Result<Value> {
let built = self.build_atomic(opts)?;
broadcast(&self.rest_url, &built.tx_raw_bytes, self.mode).await
}
pub async fn call_with_responses(
&self,
opts: &CallOptions,
) -> Result<(Value, Vec<CrossVmCallResponse>)> {
let raw = self.call(opts).await?;
let decoded = CrossVmCallResponse::list_from_broadcast(&raw)?;
Ok((raw, decoded))
}
pub async fn call_atomic_with_responses(
&self,
opts: &[CallOptions],
) -> Result<(Value, Vec<CrossVmCallResponse>)> {
let raw = self.call_atomic(opts).await?;
let decoded = CrossVmCallResponse::list_from_broadcast(&raw)?;
Ok((raw, decoded))
}
pub fn build_atomic(&self, opts: &[CallOptions]) -> Result<BuiltTx> {
if opts.is_empty() {
return Err(Error::InvalidResponse(
"cross_vm: at least one call is required".into(),
));
}
let mut messages = Vec::with_capacity(opts.len());
for o in opts {
let source_vm = if o.source_vm.is_empty() {
VM_TYPE_EVM
} else {
o.source_vm.as_str()
};
let payload = o.payload.to_bytes()?;
messages.push(cross_vm_call_any(
self.sender.clone(),
source_vm,
o.target_vm.clone(),
o.target_contract.clone(),
payload,
to_proto_coins(&o.funds),
o.queue,
));
}
send_messages(SendMessagesParams {
private_key: self.private_key.clone(),
public_key: self.public_key.clone(),
messages,
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 get_message(&self, id: &str) -> Result<Value> {
let qor = self.qor.as_ref().ok_or_else(|| {
Error::MissingEndpoint("qor (cross_vm.get_message requires a QorClient)".into())
})?;
qor.get_cross_vm_message(id).await
}
}
fn to_proto_coins(coins: &[TxCoin]) -> Vec<ProtoCoin> {
coins
.iter()
.map(|c| ProtoCoin {
denom: c.denom.clone(),
amount: c.amount.clone(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn payload_raw_passthrough() {
let p = Payload::Raw(vec![1, 2, 3]);
assert_eq!(p.to_bytes().unwrap(), vec![1, 2, 3]);
}
#[test]
fn payload_cosmwasm_is_compact_json() {
let p = Payload::CosmWasm(json!({ "increment": {} }));
assert_eq!(p.to_bytes().unwrap(), br#"{"increment":{}}"#.to_vec());
}
#[test]
fn call_options_defaults_source_to_evm() {
let o = CallOptions::new(VM_TYPE_COSMWASM, "qor1contract", vec![0u8]);
assert_eq!(o.source_vm, VM_TYPE_EVM);
assert_eq!(o.target_vm, "cosmwasm");
}
#[test]
fn vm_type_constants() {
assert_eq!(VM_TYPES, &["evm", "cosmwasm", "svm"]);
}
}