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
61fn 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 let encoded = (encode_spec.runner)(&encode_spec, &auth_ctx, &vec![input.clone()]).unwrap();
195 assert_eq!(encoded, expected_encoded, "encoded value mismatch");
196
197 let decoded = (decode_spec.runner)(&decode_spec, &auth_ctx, &vec![encoded]).unwrap();
199
200 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 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}