tx3_bindgen/
typescript.rs

1use convert_case::{Case, Casing};
2use std::{collections::HashMap, rc::Rc};
3use swc_common::{SyntaxContext, DUMMY_SP};
4use swc_core::{self, atoms::Atom};
5use swc_ecma_ast::*;
6
7use super::Job;
8
9fn ts_type_for_field(ty: &tx3_lang::ir::Type) -> TsType {
10    match ty {
11        tx3_lang::ir::Type::Int => TsType::TsKeywordType(TsKeywordType {
12            span: DUMMY_SP,
13            kind: TsKeywordTypeKind::TsNumberKeyword,
14        }),
15        tx3_lang::ir::Type::Address => TsType::TsKeywordType(TsKeywordType {
16            span: DUMMY_SP,
17            kind: TsKeywordTypeKind::TsStringKeyword,
18        }),
19        tx3_lang::ir::Type::Bool => TsType::TsKeywordType(TsKeywordType {
20            span: DUMMY_SP,
21            kind: TsKeywordTypeKind::TsBooleanKeyword,
22        }),
23        tx3_lang::ir::Type::Bytes => TsType::TsTypeRef(TsTypeRef {
24            span: DUMMY_SP,
25            type_name: TsEntityName::Ident(Ident::new_no_ctxt("Uint8Array".into(), DUMMY_SP)),
26            type_params: None,
27        }),
28        tx3_lang::ir::Type::UtxoRef => TsType::TsKeywordType(TsKeywordType {
29            span: DUMMY_SP,
30            kind: TsKeywordTypeKind::TsStringKeyword,
31        }),
32        // Add other type mappings as needed
33        _ => TsType::TsKeywordType(TsKeywordType {
34            span: DUMMY_SP,
35            kind: TsKeywordTypeKind::TsUnknownKeyword,
36        }),
37    }
38}
39
40fn map_into_object_lit(headers: &HashMap<String, String>) -> Expr {
41    Expr::Object(ObjectLit {
42        span: DUMMY_SP,
43        props: headers
44            .iter()
45            .map(|(key, value)| {
46                PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
47                    key: PropName::Str(Str::from(key.clone())),
48                    value: Box::new(Expr::Lit(Lit::Str(Str::from(value.clone())))),
49                })))
50            })
51            .collect(),
52    })
53}
54
55pub fn generate(job: &Job) {
56    let mut module_items = Vec::new();
57
58    module_items.push(swc_ecma_quote::quote!(
59        "import { TRPClient, TirEnvelope, TxEnvelope, ClientOptions } from 'tx3-trp';"
60            as ModuleItem
61    ));
62
63    module_items.push(swc_ecma_quote::quote!(
64        "export const DEFAULT_TRP_ENDPOINT = $value;" as ModuleItem,
65        value: Expr = job.trp_endpoint.clone().into(),
66    ));
67
68    let headers_lit = map_into_object_lit(&job.trp_headers);
69
70    let headers_item = swc_ecma_quote::quote!(
71        "export const DEFAULT_HEADERS = $value;" as ModuleItem,
72        value: Expr = headers_lit,
73    );
74
75    module_items.push(headers_item);
76
77    let env_args_lit = map_into_object_lit(&job.env_args);
78
79    let env_args_item = swc_ecma_quote::quote!(
80        "export const DEFAULT_ENV_ARGS = $value;" as ModuleItem,
81        value: Expr = env_args_lit,
82    );
83
84    module_items.push(env_args_item);
85
86    let mut class_members = Vec::new();
87
88    // Add private client field
89    class_members.push(ClassMember::PrivateProp(PrivateProp {
90        key: PrivateName {
91            name: Atom::from("client"),
92            ..Default::default()
93        },
94        type_ann: Some(Box::new(TsTypeAnn {
95            span: DUMMY_SP,
96            type_ann: Box::new(TsType::TsTypeRef(TsTypeRef {
97                span: DUMMY_SP,
98                type_name: TsEntityName::Ident(Ident::new_no_ctxt("TRPClient".into(), DUMMY_SP)),
99                type_params: None,
100            })),
101        })),
102        readonly: true,
103        ..Default::default()
104    }));
105
106    // Add constructor
107    class_members.push(ClassMember::Constructor(Constructor {
108        span: DUMMY_SP,
109        key: PropName::Ident(IdentName::new(Atom::from("constructor"), DUMMY_SP)),
110        params: vec![ParamOrTsParamProp::Param(Param {
111            span: DUMMY_SP,
112            decorators: vec![],
113            pat: Pat::Ident(BindingIdent {
114                id: Ident::new_no_ctxt("options".into(), DUMMY_SP),
115                type_ann: Some(Box::new(TsTypeAnn {
116                    span: DUMMY_SP,
117                    type_ann: Box::new(TsType::TsTypeRef(TsTypeRef {
118                        span: DUMMY_SP,
119                        type_name: TsEntityName::Ident(Ident::new_no_ctxt(
120                            "ClientOptions".into(),
121                            DUMMY_SP,
122                        )),
123                        type_params: None,
124                    })),
125                })),
126            }),
127        })],
128        body: Some(BlockStmt {
129            span: DUMMY_SP,
130            stmts: vec![swc_ecma_quote::quote!(
131                "this.#client = new TRPClient(options);" as Stmt
132            )],
133            ctxt: SyntaxContext::empty(),
134        }),
135        ..Default::default()
136    }));
137
138    for tx_def in job.protocol.txs() {
139        let tx_name = tx_def.name.as_str();
140        let prototx = job.protocol.new_tx(&tx_def.name).unwrap();
141
142        let ir_bytes_hex = hex::encode(prototx.ir_bytes());
143
144        let type_name = format!("{}Params", tx_name.to_case(Case::Pascal));
145        let fn_name = format!("{}Tx", tx_name.to_case(Case::Camel));
146        let ir_const_name = format!("{}_IR", tx_name.to_case(Case::Constant));
147
148        let type_members = prototx
149            .find_params()
150            .iter()
151            .map(|(key, type_)| {
152                let field_name = key.as_str().to_case(Case::Camel);
153
154                TsTypeElement::TsPropertySignature(TsPropertySignature {
155                    span: DUMMY_SP,
156                    readonly: false,
157                    key: Box::new(Expr::Ident(Ident::new_no_ctxt(field_name.into(), DUMMY_SP))),
158                    computed: false,
159                    optional: false,
160                    type_ann: Box::new(TsTypeAnn {
161                        span: DUMMY_SP,
162                        type_ann: ts_type_for_field(type_).into(),
163                    })
164                    .into(),
165                })
166            })
167            .collect();
168
169        module_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
170            decl: Decl::TsTypeAlias(Box::new(TsTypeAliasDecl {
171                span: DUMMY_SP,
172                declare: false,
173                id: Ident::new_no_ctxt(type_name.clone().into(), DUMMY_SP),
174                type_params: None,
175                type_ann: Box::new(TsType::TsTypeLit(TsTypeLit {
176                    span: DUMMY_SP,
177                    members: type_members,
178                })),
179            })),
180            span: DUMMY_SP,
181        })));
182
183        let ir_define_stmt = swc_ecma_quote::quote!(
184            "export const $ir_const_name = { bytecode: $ir_bytes, encoding: 'hex', version: 'v1alpha1' };" as ModuleItem,
185            ir_const_name =  Ident::new_no_ctxt(ir_const_name.clone().into(), DUMMY_SP),
186            ir_bytes: Expr = ir_bytes_hex.into(),
187        );
188
189        module_items.push(ir_define_stmt);
190
191        // Add method to class
192        let method = ClassMethod {
193            kind: MethodKind::Method,
194            span: DUMMY_SP,
195            key: PropName::Ident(IdentName::new(Atom::from(fn_name), DUMMY_SP)),
196            function: Box::new(Function {
197                params: vec![Param {
198                    span: DUMMY_SP,
199                    decorators: vec![],
200                    pat: Pat::Ident(BindingIdent {
201                        id: Ident::new_no_ctxt("args".into(), DUMMY_SP),
202                        type_ann: Some(Box::new(TsTypeAnn {
203                            span: DUMMY_SP,
204                            type_ann: Box::new(TsType::TsTypeRef(TsTypeRef {
205                                span: DUMMY_SP,
206                                type_name: TsEntityName::Ident(Ident::new_no_ctxt(
207                                    type_name.into(),
208                                    DUMMY_SP,
209                                )),
210                                type_params: None,
211                            })),
212                        })),
213                    }),
214                }],
215                decorators: vec![],
216                span: DUMMY_SP,
217                body: Some(BlockStmt {
218                    span: DUMMY_SP,
219                    stmts: vec![swc_ecma_quote::quote!(
220                        "return await this.#client.resolve({
221                            tir: $ir_const_name,
222                            args,
223                        });" as Stmt,
224                        ir_const_name = Ident::new_no_ctxt(ir_const_name.clone().into(), DUMMY_SP),
225                    )],
226                    ctxt: SyntaxContext::empty(),
227                }),
228                is_generator: false,
229                is_async: true,
230                type_params: None,
231                return_type: Some(Box::new(TsTypeAnn {
232                    span: DUMMY_SP,
233                    type_ann: Box::new(TsType::TsTypeRef(TsTypeRef {
234                        span: DUMMY_SP,
235                        type_name: TsEntityName::Ident(Ident::new_no_ctxt(
236                            "Promise<TxEnvelope>".into(),
237                            DUMMY_SP,
238                        )),
239                        type_params: None,
240                    })),
241                })),
242                ctxt: SyntaxContext::empty(),
243            }),
244            ..Default::default()
245        };
246
247        class_members.push(ClassMember::Method(method));
248    }
249
250    // Create and export the Client class
251    module_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
252        span: DUMMY_SP,
253        decl: Decl::Class(ClassDecl {
254            ident: Ident::new_no_ctxt("Client".into(), DUMMY_SP),
255            declare: false,
256            class: Box::new(Class {
257                body: class_members,
258                ..Default::default()
259            }),
260        }),
261    })));
262
263    module_items.push(swc_ecma_quote::quote!(
264        "export const protocol = new Client({
265            endpoint: DEFAULT_TRP_ENDPOINT,
266            headers: DEFAULT_HEADERS,
267            envArgs: DEFAULT_ENV_ARGS,
268        });" as ModuleItem
269    ));
270
271    let module = Module {
272        span: DUMMY_SP,
273        body: module_items,
274        shebang: None,
275    };
276
277    // Use swc_ecma_codegen to generate the final TypeScript code
278    let mut buf = vec![];
279
280    let writer = swc_ecma_codegen::text_writer::JsWriter::new(
281        Rc::new(swc_common::SourceMap::default()),
282        "\n",
283        &mut buf,
284        None,
285    );
286
287    let config = swc_ecma_codegen::Config::default();
288
289    let mut emitter = swc_ecma_codegen::Emitter {
290        cfg: config,
291        comments: None,
292        cm: Rc::new(swc_common::SourceMap::default()),
293        wr: writer,
294    };
295
296    emitter.emit_module(&module).unwrap();
297
298    std::fs::write(job.dest_path.join(format!("{}.ts", job.name)), buf)
299        .expect("Failed to write TypeScript output");
300}