1pub use tool_registry::{Tool as ToolAnchor, ToolInnerV1, ToolRef};
9#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct ToolState {
15 pub object_id: sui::types::Address,
17 pub inner: ToolInnerV1,
19}
20
21pub type Tool = ToolState;
23use {
24 crate::{
25 move_bindings::{move_std::ascii, tool::tool_registry},
26 sui,
27 ToolFqn,
28 },
29 anyhow::{anyhow, bail, Context as _},
30 chrono::{DateTime, Utc},
31};
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub enum OnchainToolMode {
36 Standard,
38 WorkflowAuthorization,
40}
41
42impl ToolAnchor {
43 pub fn object_id(&self) -> sui::types::Address {
44 self.id.address()
45 }
46}
47
48impl ToolState {
49 pub fn new(object_id: sui::types::Address, inner: ToolInnerV1) -> Self {
51 Self { object_id, inner }
52 }
53
54 pub fn derive_id(
56 registry_id: sui::types::Address,
57 fqn: &ToolFqn,
58 ) -> anyhow::Result<sui::types::Address> {
59 crate::move_bindings::derive_tool_id(registry_id, fqn)
60 }
61
62 pub fn registry_id(&self) -> sui::types::Address {
63 self.inner.registry.address()
64 }
65
66 pub fn fqn_string(&self) -> anyhow::Result<String> {
67 ascii_string(&self.inner.fqn).context("Tool FQN is not UTF-8")
68 }
69
70 pub fn parsed_fqn(&self) -> anyhow::Result<ToolFqn> {
71 let value = self.fqn_string()?;
72 value
73 .parse()
74 .map_err(|error| anyhow!("Tool FQN '{value}' did not parse: {error}"))
75 }
76
77 pub fn reference(&self) -> &ToolRef {
78 &self.inner.r#ref
79 }
80
81 pub fn description_string(&self) -> anyhow::Result<String> {
82 std::str::from_utf8(&self.inner.description)
83 .map(str::to_owned)
84 .context("Tool description is not UTF-8")
85 }
86
87 pub fn registered_at_datetime(&self) -> anyhow::Result<DateTime<Utc>> {
88 timestamp_millis_to_datetime(self.inner.registered_at_ms, "registered_at_ms")
89 }
90
91 pub fn unregistered_at_datetime(&self) -> anyhow::Result<Option<DateTime<Utc>>> {
92 self.unregistered_at_millis()?
93 .map(|millis| timestamp_millis_to_datetime(millis, "closed_at_ms"))
94 .transpose()
95 }
96
97 pub fn unregistered_at_millis(&self) -> anyhow::Result<Option<u64>> {
98 match self.inner.unregistered_at_ms.vec.as_slice() {
99 [] => Ok(None),
100 [millis] => Ok(Some(*millis)),
101 values => bail!(
102 "Tool unregistered_at_ms is not a valid Move option: {} values",
103 values.len()
104 ),
105 }
106 }
107}
108
109impl ToolRef {
110 pub fn http_url_string(&self) -> anyhow::Result<Option<String>> {
111 let Self::Http { url, .. } = self else {
112 return Ok(None);
113 };
114 bytes_string(url, "HTTP tool URL").map(Some)
115 }
116
117 pub fn sui_parts(
118 &self,
119 ) -> anyhow::Result<Option<(sui::types::Address, String, sui::types::Address)>> {
120 let Self::Sui {
121 package_address,
122 module_name,
123 tool_witness_id,
124 ..
125 } = self
126 else {
127 return Ok(None);
128 };
129
130 Ok(Some((
131 *package_address,
132 ascii_string(module_name).context("Sui tool module name is not UTF-8")?,
133 tool_witness_id.address(),
134 )))
135 }
136
137 pub fn display_string(&self) -> anyhow::Result<String> {
138 match self {
139 Self::Http { .. } => self
140 .http_url_string()?
141 .ok_or_else(|| anyhow!("expected HTTP tool reference")),
142 Self::Sui { .. } => {
143 let Some((package_address, module_name, tool_witness_id)) = self.sui_parts()?
144 else {
145 unreachable!("matched Sui tool reference")
146 };
147 Ok(format!(
148 "{package_address}::{module_name}@{tool_witness_id}"
149 ))
150 }
151 }
152 }
153}
154
155impl std::fmt::Display for ToolRef {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self.display_string() {
158 Ok(value) => f.write_str(&value),
159 Err(_) => f.write_str("<invalid tool reference>"),
160 }
161 }
162}
163
164fn ascii_string(value: &ascii::String) -> anyhow::Result<String> {
165 bytes_string(&value.bytes, "Move ASCII string")
166}
167
168fn bytes_string(value: &[u8], label: &str) -> anyhow::Result<String> {
169 std::str::from_utf8(value)
170 .map(str::to_owned)
171 .with_context(|| format!("{label} is not UTF-8"))
172}
173
174fn timestamp_millis_to_datetime(value: u64, field: &str) -> anyhow::Result<DateTime<Utc>> {
175 let millis = i64::try_from(value).with_context(|| format!("{field} exceeds i64::MAX"))?;
176 DateTime::<Utc>::from_timestamp_millis(millis)
177 .ok_or_else(|| anyhow!("{field} is outside chrono's timestamp range"))
178}
179
180#[cfg(test)]
181mod tests {
182 use {
183 super::*,
184 crate::{
185 fqn,
186 move_bindings::{move_std::option::Option as MoveOption, sui_framework},
187 test_utils::sui_mocks,
188 },
189 };
190
191 fn ascii(value: &str) -> ascii::String {
192 ascii::String::from(value)
193 }
194
195 fn fixture_tool(reference: ToolRef) -> ToolState {
196 let object_id = sui_mocks::mock_sui_address();
197 ToolState::new(
198 object_id,
199 ToolInnerV1::new(
200 crate::move_bindings::sui_framework::object::ID::new(sui_mocks::mock_sui_address()),
201 ascii("xyz.taluslabs.math.i64.add@1"),
202 reference,
203 b"A test tool".to_vec(),
204 crate::move_bindings::interface::meta_schema::MetaSchema::new(vec![], vec![]),
205 false,
206 sui_framework::balance::Balance {
207 value: 0,
208 phantom_t0: std::marker::PhantomData,
209 },
210 true,
211 0,
212 0,
213 MoveOption::from(None),
214 ),
215 )
216 }
217
218 #[test]
219 fn test_tool_derive_id() {
220 let registry_id = sui::types::Address::from_static(
221 "0x940f0dd81d4e4ae2cd476ff61ca5699e0d9356e1874d6c4ba3a5bdf28e67b9e9",
222 );
223
224 let fqn = fqn!("xyz.taluslabs.math.i64.add@1");
225 let expected_id = sui::types::Address::from_static(
226 "0x63152163bf12d54f38742656cba5d37a05e89d3ef5df7e9d22062e7bff0aed35",
227 );
228 assert_eq!(Tool::derive_id(registry_id, &fqn).unwrap(), expected_id);
229
230 let fqn = fqn!("xyz.taluslabs.math.i64.mul@1");
231 let expected_id = sui::types::Address::from_static(
232 "0xc841b225a7e79c76942f3df05f1fcf17c2b259626ed51cb84e562cb3403604da",
233 );
234 assert_eq!(Tool::derive_id(registry_id, &fqn).unwrap(), expected_id);
235 }
236
237 #[test]
238 fn generated_tool_parts_round_trip_and_preserve_meta_schema() {
239 let tool = fixture_tool(ToolRef::Http {
240 url: b"https://example.com/tool".to_vec(),
241 });
242
243 let inner_bytes = bcs::to_bytes(&tool.inner).expect("generated inner serializes as BCS");
244 let decoded = ToolState::new(
245 tool.object_id,
246 bcs::from_bytes(&inner_bytes).expect("generated inner decodes"),
247 );
248
249 assert_eq!(
250 decoded.fqn_string().unwrap(),
251 "xyz.taluslabs.math.i64.add@1"
252 );
253 assert_eq!(decoded.description_string().unwrap(), "A test tool");
254 assert_eq!(
255 decoded.registered_at_datetime().unwrap(),
256 DateTime::<Utc>::from_timestamp(0, 0).unwrap()
257 );
258 assert_eq!(decoded.inner.meta_schema, tool.inner.meta_schema);
259 }
260
261 #[test]
262 fn tool_ref_helpers_decode_http_and_sui() {
263 let http_ref = ToolRef::Http {
264 url: b"https://example.com/tool".to_vec(),
265 };
266 assert_eq!(
267 http_ref.http_url_string().unwrap().unwrap().as_str(),
268 "https://example.com/tool"
269 );
270 assert_eq!(http_ref.to_string(), "https://example.com/tool");
271
272 let package_address = sui::types::Address::from_static(
273 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
274 );
275 let tool_witness_id = sui::types::Address::from_static(
276 "0xabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd",
277 );
278 let sui_ref = ToolRef::Sui {
279 package_address,
280 module_name: ascii("my_tool_module"),
281 tool_witness_id: crate::move_bindings::sui_framework::object::ID::new(tool_witness_id),
282 };
283 let (package, module, witness) = sui_ref.sui_parts().unwrap().unwrap();
284 assert_eq!(package, package_address);
285 assert_eq!(module, "my_tool_module");
286 assert_eq!(witness, tool_witness_id);
287 assert_eq!(
288 sui_ref.to_string(),
289 format!("{package_address}::my_tool_module@{tool_witness_id}")
290 );
291 }
292}