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 _ => 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 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 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 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 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 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}