use crate::error::TaiError;
use crate::ids::ObjectId;
use crate::rpc::RpcClient;
use serde_json::{json, Value};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum WorkOrderStatus {
New = 0,
Accepted = 1,
ReceiptSubmitted = 2,
Released = 3,
Refunded = 4,
Disputed = 5,
}
impl WorkOrderStatus {
pub fn from_u8(v: u8) -> Result<Self, TaiError> {
match v {
0 => Ok(Self::New),
1 => Ok(Self::Accepted),
2 => Ok(Self::ReceiptSubmitted),
3 => Ok(Self::Released),
4 => Ok(Self::Refunded),
5 => Ok(Self::Disputed),
other => Err(TaiError::Decode(format!(
"unknown WorkOrder status code: {other}"
))),
}
}
pub fn label(self) -> &'static str {
match self {
Self::New => "new",
Self::Accepted => "accepted",
Self::ReceiptSubmitted => "receipt_submitted",
Self::Released => "released",
Self::Refunded => "refunded",
Self::Disputed => "disputed",
}
}
}
#[derive(Clone, Debug)]
pub struct WorkOrderView {
pub object_id: ObjectId,
pub object_type: String,
pub coin_type: String,
pub buyer: String,
pub payee_launchpad_account_id: ObjectId,
pub payee_agent_treasury_id: ObjectId,
pub locked_sui: u64,
pub amount: u64,
pub spec_hash: Vec<u8>,
pub spec_url: String,
pub created_at_ms: u64,
pub deadline_ms: u64,
pub receipt_submitted_at_ms: u64,
pub dispute_window_ms: u64,
pub receipt_hash: Vec<u8>,
pub receipt_url: String,
pub status: WorkOrderStatus,
}
impl WorkOrderView {
pub async fn fetch(rpc: &RpcClient, object_id: ObjectId) -> Result<Self, TaiError> {
let params = json!([
object_id.to_string(),
{ "showContent": true, "showType": true }
]);
let raw: Value = rpc.call("sui_getObject", params).await?;
decode_work_order(&raw, object_id)
}
}
fn decode_work_order(raw: &Value, expected_id: ObjectId) -> Result<WorkOrderView, TaiError> {
let data = raw
.get("data")
.ok_or_else(|| TaiError::Decode("missing `data` in getObject".into()))?;
let content = data
.get("content")
.ok_or_else(|| TaiError::Decode("missing `content`".into()))?;
let data_type = content
.get("dataType")
.and_then(|v| v.as_str())
.unwrap_or("");
if data_type != "moveObject" {
return Err(TaiError::Decode(format!(
"expected moveObject, got {data_type}"
)));
}
let object_type = content
.get("type")
.and_then(|v| v.as_str())
.ok_or_else(|| TaiError::Decode("missing object type".into()))?
.to_string();
let coin_type = extract_coin_type(&object_type)
.ok_or_else(|| TaiError::Decode(format!("malformed WorkOrder type: {object_type}")))?;
let fields = content
.get("fields")
.ok_or_else(|| TaiError::Decode("missing `fields`".into()))?;
let status_u8 = parse_u64_str(fields, "status")? as u8;
let status = WorkOrderStatus::from_u8(status_u8)?;
Ok(WorkOrderView {
object_id: expected_id,
object_type,
coin_type,
buyer: parse_string(fields, "buyer")?,
payee_launchpad_account_id: parse_id(fields, "payee_launchpad_account_id")?,
payee_agent_treasury_id: parse_id(fields, "payee_agent_treasury_id")?,
locked_sui: parse_balance(fields, "locked")?,
amount: parse_u64_str(fields, "amount")?,
spec_hash: parse_byte_vec(fields, "spec_hash")?,
spec_url: parse_string(fields, "spec_url")?,
created_at_ms: parse_u64_str(fields, "created_at_ms")?,
deadline_ms: parse_u64_str(fields, "deadline_ms")?,
receipt_submitted_at_ms: parse_u64_str(fields, "receipt_submitted_at_ms")?,
dispute_window_ms: parse_u64_str(fields, "dispute_window_ms")?,
receipt_hash: parse_byte_vec(fields, "receipt_hash")?,
receipt_url: parse_string(fields, "receipt_url")?,
status,
})
}
fn parse_u64_str(fields: &Value, key: &str) -> Result<u64, TaiError> {
let v = fields
.get(key)
.ok_or_else(|| TaiError::Decode(format!("missing field {key}")))?;
if let Some(s) = v.as_str() {
s.parse::<u64>()
.map_err(|e| TaiError::Decode(format!("field {key} not u64: {e}")))
} else if let Some(n) = v.as_u64() {
Ok(n)
} else {
Err(TaiError::Decode(format!("field {key} not number-like")))
}
}
fn parse_string(fields: &Value, key: &str) -> Result<String, TaiError> {
fields
.get(key)
.and_then(|v| v.as_str())
.map(str::to_string)
.ok_or_else(|| TaiError::Decode(format!("field {key} not a string")))
}
fn parse_id(fields: &Value, key: &str) -> Result<ObjectId, TaiError> {
use std::str::FromStr;
let s = parse_string(fields, key)?;
ObjectId::from_str(&s).map_err(|e| TaiError::Decode(format!("field {key}: {e}")))
}
fn parse_byte_vec(fields: &Value, key: &str) -> Result<Vec<u8>, TaiError> {
let v = fields
.get(key)
.ok_or_else(|| TaiError::Decode(format!("missing field {key}")))?;
if let Some(s) = v.as_str() {
use base64ct::{Base64, Encoding};
match Base64::decode_vec(s) {
Ok(bytes) => Ok(bytes),
Err(_) => Ok(s.as_bytes().to_vec()),
}
} else if let Some(arr) = v.as_array() {
let mut out = Vec::with_capacity(arr.len());
for elem in arr {
let n = elem
.as_u64()
.ok_or_else(|| TaiError::Decode(format!("field {key}: non-u8 element")))?;
if n > 255 {
return Err(TaiError::Decode(format!("field {key}: byte >255")));
}
out.push(n as u8);
}
Ok(out)
} else {
Err(TaiError::Decode(format!("field {key}: not bytes")))
}
}
fn parse_balance(fields: &Value, key: &str) -> Result<u64, TaiError> {
let v = fields
.get(key)
.ok_or_else(|| TaiError::Decode(format!("missing field {key}")))?;
if let Some(obj) = v.as_object() {
if let Some(inner) = obj.get("value") {
if let Some(s) = inner.as_str() {
return s
.parse::<u64>()
.map_err(|e| TaiError::Decode(format!("balance {key}.value: {e}")));
}
if let Some(n) = inner.as_u64() {
return Ok(n);
}
}
}
if let Some(s) = v.as_str() {
return s
.parse::<u64>()
.map_err(|e| TaiError::Decode(format!("balance {key}: {e}")));
}
if let Some(n) = v.as_u64() {
return Ok(n);
}
Err(TaiError::Decode(format!(
"balance {key}: unrecognized shape"
)))
}
fn extract_coin_type(t: &str) -> Option<String> {
let lt = t.find('<')?;
let gt = t.rfind('>')?;
if gt <= lt + 1 {
return None;
}
Some(t[lt + 1..gt].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_round_trips() {
for code in 0u8..=5 {
let s = WorkOrderStatus::from_u8(code).unwrap();
assert_eq!(s as u8, code);
}
assert!(WorkOrderStatus::from_u8(99).is_err());
}
#[test]
fn coin_type_extracted_from_object_type() {
let t = "0xabc::work_order::WorkOrder<0xdef::larry::LARRY>";
assert_eq!(extract_coin_type(t).unwrap(), "0xdef::larry::LARRY");
}
#[test]
fn labels_are_lowercase_snake_case() {
assert_eq!(WorkOrderStatus::New.label(), "new");
assert_eq!(
WorkOrderStatus::ReceiptSubmitted.label(),
"receipt_submitted"
);
}
}