Skip to main content

taproot_assets_rpc/
convert.rs

1use crate::taprpc;
2use taproot_assets_types as types;
3
4use bitcoin::hashes::{Hash, sha256::Hash as Sha256Hash};
5use bitcoin::{BlockHash, OutPoint, Witness};
6use std::convert::{TryFrom, TryInto};
7use std::str::FromStr;
8
9#[derive(Debug)]
10pub enum ConversionError {
11    InvalidEnumValue(String),
12    MissingField(String), // Kept for future use, not used in current impl
13    InvalidStringFormat(String),
14    InvalidHashBytes(String),
15    InvalidWitnessData(String), // Kept for future use
16    RecursiveError(Box<ConversionError>),
17    Other(String), // Generic fallback
18}
19
20pub type Result<T> = std::result::Result<T, ConversionError>;
21
22impl std::fmt::Display for ConversionError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            ConversionError::InvalidEnumValue(s) => write!(f, "Invalid enum value: {}", s),
26            ConversionError::MissingField(s) => write!(f, "Missing field: {}", s),
27            ConversionError::InvalidStringFormat(s) => write!(f, "Invalid string format: {}", s),
28            ConversionError::InvalidHashBytes(s) => write!(f, "Invalid hash bytes: {}", s),
29            ConversionError::InvalidWitnessData(s) => write!(f, "Invalid witness data: {}", s),
30            ConversionError::RecursiveError(e) => write!(f, "Recursive conversion error: {}", e),
31            ConversionError::Other(s) => write!(f, "Conversion error: {}", s),
32        }
33    }
34}
35
36impl std::error::Error for ConversionError {
37    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
38        match self {
39            ConversionError::RecursiveError(e) => Some(e.as_ref()),
40            _ => None,
41        }
42    }
43}
44
45impl From<bitcoin::hashes::FromSliceError> for ConversionError {
46    fn from(e: bitcoin::hashes::FromSliceError) -> Self {
47        ConversionError::InvalidHashBytes(format!("Failed to convert from slice: {}", e))
48    }
49}
50
51impl From<bitcoin::consensus::encode::Error> for ConversionError {
52    fn from(e: bitcoin::consensus::encode::Error) -> Self {
53        ConversionError::Other(format!("Bitcoin consensus decoding error: {}", e))
54    }
55}
56
57// Helper for hash conversion from Vec<u8> to Sha256Hash
58fn vec_to_sha256hash(bytes: Vec<u8>, field_name: &str) -> Result<Sha256Hash> {
59    Sha256Hash::from_slice(&bytes).map_err(|err| {
60        ConversionError::InvalidHashBytes(format!(
61            "Invalid {} hash bytes (expected 32 for {}): {}, (got {} bytes)",
62            field_name,
63            field_name,
64            err,
65            bytes.len()
66        ))
67    })
68}
69
70// Helper to parse OutPoint from string "txid:vout"
71fn string_to_outpoint(s: String, field_name: &str) -> Result<OutPoint> {
72    OutPoint::from_str(&s).map_err(|e| {
73        ConversionError::InvalidStringFormat(format!(
74            "Failed to parse {} from '{}': {}",
75            field_name, s, e
76        ))
77    })
78}
79
80// Helper to parse BlockHash from string
81fn string_to_blockhash(s: String, field_name: &str) -> Result<BlockHash> {
82    BlockHash::from_str(&s).map_err(|e| {
83        ConversionError::InvalidStringFormat(format!(
84            "Failed to parse {} from '{}': {}",
85            field_name, s, e
86        ))
87    })
88}
89
90// Helper for Option<F> -> Result<Option<R>, E> conversion
91fn try_option<F, R>(opt_f: Option<F>) -> Result<Option<R>>
92where
93    F: TryInto<R, Error = ConversionError>,
94{
95    opt_f
96        .map(TryInto::try_into)
97        .transpose()
98        .map_err(|err| ConversionError::RecursiveError(Box::new(err)))
99}
100
101// Helper for Vec<F> -> Result<Vec<R>, E> conversion
102fn try_vec<F, R>(vec_f: Vec<F>) -> Result<Vec<R>>
103where
104    F: TryInto<R, Error = ConversionError>,
105{
106    vec_f
107        .into_iter()
108        .map(TryInto::try_into)
109        .collect::<std::result::Result<Vec<R>, ConversionError>>()
110        .map_err(|err| ConversionError::RecursiveError(Box::new(err)))
111}
112
113// --- Struct Conversions ---
114
115impl TryFrom<taprpc::DecimalDisplay> for types::asset::DecimalDisplay {
116    type Error = ConversionError;
117
118    fn try_from(value: taprpc::DecimalDisplay) -> Result<Self> {
119        Ok(types::asset::DecimalDisplay {
120            decimal_display: value.decimal_display,
121        })
122    }
123}
124
125impl TryFrom<taprpc::GenesisInfo> for types::asset::GenesisInfo {
126    type Error = ConversionError;
127
128    fn try_from(value: taprpc::GenesisInfo) -> Result<Self> {
129        Ok(types::asset::GenesisInfo {
130            genesis_point: string_to_outpoint(value.genesis_point, "GenesisInfo.genesis_point")?,
131            name: value.name,
132            meta_hash: vec_to_sha256hash(value.meta_hash, "GenesisInfo.meta_hash")?,
133            asset_id: vec_to_sha256hash(value.asset_id, "GenesisInfo.asset_id")?,
134            asset_type: types::asset::AssetType::try_from(value.asset_type).map_err(|e| {
135                ConversionError::InvalidEnumValue(format!(
136                    "GenesisInfo.asset_type (value: {}): {}",
137                    value.asset_type, e
138                ))
139            })?,
140            output_index: value.output_index,
141        })
142    }
143}
144
145impl TryFrom<taprpc::AssetGroup> for types::asset::AssetGroup {
146    type Error = ConversionError;
147
148    fn try_from(value: taprpc::AssetGroup) -> Result<Self> {
149        let tapscript_root = if value.tapscript_root.is_empty() {
150            None
151        } else {
152            Some(vec_to_sha256hash(
153                value.tapscript_root,
154                "AssetGroup.tapscript_root",
155            )?)
156        };
157
158        Ok(types::asset::AssetGroup {
159            raw_group_key: value.raw_group_key,
160            tweaked_group_key: value.tweaked_group_key,
161            asset_witness: value.asset_witness,
162            tapscript_root: tapscript_root,
163        })
164    }
165}
166
167impl TryFrom<taprpc::AnchorInfo> for types::asset::AnchorInfo {
168    type Error = ConversionError;
169
170    fn try_from(value: taprpc::AnchorInfo) -> Result<Self> {
171        let tx = bitcoin::consensus::encode::deserialize(&value.anchor_tx)?;
172
173        Ok(types::asset::AnchorInfo {
174            anchor_tx: tx,
175            anchor_block_hash: string_to_blockhash(
176                value.anchor_block_hash,
177                "AnchorInfo.anchor_block_hash",
178            )?,
179            anchor_outpoint: string_to_outpoint(
180                value.anchor_outpoint,
181                "AnchorInfo.anchor_outpoint",
182            )?,
183            internal_key: value.internal_key,
184            merkle_root: vec_to_sha256hash(value.merkle_root, "AnchorInfo.merkle_root")?,
185            tapscript_sibling: value.tapscript_sibling,
186            block_height: value.block_height,
187            block_timestamp: value.block_timestamp,
188        })
189    }
190}
191
192impl TryFrom<taprpc::PrevInputAsset> for types::asset::PrevInputAsset {
193    type Error = ConversionError;
194
195    fn try_from(value: taprpc::PrevInputAsset) -> Result<Self> {
196        Ok(types::asset::PrevInputAsset {
197            anchor_point: string_to_outpoint(value.anchor_point, "PrevInputAsset.anchor_point")?,
198            asset_id: vec_to_sha256hash(value.asset_id, "PrevInputAsset.asset_id")?,
199            script_key: value.script_key,
200            amount: value.amount,
201        })
202    }
203}
204
205impl TryFrom<taprpc::Asset> for types::asset::Asset {
206    type Error = ConversionError;
207
208    fn try_from(value: taprpc::Asset) -> Result<Self> {
209        Ok(types::asset::Asset {
210            version: types::asset::AssetVersion::try_from(value.version).map_err(|e| {
211                ConversionError::InvalidEnumValue(format!(
212                    "Asset.version (value: {}): {}",
213                    value.version, e
214                ))
215            })?,
216            asset_genesis: try_option(value.asset_genesis)?,
217            amount: value.amount,
218            lock_time: value.lock_time,
219            relative_lock_time: value.relative_lock_time,
220            script_version: value.script_version,
221            script_key: value.script_key,
222            script_key_is_local: value.script_key_is_local,
223            asset_group: try_option(value.asset_group)?,
224            chain_anchor: try_option(value.chain_anchor)?,
225            prev_witnesses: try_vec(value.prev_witnesses)?,
226            split_commitment_root: None,
227            is_spent: value.is_spent,
228            lease_owner: value.lease_owner,
229            lease_expiry: value.lease_expiry,
230            is_burn: value.is_burn,
231            script_key_declared_known: value.script_key_declared_known,
232            script_key_has_script_path: value.script_key_has_script_path,
233            decimal_display: try_option(value.decimal_display)?,
234            script_key_type: types::asset::ScriptKeyType::try_from(value.script_key_type).map_err(
235                |e| {
236                    ConversionError::InvalidEnumValue(format!(
237                        "Asset.script_key_type (value: {}): {}",
238                        value.script_key_type, e
239                    ))
240                },
241            )?,
242        })
243    }
244}
245
246impl TryFrom<taprpc::SplitCommitment> for types::asset::SplitCommitment {
247    type Error = ConversionError;
248
249    fn try_from(value: taprpc::SplitCommitment) -> Result<Self> {
250        let rpc_asset = value.root_asset.ok_or_else(|| {
251            ConversionError::MissingField("SplitCommitment.root_asset".to_string())
252        })?;
253        let domain_asset: types::asset::Asset = rpc_asset
254            .try_into()
255            .map_err(|e: ConversionError| ConversionError::RecursiveError(Box::new(e)))?;
256        let proof = types::mssmt::MssmtProof { nodes: Vec::new() };
257        Ok(types::asset::SplitCommitment {
258            proof,
259            root_asset: Box::new(domain_asset),
260        })
261    }
262}
263
264impl TryFrom<taprpc::PrevWitness> for types::asset::PrevWitness {
265    type Error = ConversionError;
266
267    fn try_from(value: taprpc::PrevWitness) -> Result<Self> {
268        let prev_id = value
269            .prev_id
270            .map(|prev| {
271                let out_point =
272                    string_to_outpoint(prev.anchor_point, "PrevInputAsset.anchor_point")?;
273                let asset_id = vec_to_sha256hash(prev.asset_id, "PrevInputAsset.asset_id")?;
274                if prev.script_key.len() != 33 {
275                    return Err(ConversionError::InvalidHashBytes(format!(
276                        "Invalid PrevInputAsset.script_key length: {}",
277                        prev.script_key.len()
278                    )));
279                }
280                let mut key_bytes = [0u8; 33];
281                key_bytes.copy_from_slice(&prev.script_key);
282                Ok(types::asset::PrevId {
283                    out_point,
284                    asset_id,
285                    script_key: types::asset::SerializedKey { bytes: key_bytes },
286                })
287            })
288            .transpose()?;
289
290        Ok(types::asset::PrevWitness {
291            prev_id,
292            tx_witness: Witness::from(value.tx_witness),
293            split_commitment: try_option(value.split_commitment)?,
294        })
295    }
296}
297
298impl TryFrom<taprpc::ListAssetResponse> for crate::taprpc::types::ListAssetsResponse {
299    type Error = ConversionError;
300
301    fn try_from(value: taprpc::ListAssetResponse) -> Result<Self> {
302        let assets = value
303            .assets
304            .into_iter()
305            .map(types::asset::Asset::try_from)
306            .collect::<Result<Vec<types::asset::Asset>>>()
307            .map_err(|e| ConversionError::RecursiveError(Box::new(e)))?;
308
309        Ok(crate::taprpc::types::ListAssetsResponse {
310            assets,
311            unconfirmed_transfers: value.unconfirmed_transfers,
312            unconfirmed_mints: value.unconfirmed_mints,
313        })
314    }
315}
316
317impl TryFrom<taprpc::ProofFile> for crate::taprpc::types::ExportProofResponse {
318    type Error = ConversionError;
319
320    fn try_from(value: taprpc::ProofFile) -> Result<Self> {
321        let genesis_point = if value.genesis_point.is_empty() {
322            None
323        } else {
324            Some(string_to_outpoint(
325                value.genesis_point,
326                "ProofFile.genesis_point",
327            )?)
328        };
329
330        Ok(crate::taprpc::types::ExportProofResponse {
331            raw_proof_file: value.raw_proof_file,
332            genesis_point: genesis_point,
333        })
334    }
335}
336
337impl TryFrom<taprpc::VerifyProofResponse> for crate::taprpc::types::VerifyProofResponse {
338    type Error = ConversionError;
339
340    fn try_from(value: taprpc::VerifyProofResponse) -> Result<Self> {
341        Ok(crate::taprpc::types::VerifyProofResponse {
342            valid: value.valid,
343            decoded_proof: try_option(value.decoded_proof)?,
344        })
345    }
346}
347
348// --- Proof Conversion ---
349
350impl TryFrom<taprpc::AssetMeta> for types::asset::AssetMeta {
351    type Error = ConversionError;
352
353    fn try_from(value: taprpc::AssetMeta) -> Result<Self> {
354        Ok(types::asset::AssetMeta {
355            data: value.data,
356            meta_type: types::asset::AssetMetaType::try_from(value.r#type).map_err(|e| {
357                ConversionError::InvalidEnumValue(format!(
358                    "AssetMeta.type (value: {}): {}",
359                    value.r#type, e
360                ))
361            })?,
362        })
363    }
364}
365
366// --- Fixed GenesisReveal conversion ---
367impl TryFrom<taprpc::GenesisReveal> for types::asset::GenesisReveal {
368    type Error = ConversionError;
369
370    fn try_from(value: taprpc::GenesisReveal) -> Result<Self> {
371        // The taprpc::GenesisReveal only contains genesis_base_reveal, but
372        // types::asset::GenesisReveal requires additional fields. Since these
373        // aren't available in the RPC structure, we use reasonable defaults.
374        Ok(types::asset::GenesisReveal {
375            genesis_base: try_option(value.genesis_base_reveal)?,
376            asset_type: types::asset::AssetType::Normal, // Default to Normal type
377            amount: 0,         // Default amount since not available in RPC
378            meta_reveal: None, // No meta reveal available in RPC
379        })
380    }
381}
382
383impl TryFrom<taprpc::GroupKeyReveal> for types::asset::GroupKeyReveal {
384    type Error = ConversionError;
385
386    fn try_from(value: taprpc::GroupKeyReveal) -> Result<Self> {
387        if value.raw_group_key.len() != 33 {
388            return Err(ConversionError::InvalidHashBytes(format!(
389                "Invalid GroupKeyReveal.raw_group_key length: {}",
390                value.raw_group_key.len()
391            )));
392        }
393        let mut key_bytes = [0u8; 33];
394        key_bytes.copy_from_slice(&value.raw_group_key);
395        let tapscript_root = if value.tapscript_root.is_empty() {
396            None
397        } else {
398            Some(vec_to_sha256hash(
399                value.tapscript_root,
400                "GroupKeyReveal.tapscript_root",
401            )?)
402        };
403
404        Ok(types::asset::GroupKeyReveal {
405            raw_group_key: types::asset::SerializedKey { bytes: key_bytes },
406            tapscript_root,
407            version: None,
408            custom_subtree_root: None,
409        })
410    }
411}
412
413impl TryFrom<taprpc::DecodedProof> for crate::taprpc::types::DecodedProof {
414    type Error = ConversionError;
415
416    fn try_from(value: taprpc::DecodedProof) -> Result<Self> {
417        let challenge_witness = if value.challenge_witness.is_empty() {
418            None
419        } else {
420            Some(Witness::from(value.challenge_witness))
421        };
422
423        let inclusion_proof = types::proof::TaprootProof::from_bytes(&value.inclusion_proof)
424            .map_err(|e| {
425                ConversionError::Other(format!("Failed to decode inclusion proof: {}", e))
426            })?;
427
428        let exclusion_proofs = value
429            .exclusion_proofs
430            .into_iter()
431            .map(|proof_bytes| {
432                types::proof::TaprootProof::from_bytes(&proof_bytes).map_err(|e| {
433                    ConversionError::Other(format!("Failed to decode exclusion proof: {}", e))
434                })
435            })
436            .collect::<Result<Vec<types::proof::TaprootProof>>>()?;
437
438        let split_root_proof = if value.split_root_proof.is_empty() {
439            None
440        } else {
441            Some(
442                types::proof::TaprootProof::from_bytes(&value.split_root_proof).map_err(|e| {
443                    ConversionError::Other(format!("Failed to decode split root proof: {}", e))
444                })?,
445            )
446        };
447
448        let tx_merkle_proof = types::proof::TxMerkleProof::from_bytes(&value.tx_merkle_proof)
449            .map_err(|e| {
450                ConversionError::Other(format!("Failed to decode tx merkle proof: {}", e))
451            })?;
452
453        Ok(crate::taprpc::types::DecodedProof {
454            proof_at_depth: value.proof_at_depth,
455            number_of_proofs: value.number_of_proofs,
456            asset: value
457                .asset
458                .ok_or_else(|| ConversionError::MissingField("DecodedProof.asset".to_string()))?
459                .try_into()?,
460            meta_reveal: try_option(value.meta_reveal)?,
461            tx_merkle_proof,
462            inclusion_proof,
463            exclusion_proofs,
464            split_root_proof,
465            num_additional_inputs: value.num_additional_inputs,
466            challenge_witness,
467            is_burn: value.is_burn,
468            genesis_reveal: try_option(value.genesis_reveal)?,
469            group_key_reveal: try_option(value.group_key_reveal)?,
470        })
471    }
472}
473
474// --- Commenting out TxMerkleProof conversion due to field mismatch ---
475/*
476impl TryFrom<taprpc::TxMerkleProof> for types::proof::TxMerkleProof {
477    type Error = ConversionError;
478
479    fn try_from(value: taprpc::TxMerkleProof) -> Result<Self> {
480        // The `taprpc::TxMerkleProof` does not contain the `bits` field that is
481        // part of `types::proof::TxMerkleProof`. The `bits` are used to
482        // determine the side of the sibling hash in the merkle tree.
483        // Without this information, we cannot fully construct the proof.
484        // The `block_header` and `tx_index` are also not used here.
485        // This suggests a potential mismatch between the rpc and type definitions.
486        let nodes = value
487            .merkle_nodes
488            .into_iter()
489            .map(|node_bytes| {
490                bitcoin::TxMerkleNode::from_slice(&node_bytes).map_err(|e| {
491                    ConversionError::InvalidHashBytes(format!(
492                        "Invalid TxMerkleNode hash bytes: {}",
493                        e
494                    ))
495                })
496            })
497            .collect::<Result<Vec<bitcoin::TxMerkleNode>>>()?;
498
499        Ok(types::proof::TxMerkleProof {
500            nodes,
501            bits: vec![], // Bits are not available in taprpc::TxMerkleProof
502        })
503    }
504}
505*/