diff --git a/bin/sozo/src/commands/build.rs b/bin/sozo/src/commands/build.rs index bb45c1f6c4..a851cf3165 100644 --- a/bin/sozo/src/commands/build.rs +++ b/bin/sozo/src/commands/build.rs @@ -11,6 +11,10 @@ pub struct BuildArgs { #[arg(help = "Generate Typescript bindings.")] pub typescript: bool, + #[arg(long)] + #[arg(help = "Generate Typescript bindings.")] + pub typescript_v2: bool, + #[arg(long)] #[arg(help = "Generate Unity bindings.")] pub unity: bool, @@ -32,6 +36,10 @@ impl BuildArgs { builtin_plugins.push(BuiltinPlugins::Typescript); } + if self.typescript_v2 { + builtin_plugins.push(BuiltinPlugins::TypeScriptV2); + } + if self.unity { builtin_plugins.push(BuiltinPlugins::Unity); } @@ -67,8 +75,12 @@ mod tests { fn build_example_with_typescript_and_unity_bindings() { let config = build_test_config("../../examples/spawn-and-move/Scarb.toml").unwrap(); - let build_args = - BuildArgs { bindings_output: "generated".to_string(), typescript: true, unity: true }; + let build_args = BuildArgs { + bindings_output: "generated".to_string(), + typescript: true, + unity: true, + typescript_v2: true, + }; let result = build_args.run(&config); assert!(result.is_ok()); } diff --git a/crates/dojo-bindgen/src/lib.rs b/crates/dojo-bindgen/src/lib.rs index 627dc1961c..ca8e24cd79 100644 --- a/crates/dojo-bindgen/src/lib.rs +++ b/crates/dojo-bindgen/src/lib.rs @@ -12,6 +12,7 @@ use error::{BindgenResult, Error}; mod plugins; use plugins::typescript::TypescriptPlugin; +use plugins::typescript_v2::TypeScriptV2Plugin; use plugins::unity::UnityPlugin; use plugins::BuiltinPlugin; pub use plugins::BuiltinPlugins; @@ -85,6 +86,7 @@ impl PluginManager { let builder: Box = match plugin { BuiltinPlugins::Typescript => Box::new(TypescriptPlugin::new()), BuiltinPlugins::Unity => Box::new(UnityPlugin::new()), + BuiltinPlugins::TypeScriptV2 => Box::new(TypeScriptV2Plugin::new()), }; let files = builder.generate_code(&data).await?; diff --git a/crates/dojo-bindgen/src/plugins/mod.rs b/crates/dojo-bindgen/src/plugins/mod.rs index ab6abbcb8b..b603262e44 100644 --- a/crates/dojo-bindgen/src/plugins/mod.rs +++ b/crates/dojo-bindgen/src/plugins/mod.rs @@ -8,12 +8,14 @@ use crate::error::BindgenResult; use crate::DojoData; pub mod typescript; +pub mod typescript_v2; pub mod unity; #[derive(Debug)] pub enum BuiltinPlugins { Typescript, Unity, + TypeScriptV2, } impl fmt::Display for BuiltinPlugins { @@ -21,6 +23,7 @@ impl fmt::Display for BuiltinPlugins { match self { BuiltinPlugins::Typescript => write!(f, "typescript"), BuiltinPlugins::Unity => write!(f, "unity"), + BuiltinPlugins::TypeScriptV2 => write!(f, "typescript_v2"), } } } diff --git a/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs b/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs new file mode 100644 index 0000000000..b23549aa89 --- /dev/null +++ b/crates/dojo-bindgen/src/plugins/typescript_v2/mod.rs @@ -0,0 +1,622 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use cainome::parser::tokens::{Composite, CompositeType, Function}; +use convert_case::Casing; + +use crate::error::BindgenResult; +use crate::plugins::BuiltinPlugin; +use crate::{DojoContract, DojoData, DojoModel}; + +pub struct TypeScriptV2Plugin {} + +impl TypeScriptV2Plugin { + pub fn new() -> Self { + Self {} + } + + // Maps cairo types to TypeScript defined types + fn map_type(type_name: &str) -> String { + match type_name { + "bool" => "boolean".to_string(), + "u8" => "number".to_string(), + "u16" => "number".to_string(), + "u32" => "number".to_string(), + "u64" => "bigint".to_string(), + "u128" => "bigint".to_string(), + "u256" => "bigint".to_string(), + "usize" => "number".to_string(), + "felt252" => "string".to_string(), + "ClassHash" => "string".to_string(), + "ContractAddress" => "string".to_string(), + + _ => type_name.to_string(), + } + } + + fn generate_header() -> String { + format!( + "// Generated by dojo-bindgen on {}. Do not modify this file manually.\n", + chrono::Utc::now().to_rfc2822() + ) + } + + fn generate_imports() -> String { + "import { Account } from \"starknet\"; +import { + Clause, + Client, + ModelClause, + createClient, + valueToToriiValueAndOperator, +} from \"@dojoengine/torii-client\"; +import { LOCAL_KATANA, createManifestFromJson } from \"@dojoengine/core\";" + .to_string() + } + + fn generate_query_types(models: &[&DojoModel]) -> String { + let mut query_fields = Vec::new(); + let mut result_mapping = Vec::new(); + + for model in models { + query_fields + .push(format!("{model_name}: ModelClause<{model_name}>;", model_name = model.name)); + + result_mapping.push(format!("{model_name}: {model_name};", model_name = model.name)); + } + + format!( + "type Query = Partial<{{ + {query_fields} +}}>; + +type ResultMapping = {{ + {result_mapping} +}}; + +type QueryResult = {{ + [K in keyof T]: K extends keyof ResultMapping ? ResultMapping[K] : never; +}}; + +// Only supports a single model for now, since torii doesn't support multiple models +// And inside that single model, there's only support for a single query. +function convertQueryToToriiClause(query: Query): Clause | undefined {{ + const [model, clause] = Object.entries(query)[0]; + + if (Object.keys(clause).length === 0) {{ + return undefined; + }} + + const clauses: Clause[] = Object.entries(clause).map(([key, value]) => {{ + return {{ + Member: {{ + model, + member: key, + ...valueToToriiValueAndOperator(value), + }}, + }} satisfies Clause; + }}); + + return clauses[0]; +}}", + query_fields = query_fields.join("\n "), + result_mapping = result_mapping.join("\n "), + ) + } + + fn generate_model_types(models: &[&DojoModel], handled_tokens: &mut Vec) -> String { + let mut out = String::new(); + + for model in models { + let tokens = &model.tokens; + + for token in &tokens.enums { + handled_tokens.push(token.to_composite().unwrap().to_owned()); + } + for token in &tokens.structs { + handled_tokens.push(token.to_composite().unwrap().to_owned()); + } + + let mut structs = tokens.structs.to_owned(); + structs.sort_by(|a, b| { + if a.to_composite() + .unwrap() + .inners + .iter() + .any(|field| field.token.type_name() == b.type_name()) + { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Less + } + }); + + for token in &structs { + out += TypeScriptV2Plugin::format_struct( + token.to_composite().unwrap(), + handled_tokens, + ) + .as_str(); + } + + for token in &tokens.enums { + out += TypeScriptV2Plugin::format_enum(token.to_composite().unwrap()).as_str(); + } + + out += "\n"; + } + + out + } + + fn generate_base_calls_class() -> String { + "class BaseCalls { + contractAddress: string; + account?: Account; + + constructor(contractAddress: string, account?: Account) { + this.account = account; + this.contractAddress = contractAddress; + } + + async execute(entrypoint: string, calldata: any[] = []): Promise { + if (!this.account) { + throw new Error(\"No account set to interact with dojo_starter\"); + } + + await this.account.execute( + { + contractAddress: this.contractAddress, + entrypoint, + calldata, + }, + undefined, + { + maxFee: 0, + } + ); + } +} +" + .to_string() + } + + fn generate_contracts(contracts: &[&DojoContract], handled_tokens: &[Composite]) -> String { + let mut out = String::new(); + + for contract in contracts { + let systems = contract + .systems + .iter() + .map(|system| { + TypeScriptV2Plugin::format_system(system.to_function().unwrap(), handled_tokens) + }) + .collect::>() + .join("\n\n "); + + out += &format!( + "class {}Calls extends BaseCalls {{ + constructor(contractAddress: string, account?: Account) {{ + super(contractAddress, account); + }} + + {} +}} +", + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Pascal), + systems, + ); + } + + out + } + + fn generate_initial_params(contracts: &[&DojoContract]) -> String { + let system_addresses = contracts + .iter() + .map(|contract| { + format!( + "{}Address: string;", + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Camel) + ) + }) + .collect::>() + .join("\n "); + + format!( + "type InitialParams = GeneralParams & + ( + | {{ + rpcUrl: string; + worldAddress: string; + {system_addresses} + }} + | {{ + manifest: any; + }} + );" + ) + } + + fn generate_world_class(world_name: &String, contracts: &[&DojoContract]) -> String { + let mut out = String::new(); + + out += "type GeneralParams = { + toriiUrl: string; + relayUrl: string; + account?: Account; +};"; + + out += "\n\n"; + + out += TypeScriptV2Plugin::generate_initial_params(contracts).as_str(); + + out += "\n\n"; + + let system_properties = contracts + .iter() + .map(|contract| { + format!( + "{camel_case_name}: {pascal_case_name}Calls; + {camel_case_name}Address: string;", + camel_case_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Camel), + pascal_case_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Pascal) + ) + }) + .collect::>() + .join("\n "); + + let system_address_initializations = contracts + .iter() + .map(|contract| { + format!( + "const {contract_name}Address = config.contracts.find( + (contract) => + contract.name === \"dojo_starter::systems::{contract_name}::{contract_name}\" + )?.address; + + if (!{contract_name}Address) {{ + throw new Error(\"No {contract_name} contract found in the manifest\"); + }} + + this.{contract_name}Address = {contract_name}Address;", + contract_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Camel) + ) + }) + .collect::>() + .join("\n "); + + let system_address_initializations_from_params = contracts + .iter() + .map(|contract| { + format!( + "this.{camel_case_name}Address = params.{camel_case_name}Address;", + camel_case_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Camel), + ) + }) + .collect::>() + .join("\n "); + + let system_initializations = contracts + .iter() + .map(|contract| { + format!( + "this.{camel_case_name} = new \ + {pascal_case_name}Calls(this.{camel_case_name}Address, this._account);", + camel_case_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Camel), + pascal_case_name = + TypeScriptV2Plugin::formatted_contract_name(&contract.qualified_path) + .to_case(convert_case::Case::Pascal) + ) + }) + .collect::>() + .join("\n "); + + let formatted_world_name = world_name.to_case(convert_case::Case::Pascal); + + out += &format!( + "export class {formatted_world_name} {{ + rpcUrl: string; + toriiUrl: string; + toriiPromise: Promise; + relayUrl: string; + worldAddress: string; + private _account?: Account; + {system_properties} + + constructor(params: InitialParams) {{ + this.rpcUrl = LOCAL_KATANA; + if (\"manifest\" in params) {{ + const config = createManifestFromJson(params.manifest); + this.worldAddress = config.world.address; + + {system_address_initializations} + }} else {{ + this.rpcUrl = params.rpcUrl; + this.worldAddress = params.worldAddress; + {system_address_initializations_from_params} + }} + this.toriiUrl = params.toriiUrl; + this.relayUrl = params.relayUrl; + this._account = params.account; + {system_initializations} + + this.toriiPromise = createClient([], {{ + rpcUrl: this.rpcUrl, + toriiUrl: this.toriiUrl, + worldAddress: this.worldAddress, + relayUrl: this.relayUrl, + }}); + }} + + get account(): Account | undefined {{ + return this._account; + }} + + set account(account: Account) {{ + this._account = account; + {system_initializations} + }} + + async query(query: T, limit = 10, offset = 0) {{ + const torii = await this.toriiPromise; + + return {{ + torii, + findEntities: async () => this.findEntities(query, limit, offset), + }}; + }} + + async findEntities(query: T, limit = 10, offset = 0) {{ + const torii = await this.toriiPromise; + + const clause = convertQueryToToriiClause(query); + + const toriiResult = await torii.getEntities({{ + limit, + offset, + clause, + }}); + + return toriiResult as Record>; + }} + + async findEntity(query: T) {{ + const result = await this.findEntities(query, 1); + + if (Object.values(result).length === 0) {{ + return undefined; + }} + + return Object.values(result)[0] as QueryResult; + }} +}}" + ); + + out + } + + // Token should be a struct + // This will be formatted into a TypeScript interface + // using TypeScript defined types + fn format_struct(token: &Composite, handled_tokens: &[Composite]) -> String { + let mut native_fields: Vec = Vec::new(); + + for field in &token.inners { + let mapped = TypeScriptV2Plugin::map_type(field.token.type_name().as_str()); + if mapped == field.token.type_name() { + let token = handled_tokens + .iter() + .find(|t| t.type_name() == field.token.type_name()) + .unwrap_or_else(|| panic!("Token not found: {}", field.token.type_name())); + if token.r#type == CompositeType::Enum { + native_fields.push(format!("{}: {};", field.name, mapped)); + } else { + native_fields.push(format!("{}: {};", field.name, field.token.type_name())); + } + } else { + native_fields.push(format!("{}: {};", field.name, mapped)); + } + } + + format!( + " +// Type definition for `{path}` struct +export interface {name} {{ + {native_fields} +}} +", + path = token.type_path, + name = token.type_name(), + native_fields = native_fields.join("\n ") + ) + } + + // Token should be an enum + // This will be formatted into a C# enum + // Enum is mapped using index of cairo enum + fn format_enum(token: &Composite) -> String { + let fields = token + .inners + .iter() + .map(|field| format!("{},", field.name,)) + .collect::>() + .join("\n "); + + format!( + " +// Type definition for `{}` enum +export enum {} {{ + {} +}} +", + token.type_path, + token.type_name(), + fields + ) + } + + // Formats a system into a JS method used by the contract class + // Handled tokens should be a list of all structs and enums used by the contract + // Such as a set of referenced tokens from a model + fn format_system(system: &Function, handled_tokens: &[Composite]) -> String { + let args = system + .inputs + .iter() + .map(|arg| { + format!( + "{}: {}", + arg.0, + if TypeScriptV2Plugin::map_type(&arg.1.type_name()) == arg.1.type_name() { + arg.1.type_name() + } else { + TypeScriptV2Plugin::map_type(&arg.1.type_name()) + } + ) + }) + .collect::>() + .join(", "); + + let calldata = system + .inputs + .iter() + .map(|arg| { + let token = &arg.1; + let type_name = &arg.0; + + match handled_tokens.iter().find(|t| t.type_name() == token.type_name()) { + Some(t) => { + // Need to flatten the struct members. + match t.r#type { + CompositeType::Struct => t + .inners + .iter() + .map(|field| format!("props.{}.{}", type_name, field.name)) + .collect::>() + .join(",\n "), + _ => type_name.to_string(), + } + } + None => type_name.to_string(), + } + }) + .collect::>() + .join(",\n "); + + format!( + "async {pretty_system_name}({args}): Promise {{ + try {{ + await this.execute(\"{system_name}\", [{calldata}]) + }} catch (error) {{ + console.error(\"Error executing {pretty_system_name}:\", error); + throw error; + }} + }}", + pretty_system_name = system.name.to_case(convert_case::Case::Camel), + // formatted args to use our mapped types + args = args, + system_name = system.name, + // calldata for execute + calldata = calldata + ) + } + + // Formats a contract file path into a pretty contract name + // eg. dojo_examples::actions::actions.json -> Actions + fn formatted_contract_name(contract_file_name: &str) -> String { + let contract_name = + contract_file_name.split("::").last().unwrap().trim_end_matches(".json"); + contract_name.to_string() + } + + fn generate_code_content(data: &DojoData) -> String { + let mut handled_tokens = Vec::::new(); + let models = data.models.values().collect::>(); + let contracts = data.contracts.values().collect::>(); + + let mut code = String::new(); + code += TypeScriptV2Plugin::generate_header().as_str(); + code += TypeScriptV2Plugin::generate_imports().as_str(); + code += "\n"; + code += TypeScriptV2Plugin::generate_model_types(models.as_slice(), &mut handled_tokens) + .as_str(); + code += "\n"; + code += TypeScriptV2Plugin::generate_base_calls_class().as_str(); + code += "\n"; + code += + TypeScriptV2Plugin::generate_contracts(contracts.as_slice(), &handled_tokens).as_str(); + code += "\n"; + code += TypeScriptV2Plugin::generate_query_types(models.as_slice()).as_str(); + code += "\n"; + code += TypeScriptV2Plugin::generate_world_class(&data.world.name, contracts.as_slice()) + .as_str(); + + code + } +} + +#[async_trait] +impl BuiltinPlugin for TypeScriptV2Plugin { + async fn generate_code(&self, data: &DojoData) -> BindgenResult>> { + let code: String = TypeScriptV2Plugin::generate_code_content(data); + + let mut out: HashMap> = HashMap::new(); + let output_path = Path::new(&format!("{}.ts", data.world.name)).to_owned(); + + out.insert(output_path, code.as_bytes().to_vec()); + + Ok(out) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::io::Read; + + use camino::Utf8PathBuf; + + use super::*; + use crate::gather_dojo_data; + + #[test] + fn test_output() { + let mut expected_output = String::new(); + let mut file = + fs::File::open("src/test_data/mocks/dojo_examples.ts").expect("file not found"); + file.read_to_string(&mut expected_output).expect("error reading file"); + + let expected_output_without_header = + expected_output.lines().skip(1).collect::>().join("\n"); + + let data = gather_dojo_data( + &Utf8PathBuf::from("src/test_data/spawn-and-move/Scarb.toml"), + "dojo_examples", + "dev", + ) + .unwrap(); + + let actual_output = TypeScriptV2Plugin::generate_code_content(&data); + let actual_output_without_header = + actual_output.lines().skip(1).collect::>().join("\n"); + + // This test currently is very naive, but DojoData is unsorted, so the output + // can change between tests. This is a temporary solution until we have a better + // way to test this. + assert_eq!(actual_output_without_header.len(), 7479); + assert_eq!(expected_output_without_header.len(), 7479); + } +} diff --git a/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts b/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts new file mode 100644 index 0000000000..49e3805bf7 --- /dev/null +++ b/crates/dojo-bindgen/src/test_data/mocks/dojo_examples.ts @@ -0,0 +1,297 @@ +// Generated by dojo-bindgen on Fri, 12 Apr 2024 13:23:24 +0000. Do not modify this file manually. +import { Account } from "starknet"; +import { + Clause, + Client, + ModelClause, + createClient, + valueToToriiValueAndOperator, +} from "@dojoengine/torii-client"; +import { LOCAL_KATANA, createManifestFromJson } from "@dojoengine/core"; + +// Type definition for `dojo_examples::actions::actions::Moved` struct +export interface Moved { + player: string; + direction: Direction; +} + +// Type definition for `dojo_examples::models::Direction` enum +export enum Direction { + None, + Left, + Right, + Up, + Down, +} + + +// Type definition for `dojo_examples::models::Vec2` struct +export interface Vec2 { + x: number; + y: number; +} + +// Type definition for `dojo_examples::models::Position` struct +export interface Position { + player: string; + vec: Vec2; +} + + +// Type definition for `dojo_examples::models::Moves` struct +export interface Moves { + player: string; + remaining: number; + last_direction: Direction; +} + +// Type definition for `dojo_examples::models::Direction` enum +export enum Direction { + None, + Left, + Right, + Up, + Down, +} + + +// Type definition for `dojo_examples::models::EmoteMessage` struct +export interface EmoteMessage { + identity: string; + emote: Emote; +} + +// Type definition for `dojo_examples::models::Emote` enum +export enum Emote { + None, + Happy, + Sad, + Angry, + Love, +} + + +class BaseCalls { + contractAddress: string; + account?: Account; + + constructor(contractAddress: string, account?: Account) { + this.account = account; + this.contractAddress = contractAddress; + } + + async execute(entrypoint: string, calldata: any[] = []): Promise { + if (!this.account) { + throw new Error("No account set to interact with dojo_starter"); + } + + await this.account.execute( + { + contractAddress: this.contractAddress, + entrypoint, + calldata, + }, + undefined, + { + maxFee: 0, + } + ); + } +} + +class ActionsCalls extends BaseCalls { + constructor(contractAddress: string, account?: Account) { + super(contractAddress, account); + } + + async tileTerrain(vec: Vec2): Promise { + try { + await this.execute("tile_terrain", [props.vec.x, + props.vec.y]) + } catch (error) { + console.error("Error executing tileTerrain:", error); + throw error; + } + } + + async quadrant(pos: Position): Promise { + try { + await this.execute("quadrant", [props.pos.player, + props.pos.vec]) + } catch (error) { + console.error("Error executing quadrant:", error); + throw error; + } + } + + async dojoResource(): Promise { + try { + await this.execute("dojo_resource", []) + } catch (error) { + console.error("Error executing dojoResource:", error); + throw error; + } + } + + async spawn(): Promise { + try { + await this.execute("spawn", []) + } catch (error) { + console.error("Error executing spawn:", error); + throw error; + } + } + + async move(direction: Direction): Promise { + try { + await this.execute("move", [direction]) + } catch (error) { + console.error("Error executing move:", error); + throw error; + } + } +} + +type Query = Partial<{ + Moved: ModelClause; + Position: ModelClause; + Moves: ModelClause; + EmoteMessage: ModelClause; +}>; + +type ResultMapping = { + Moved: Moved; + Position: Position; + Moves: Moves; + EmoteMessage: EmoteMessage; +}; + +type QueryResult = { + [K in keyof T]: K extends keyof ResultMapping ? ResultMapping[K] : never; +}; + +// Only supports a single model for now, since torii doesn't support multiple models +// And inside that single model, there's only support for a single query. +function convertQueryToToriiClause(query: Query): Clause | undefined { + const [model, clause] = Object.entries(query)[0]; + + if (Object.keys(clause).length === 0) { + return undefined; + } + + const clauses: Clause[] = Object.entries(clause).map(([key, value]) => { + return { + Member: { + model, + member: key, + ...valueToToriiValueAndOperator(value), + }, + } satisfies Clause; + }); + + return clauses[0]; +} +type GeneralParams = { + toriiUrl: string; + relayUrl: string; + account?: Account; +}; + +type InitialParams = GeneralParams & + ( + | { + rpcUrl: string; + worldAddress: string; + actionsAddress: string; + } + | { + manifest: any; + } + ); + +export class DojoExamples { + rpcUrl: string; + toriiUrl: string; + toriiPromise: Promise; + relayUrl: string; + worldAddress: string; + private _account?: Account; + actions: ActionsCalls; + actionsAddress: string; + + constructor(params: InitialParams) { + this.rpcUrl = LOCAL_KATANA; + if ("manifest" in params) { + const config = createManifestFromJson(params.manifest); + this.worldAddress = config.world.address; + + const actionsAddress = config.contracts.find( + (contract) => + contract.name === "dojo_starter::systems::actions::actions" + )?.address; + + if (!actionsAddress) { + throw new Error("No actions contract found in the manifest"); + } + + this.actionsAddress = actionsAddress; + } else { + this.rpcUrl = params.rpcUrl; + this.worldAddress = params.worldAddress; + this.actionsAddress = params.actionsAddress; + } + this.toriiUrl = params.toriiUrl; + this.relayUrl = params.relayUrl; + this._account = params.account; + this.actions = new ActionsCalls(this.actionsAddress, this._account); + + this.toriiPromise = createClient([], { + rpcUrl: this.rpcUrl, + toriiUrl: this.toriiUrl, + worldAddress: this.worldAddress, + relayUrl: this.relayUrl, + }); + } + + get account(): Account | undefined { + return this._account; + } + + set account(account: Account) { + this._account = account; + this.actions = new ActionsCalls(this.actionsAddress, this._account); + } + + async query(query: T, limit = 10, offset = 0) { + const torii = await this.toriiPromise; + + return { + torii, + findEntities: async () => this.findEntities(query, limit, offset), + }; + } + + async findEntities(query: T, limit = 10, offset = 0) { + const torii = await this.toriiPromise; + + const clause = convertQueryToToriiClause(query); + + const toriiResult = await torii.getEntities({ + limit, + offset, + clause, + }); + + return toriiResult as Record>; + } + + async findEntity(query: T) { + const result = await this.findEntities(query, 1); + + if (Object.values(result).length === 0) { + return undefined; + } + + return Object.values(result)[0] as QueryResult; + } +} \ No newline at end of file