1use std::collections::HashMap;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19
20use thruput_json_rpc_types::transaction::TxHash;
21use thruput_xdr::contract::{Contract, ContractBlob};
22use thruput_xdr::types::{ContractAddress, SHA256Hash};
23use tokio::sync::{Mutex, MutexGuard};
24use xdr_common::BytesM;
25
26use crate::error::{Result, TestkitError};
27use crate::nonce::NonceNamespace;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ArtifactKind {
32 Rv32,
34}
35
36#[derive(Debug)]
38pub(crate) enum ArtifactSpec {
39 Path {
41 name: String,
42 path: PathBuf,
43 kind: Option<ArtifactKind>,
44 },
45 Bytes { name: String, bytes: Vec<u8> },
47}
48
49impl ArtifactSpec {
50 fn name(&self) -> &str {
51 match self {
52 Self::Path { name, .. } | Self::Bytes { name, .. } => name,
53 }
54 }
55}
56
57#[derive(Debug)]
60pub(crate) struct ArtifactEntry {
61 bytes: Vec<u8>,
62 hash: SHA256Hash,
63 registered: AtomicBool,
64 pending_registration: Mutex<Option<PendingRegistration>>,
65}
66
67#[derive(Debug, Clone)]
69pub(crate) struct PendingRegistration {
70 pub(crate) tx_hash: TxHash,
71 pub(crate) tx_hex: String,
72 pub(crate) owner: RegistrationOwner,
73 pub(crate) effects: Vec<u8>,
74 pub(crate) address: ContractAddress,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub(crate) struct RegistrationOwner {
80 pub(crate) address: ContractAddress,
81 pub(crate) namespace: NonceNamespace,
82}
83
84impl ArtifactEntry {
85 pub(crate) fn hash(&self) -> SHA256Hash {
87 self.hash
88 }
89
90 pub(crate) fn registered(&self) -> bool {
92 self.registered.load(Ordering::Acquire)
93 }
94
95 pub(crate) fn mark_registered(&self) {
97 self.registered.store(true, Ordering::Release);
98 }
99
100 pub(crate) async fn registration_guard(&self) -> MutexGuard<'_, Option<PendingRegistration>> {
102 self.pending_registration.lock().await
103 }
104
105 pub(crate) async fn finish_registration(&self, tx_hash: TxHash, succeeded: bool) {
107 let mut pending = self.pending_registration.lock().await;
108 if pending.as_ref().map(|attempt| attempt.tx_hash) != Some(tx_hash) {
109 return;
110 }
111 if succeeded {
112 self.mark_registered();
113 }
114 *pending = None;
115 }
116
117 pub(crate) fn contract(&self) -> Contract {
119 contract_from_bytes(self.bytes.clone())
120 }
121}
122
123#[derive(Debug)]
125pub(crate) struct Registry {
126 entries: HashMap<String, Arc<ArtifactEntry>>,
127}
128
129impl Registry {
130 pub(crate) fn from_specs(specs: Vec<ArtifactSpec>) -> Result<Self> {
132 let mut entries = HashMap::with_capacity(specs.len());
133 for spec in specs {
134 let name = spec.name().to_owned();
135 if entries.contains_key(&name) {
136 return Err(TestkitError::DuplicateArtifact { name });
137 }
138 entries.insert(name, Arc::new(load_entry(spec)?));
139 }
140 Ok(Self { entries })
141 }
142
143 pub(crate) fn get(&self, name: &str) -> Result<Arc<ArtifactEntry>> {
145 self.entries
146 .get(name)
147 .map(Arc::clone)
148 .ok_or_else(|| TestkitError::UnknownArtifact {
149 name: name.to_owned(),
150 })
151 }
152}
153
154pub(crate) fn contract_from_bytes(bytes: Vec<u8>) -> Contract {
155 Contract::kRv32 {
156 rv32_contract: ContractBlob {
157 data: BytesM(bytes),
158 },
159 }
160}
161
162pub(crate) fn contract_hash(contract: &Contract) -> Result<SHA256Hash> {
163 thruput_client::contract_hash(contract).map_err(TestkitError::encode)
164}
165
166fn load_entry(spec: ArtifactSpec) -> Result<ArtifactEntry> {
167 let bytes = match spec {
168 ArtifactSpec::Bytes { bytes, .. } => bytes,
169 ArtifactSpec::Path { name, path, kind } => {
170 if kind.is_none() {
171 validate_inferred_kind(&path)?;
172 }
173 std::fs::read(&path).map_err(|source| TestkitError::ArtifactLoad {
174 name,
175 path: path.display().to_string(),
176 detail: source.to_string(),
177 })?
178 }
179 };
180 let hash = contract_hash(&contract_from_bytes(bytes.clone()))?;
181 Ok(ArtifactEntry {
182 bytes,
183 hash,
184 registered: AtomicBool::new(false),
185 pending_registration: Mutex::new(None),
186 })
187}
188
189fn validate_inferred_kind(path: &Path) -> Result<()> {
190 match path.extension().and_then(|extension| extension.to_str()) {
191 Some("rv32a") => Ok(()),
192 _ => Err(TestkitError::UnknownArtifactExtension {
193 path: path.display().to_string(),
194 }),
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use std::path::{Path, PathBuf};
201
202 use thruput_json_rpc_types::{base58::Base58, transaction::TxHash};
203 use thruput_xdr::contract::Contract;
204 use thruput_xdr::types::{ContractAddress, SHA256Hash};
205
206 use crate::nonce::NonceNamespace;
207
208 use super::{
209 ArtifactKind, ArtifactSpec, RegistrationOwner, Registry, contract_from_bytes,
210 validate_inferred_kind,
211 };
212
213 #[test]
214 fn raw_artifact_bytes_build_an_rv32_contract() {
215 let bytes = vec![1, 2, 3];
216 let contract = contract_from_bytes(bytes.clone());
217
218 match contract {
219 Contract::kRv32 { rv32_contract } => assert_eq!(rv32_contract.data.0, bytes),
220 other => panic!("expected RV32 contract, got {other:?}"),
221 }
222 }
223
224 #[test]
225 fn inferred_artifact_kind_requires_rv32_extension() {
226 assert!(validate_inferred_kind(Path::new("counter.rv32a")).is_ok());
227 assert!(validate_inferred_kind(Path::new("counter.wasm")).is_err());
228 assert!(validate_inferred_kind(Path::new("counter")).is_err());
229 }
230
231 #[cfg(unix)]
232 #[test]
233 fn inferred_artifact_kind_rejects_non_utf8_extension() {
234 use std::os::unix::ffi::OsStringExt;
235
236 let path = PathBuf::from(std::ffi::OsString::from_vec(b"counter.\xff".to_vec()));
237 assert!(validate_inferred_kind(&path).is_err());
238 }
239
240 #[test]
241 fn explicit_rv32_kind_overrides_filename_extension() {
242 let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/artifact.rs");
243 let registry = Registry::from_specs(vec![ArtifactSpec::Path {
244 name: "counter".to_owned(),
245 path,
246 kind: Some(ArtifactKind::Rv32),
247 }]);
248
249 assert!(registry.is_ok());
250 }
251
252 #[tokio::test]
253 async fn registration_resolution_only_clears_the_matching_transaction() {
254 let registry = Registry::from_specs(vec![ArtifactSpec::Bytes {
255 name: "counter".to_owned(),
256 bytes: vec![1, 2, 3],
257 }])
258 .expect("bug: in-memory artifact should load");
259 let entry = registry.get("counter").expect("bug: artifact exists");
260 let pending_hash = TxHash(Base58(SHA256Hash::from([0x11; 32])));
261 let other_hash = TxHash(Base58(SHA256Hash::from([0x22; 32])));
262 *entry.registration_guard().await = Some(super::PendingRegistration {
263 tx_hash: pending_hash,
264 tx_hex: "0x01".to_owned(),
265 effects: Vec::new(),
266 owner: RegistrationOwner {
267 address: [0; 32].into(),
268 namespace: NonceNamespace::for_run("seed", 0),
269 },
270 address: [0; 32].into(),
271 });
272
273 entry.finish_registration(other_hash, true).await;
274 assert_eq!(
275 entry
276 .registration_guard()
277 .await
278 .as_ref()
279 .map(|attempt| attempt.tx_hash),
280 Some(pending_hash)
281 );
282 assert!(!entry.registered());
283
284 entry.finish_registration(pending_hash, true).await;
285 assert!(entry.registration_guard().await.is_none());
286 assert!(entry.registered());
287 }
288
289 #[test]
290 fn registration_owner_distinguishes_repeated_runs_using_the_same_account() {
291 let address = ContractAddress::from([0x42; 32]);
292
293 assert_ne!(
294 RegistrationOwner {
295 address,
296 namespace: NonceNamespace::for_run("seed", 0),
297 },
298 RegistrationOwner {
299 address,
300 namespace: NonceNamespace::for_run("seed", 1),
301 }
302 );
303 }
304}