Skip to main content

unc_abi_client_impl/
lib.rs

1use unc_abi::{AbiFunctionKind, AbiParameters, AbiRoot, AbiType};
2use near_schemafy_lib::{Expander, Generator, Schema};
3use quote::{format_ident, quote};
4use std::path::{Path, PathBuf};
5
6pub fn generate_abi_client(
7    unc_abi: AbiRoot,
8    contract_name: proc_macro2::Ident,
9) -> proc_macro2::TokenStream {
10    let schema_json = serde_json::to_string(&unc_abi.body.root_schema).unwrap();
11
12    let generator = Generator::builder().with_input_json(&schema_json).build();
13    let (mut token_stream, schema) = generator.generate_with_schema();
14    let mut expander = Expander::new(None, "", &schema);
15
16    token_stream.extend(quote! {
17        pub struct #contract_name {
18            pub contract: utility_workspaces::Contract,
19        }
20    });
21
22    let mut methods_stream = proc_macro2::TokenStream::new();
23    for function in unc_abi.body.functions {
24        let name = format_ident!("{}", function.name);
25
26        let mut param_names = vec![];
27        let params = match &function.params {
28            AbiParameters::Borsh { .. } => panic!("Borsh is currently unsupported"),
29            AbiParameters::Json { args } => args
30                .iter()
31                .map(|arg| {
32                    param_names.push(format_ident!("{}", arg.name));
33                    let arg_name = param_names.last().unwrap();
34                    let arg_type = expand_subschema(&mut expander, &arg.type_schema);
35                    quote! { #arg_name: #arg_type }
36                })
37                .collect::<Vec<_>>(),
38        };
39
40        let return_type = function
41            .result
42            .map(|r_type| match r_type {
43                AbiType::Json { type_schema } => expand_subschema(&mut expander, &type_schema),
44                AbiType::Borsh { type_schema: _ } => panic!("Borsh is currently unsupported"),
45            })
46            .unwrap_or_else(|| format_ident!("{}", "()"));
47        let name_str = name.to_string();
48        let args = if param_names.is_empty() {
49            // Special case for parameter-less functions because otherwise the type for
50            // `[]` is not inferrable.
51            quote! { () }
52        } else {
53            quote! { [#(#param_names),*] }
54        };
55        if function.kind == AbiFunctionKind::View {
56            methods_stream.extend(quote! {
57                pub async fn #name(
58                    &self,
59                    #(#params),*
60                ) -> anyhow::Result<#return_type> {
61                    let result = self.contract
62                        .call(#name_str)
63                        .args_json(#args)
64                        .view()
65                        .await?;
66                    Ok(result.json::<#return_type>()?)
67                }
68            });
69        } else {
70            methods_stream.extend(quote! {
71                pub async fn #name(
72                    &self,
73                    gas: utility_workspaces::types::Gas,
74                    deposit: utility_workspaces::types::Balance,
75                    #(#params),*
76                ) -> anyhow::Result<#return_type> {
77                    let result = self.contract
78                        .call(#name_str)
79                        .args_json(#args)
80                        .gas(gas)
81                        .deposit(deposit)
82                        .transact()
83                        .await?;
84                    Ok(result.json::<#return_type>()?)
85                }
86            });
87        }
88    }
89
90    token_stream.extend(quote! {
91        impl #contract_name {
92            #methods_stream
93        }
94    });
95
96    token_stream
97}
98
99pub fn read_abi(abi_path: impl AsRef<Path>) -> AbiRoot {
100    let abi_path = if abi_path.as_ref().is_relative() {
101        let crate_root = get_crate_root().unwrap();
102        crate_root.join(&abi_path)
103    } else {
104        PathBuf::from(abi_path.as_ref())
105    };
106
107    let abi_json = std::fs::read_to_string(&abi_path)
108        .unwrap_or_else(|err| panic!("Unable to read `{}`: {}", abi_path.to_string_lossy(), err));
109
110    serde_json::from_str::<AbiRoot>(&abi_json).unwrap_or_else(|err| {
111        panic!(
112            "Cannot parse `{}` as ABI: {}",
113            abi_path.to_string_lossy(),
114            err
115        )
116    })
117}
118
119fn get_crate_root() -> std::io::Result<PathBuf> {
120    if let Ok(path) = std::env::var("CARGO_MANIFEST_DIR") {
121        return Ok(PathBuf::from(path));
122    }
123
124    let current_dir = std::env::current_dir()?;
125
126    for p in current_dir.ancestors() {
127        if std::fs::read_dir(p)?
128            .filter_map(Result::ok)
129            .any(|p| p.file_name().eq("Cargo.toml"))
130        {
131            return Ok(PathBuf::from(p));
132        }
133    }
134
135    Ok(current_dir)
136}
137
138fn schemars_schema_to_schemafy(schema: &schemars::schema::Schema) -> Schema {
139    let schema_json = serde_json::to_string(&schema).unwrap();
140    serde_json::from_str(&schema_json).unwrap_or_else(|err| {
141        panic!(
142            "Could not convert schemars schema to schemafy model: {}",
143            err
144        )
145    })
146}
147
148fn expand_subschema(
149    expander: &mut Expander,
150    schema: &schemars::schema::Schema,
151) -> proc_macro2::Ident {
152    let schemafy_schema = schemars_schema_to_schemafy(schema);
153    format_ident!("{}", expander.expand_type_from_schema(&schemafy_schema).typ)
154}