Skip to main content

txtx_core/std/functions/
base58.rs

1use super::{arg_checker, to_diag};
2use txtx_addon_kit::{
3    define_function, indoc,
4    types::{
5        diagnostics::Diagnostic,
6        functions::{FunctionImplementation, FunctionSpecification},
7        types::{Type, Value},
8    },
9};
10use txtx_addon_kit::types::AuthorizationContext;
11
12lazy_static! {
13    pub static ref FUNCTIONS: Vec<FunctionSpecification> = vec![
14        define_function! {
15            Base58Encode => {
16                name: "encode_base58",
17                documentation: "`encode_base58` encodes a buffer or string as a base58 string.",
18                example: indoc!{r#"
19                output "encoded" {
20                    value = encode_base58("0xaca1e2ae0c54a9a8f12da5dde27a93bb5ff94aeef722b1e474a16318234f83c8")
21                }
22                > encoded: CctJBuDbaFtojUWfQ3iEcq77eFDjojCtoS4Q59f6bUtF
23              "#},
24                inputs: [
25                    value: {
26                        documentation: "The buffer or string to encode. Strings starting with '0x' are decoded as hex; otherwise, raw UTF-8 bytes are used.",
27                        typing: vec![Type::buffer(), Type::string(), Type::addon("any")]
28                    }
29                ],
30                output: {
31                    documentation: "The input, encoded as a base58 string.",
32                    typing: Type::string()
33                },
34            }
35        },
36        define_function! {
37            Base58Decode => {
38                name: "decode_base58",
39                documentation: "`decode_base58` decodes a base58 encoded string and returns the result as a buffer.",
40                example: indoc!{r#"
41                output "decoded" {
42                    value = decode_base58("CctJBuDbaFtojUWfQ3iEcq77eFDjojCtoS4Q59f6bUtF")
43                }
44                > decoded: 0xaca1e2ae0c54a9a8f12da5dde27a93bb5ff94aeef722b1e474a16318234f83c8
45              "#},
46                inputs: [
47                    base58_string: {
48                        documentation: "The base58 string to decode.",
49                        typing: vec![Type::string()]
50                    }
51                ],
52                output: {
53                    documentation: "The decoded base58 string, as a buffer.",
54                    typing: Type::buffer()
55                },
56            }
57        },
58    ];
59}
60
61/// Helper to get bytes from a Value for encoding functions.
62/// - Buffer: use bytes directly
63/// - String with "0x" prefix: decode as hex
64/// - String without "0x" prefix: use raw UTF-8 bytes
65/// - Addon: use addon bytes
66fn get_bytes_for_encoding(value: &Value) -> Result<Vec<u8>, Diagnostic> {
67    match value {
68        Value::Buffer(b) => Ok(b.clone()),
69        Value::String(s) => {
70            if s.starts_with("0x") {
71                txtx_addon_kit::hex::decode(&s[2..]).map_err(|e| {
72                    Diagnostic::error_from_string(format!("failed to decode hex string: {}", e))
73                })
74            } else {
75                Ok(s.as_bytes().to_vec())
76            }
77        }
78        Value::Addon(addon) => Ok(addon.bytes.clone()),
79        _ => Err(Diagnostic::error_from_string(
80            "expected a buffer, string, or addon value".to_string(),
81        )),
82    }
83}
84
85pub struct Base58Encode;
86impl FunctionImplementation for Base58Encode {
87    fn check_instantiability(
88        _fn_spec: &FunctionSpecification,
89        _auth_ctx: &AuthorizationContext,
90        _args: &Vec<Type>,
91    ) -> Result<Type, Diagnostic> {
92        unimplemented!()
93    }
94
95    fn run(
96        fn_spec: &FunctionSpecification,
97        _auth_ctx: &AuthorizationContext,
98        args: &Vec<Value>,
99    ) -> Result<Value, Diagnostic> {
100        arg_checker(fn_spec, args)?;
101        let bytes = get_bytes_for_encoding(args.get(0).unwrap())?;
102        let encoded = bs58::encode(bytes).into_string();
103        Ok(Value::string(encoded))
104    }
105}
106
107pub struct Base58Decode;
108impl FunctionImplementation for Base58Decode {
109    fn check_instantiability(
110        _fn_spec: &FunctionSpecification,
111        _auth_ctx: &AuthorizationContext,
112        _args: &Vec<Type>,
113    ) -> Result<Type, Diagnostic> {
114        unimplemented!()
115    }
116
117    fn run(
118        fn_spec: &FunctionSpecification,
119        _auth_ctx: &AuthorizationContext,
120        args: &Vec<Value>,
121    ) -> Result<Value, Diagnostic> {
122        arg_checker(fn_spec, args)?;
123        let encoded = args.get(0).unwrap().expect_string();
124
125        let decoded = bs58::decode(encoded)
126            .into_vec()
127            .map_err(|e| to_diag(fn_spec, format!("failed to decode base58 string {}: {}", encoded, e)))?;
128
129        Ok(Value::buffer(decoded))
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use test_case::test_case;
136    use txtx_addon_kit::helpers::fs::FileLocation;
137    use txtx_addon_kit::hex as kit_hex;
138
139    use super::*;
140
141    fn get_spec_by_name(name: &str) -> FunctionSpecification {
142        FUNCTIONS.iter().find(|f| f.name == name).cloned().unwrap()
143    }
144
145    fn dummy_auth_ctx() -> AuthorizationContext {
146        AuthorizationContext { workspace_location: FileLocation::working_dir() }
147    }
148
149    fn hex_to_buffer(hex: &str) -> Value {
150        Value::buffer(kit_hex::decode(hex).unwrap())
151    }
152
153    #[test_case(
154        hex_to_buffer("aca1e2ae0c54a9a8f12da5dde27a93bb5ff94aeef722b1e474a16318234f83c8"),
155        Value::string("CctJBuDbaFtojUWfQ3iEcq77eFDjojCtoS4Q59f6bUtF".to_string());
156        "buffer 32 bytes"
157    )]
158    #[test_case(
159        Value::string("0xaca1e2ae0c54a9a8f12da5dde27a93bb5ff94aeef722b1e474a16318234f83c8".to_string()),
160        Value::string("CctJBuDbaFtojUWfQ3iEcq77eFDjojCtoS4Q59f6bUtF".to_string());
161        "hex string 32 bytes"
162    )]
163    #[test_case(
164        Value::string("hello".to_string()),
165        Value::string("Cn8eVZg".to_string());
166        "plain string hello"
167    )]
168    #[test_case(
169        Value::string("__event_authority".to_string()),
170        Value::string("tyvCZETMWX6hYsUwTchxRWG".to_string());
171        "plain string with underscores"
172    )]
173    #[test_case(
174        Value::buffer(vec![0]),
175        Value::string("1".to_string());
176        "single zero byte"
177    )]
178    #[test_case(
179        Value::buffer(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
180        Value::string("8DfbjXLth7APvt3qQPgtf".to_string());
181        "sequential bytes"
182    )]
183    #[test_case(
184        Value::buffer(vec![0, 0, 0, 1]),
185        Value::string("1112".to_string());
186        "leading zeros preserved"
187    )]
188    fn test_base58_encode_decode_roundtrip(input: Value, expected_encoded: Value) {
189        let encode_spec = get_spec_by_name("encode_base58");
190        let decode_spec = get_spec_by_name("decode_base58");
191        let auth_ctx = dummy_auth_ctx();
192
193        // Encode the input and verify it matches expected
194        let encoded = (encode_spec.runner)(&encode_spec, &auth_ctx, &vec![input.clone()]).unwrap();
195        assert_eq!(encoded, expected_encoded, "encoded value mismatch");
196
197        // Decode the result and verify we get back the original bytes
198        let decoded = (decode_spec.runner)(&decode_spec, &auth_ctx, &vec![encoded]).unwrap();
199
200        // Get expected bytes based on input type
201        let expected_bytes = match &input {
202            Value::Buffer(b) => b.clone(),
203            Value::String(s) if s.starts_with("0x") => {
204                kit_hex::decode(&s[2..]).unwrap()
205            }
206            Value::String(s) => s.as_bytes().to_vec(),
207            _ => unreachable!(),
208        };
209        assert_eq!(decoded, Value::buffer(expected_bytes), "decoded value mismatch");
210    }
211
212    #[test]
213    fn test_decode_base58_invalid_input() {
214        let fn_spec = get_spec_by_name("decode_base58");
215        // "0", "O", "I", "l" are not valid in base58
216        let args = vec![Value::string("0OIl".to_string())];
217        let result = (fn_spec.runner)(&fn_spec, &dummy_auth_ctx(), &args);
218        assert!(result.is_err());
219    }
220}