diff --git a/Cargo.lock b/Cargo.lock index e40cf80..6862ff6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,7 +461,7 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enum-compat-util" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "serde_yaml", ] @@ -846,15 +846,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -947,17 +938,17 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "move-abstract-interpreter" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" [[package]] name = "move-abstract-stack" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" [[package]] name = "move-binary-format" version = "0.0.3" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "enum-compat-util", @@ -973,12 +964,12 @@ dependencies = [ [[package]] name = "move-borrow-graph" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" [[package]] name = "move-bytecode-source-map" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "bcs", @@ -994,7 +985,7 @@ dependencies = [ [[package]] name = "move-bytecode-verifier" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "move-abstract-interpreter", "move-abstract-stack", @@ -1011,7 +1002,7 @@ dependencies = [ [[package]] name = "move-bytecode-verifier-meter" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "move-binary-format", "move-core-types", @@ -1021,7 +1012,7 @@ dependencies = [ [[package]] name = "move-command-line-common" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "bcs", @@ -1041,7 +1032,7 @@ dependencies = [ [[package]] name = "move-compiler" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "bcs", @@ -1049,6 +1040,7 @@ dependencies = [ "codespan-reporting", "dunce", "hex", + "indexmap 2.14.0", "insta", "lsp-types", "move-abstract-interpreter", @@ -1076,11 +1068,10 @@ dependencies = [ [[package]] name = "move-core-types" version = "0.0.4" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "bcs", - "enum-compat-util", "ethnum", "hex", "indexmap 2.14.0", @@ -1091,7 +1082,6 @@ dependencies = [ "rand 0.8.5", "ref-cast", "serde", - "serde_bytes", "serde_with", "thiserror", "uint", @@ -1100,7 +1090,7 @@ dependencies = [ [[package]] name = "move-ir-to-bytecode" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "codespan-reporting", @@ -1118,7 +1108,7 @@ dependencies = [ [[package]] name = "move-ir-to-bytecode-syntax" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "anyhow", "hex", @@ -1131,7 +1121,7 @@ dependencies = [ [[package]] name = "move-ir-types" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "hex", "move-command-line-common", @@ -1143,7 +1133,7 @@ dependencies = [ [[package]] name = "move-proc-macros" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "enum-compat-util", "quote", @@ -1153,22 +1143,19 @@ dependencies = [ [[package]] name = "move-regex-borrow-graph" version = "0.0.1" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "indexmap 2.14.0", - "insta", - "itertools", "move-binary-format", "move-command-line-common", "move-core-types", - "petgraph", "proptest", ] [[package]] name = "move-symbol-pool" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "phf", "serde", @@ -1177,7 +1164,7 @@ dependencies = [ [[package]] name = "move-vm-config" version = "0.1.0" -source = "git+https://github.com/MystenLabs/sui.git?rev=ca31568261#ca31568261d1ecf95c7735564671235fa1a23b37" +source = "git+https://github.com/MystenLabs/sui.git?rev=585d7a076f990fa1ba8760e39883ddfc99b9276e#585d7a076f990fa1ba8760e39883ddfc99b9276e" dependencies = [ "move-binary-format", ] @@ -1833,16 +1820,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 2c04c44..9ca8ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,11 @@ convert_case = "0.7" # Move compiler crates — git dep pinned to Sui release tag # To update Move version: change rev to new commit/tag -move-compiler = { git = "https://github.com/MystenLabs/sui.git", rev = "ca31568261" } -move-ir-types = { git = "https://github.com/MystenLabs/sui.git", rev = "ca31568261" } -move-symbol-pool = { git = "https://github.com/MystenLabs/sui.git", rev = "ca31568261" } -move-command-line-common = { git = "https://github.com/MystenLabs/sui.git", rev = "ca31568261" } -move-core-types = { git = "https://github.com/MystenLabs/sui.git", rev = "ca31568261" } +move-compiler = { git = "https://github.com/MystenLabs/sui.git", rev = "585d7a076f990fa1ba8760e39883ddfc99b9276e" } +move-ir-types = { git = "https://github.com/MystenLabs/sui.git", rev = "585d7a076f990fa1ba8760e39883ddfc99b9276e" } +move-symbol-pool = { git = "https://github.com/MystenLabs/sui.git", rev = "585d7a076f990fa1ba8760e39883ddfc99b9276e" } +move-command-line-common = { git = "https://github.com/MystenLabs/sui.git", rev = "585d7a076f990fa1ba8760e39883ddfc99b9276e" } +move-core-types = { git = "https://github.com/MystenLabs/sui.git", rev = "585d7a076f990fa1ba8760e39883ddfc99b9276e" } [dev-dependencies] insta = "1" diff --git a/src/codegen.rs b/src/codegen.rs index 5ebe546..bf2855d 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -389,7 +389,18 @@ pub fn generate_module(module: &ModuleInfo, config: &CodegenConfig) -> String { .iter() .any(|f| f.params.iter().any(|p| type_contains_vecmap(&p.move_type))); - let needs_bcs = !used_pure_structs.is_empty() || has_vecmap_param; + // Emitted-event structs that get BCS schemas (only when --events is enabled). + let event_structs: Vec<&StructInfo> = if config.include_events { + module + .structs + .iter() + .filter(|s| module.emitted_events.contains(&s.name)) + .collect() + } else { + Vec::new() + }; + + let needs_bcs = !used_pure_structs.is_empty() || has_vecmap_param || !event_structs.is_empty(); let needs_address = used_pure_structs.iter().any(|s| { s.fields .iter() @@ -398,6 +409,10 @@ pub fn generate_module(module: &ModuleInfo, config: &CodegenConfig) -> String { f.params .iter() .any(|p| param_vecmap_contains_address(&p.move_type, &module.structs)) + }) || event_structs.iter().any(|s| { + s.fields + .iter() + .any(|(_, ty)| type_contains_address(ty, &module.structs)) }); // --- Imports --- @@ -420,6 +435,7 @@ pub fn generate_module(module: &ModuleInfo, config: &CodegenConfig) -> String { let referenced_structs = collect_referenced_structs(module); if config.include_events { generate_event_types(&mut w, module, &referenced_structs); + generate_event_bcs_schemas(&mut w, &event_structs, &module.structs); } // --- Struct interfaces (only for structs referenced in function params) --- @@ -482,6 +498,22 @@ fn generate_event_types(w: &mut CodeWriter, module: &ModuleInfo, referenced: &Ha } } +/// Generates `export const = bcs.struct(...)` schemas for emitted events, +/// so callers can decode raw event BCS bytes (`SuiEvent.bcs`) into typed values. +/// Field keys use the same camelCase convention as param-struct BCS schemas. The +/// schema const coexists with the snake_case `export type` (TS keeps type and +/// value namespaces separate, so sharing the event name is legal). +fn generate_event_bcs_schemas(w: &mut CodeWriter, events: &[&StructInfo], structs: &[StructInfo]) { + for event in events { + w.line(&format!( + "export const {} = {};", + event.name, + to_bcs_struct_schema(event, structs) + )); + w.blank(); + } +} + /// Generates the `move2ts-errors.ts` module content. pub fn generate_errors_module() -> String { let mut w = CodeWriter::new(); @@ -2080,6 +2112,126 @@ mod tests { ); } + /// Builds a module with one emitted event (`Trade`) carrying mixed field types, + /// plus an optional same-named function param to exercise the collision case. + fn event_module(emit_as_param: bool) -> ModuleInfo { + let trade = StructInfo { + name: "Trade".to_string(), + fields: vec![ + ("trade_amount".to_string(), MoveType::U64), + ("is_buy".to_string(), MoveType::Bool), + ("counterparty".to_string(), MoveType::Address), + ], + has_key: false, + has_copy: true, + has_drop: true, + }; + let functions = if emit_as_param { + vec![FunctionInfo { + name: "do_trade".to_string(), + is_entry: false, + type_params: vec![], + params: vec![ParamInfo { + name: "trade".to_string(), + move_type: MoveType::Struct { + module: None, + name: "Trade".to_string(), + type_args: vec![], + }, + is_singleton: false, + }], + has_clock_param: false, + has_random_param: false, + }] + } else { + vec![] + }; + ModuleInfo { + name: "trades".to_string(), + functions, + structs: vec![trade], + singletons: HashSet::new(), + emitted_events: HashSet::from(["Trade".to_string()]), + } + } + + #[test] + fn event_gets_bcs_schema_const_with_camelcase_fields() { + let config = CodegenConfig { + package_id_env_var: "MY_PROJECT_PACKAGE_ID".to_string(), + project_name: "my_project".to_string(), + include_events: true, + }; + let output = generate_module(&event_module(false), &config); + + // BCS schema const, camelCase keys, correct encoders. + assert!( + output.contains("export const Trade = bcs.struct('Trade', {"), + "event should get a BCS schema const, got:\n{output}" + ); + assert!(output.contains("tradeAmount: bcs.u64()")); + assert!(output.contains("isBuy: bcs.bool()")); + assert!(output.contains("counterparty: bcs.Address")); + assert!( + !output.contains("trade_amount: bcs"), + "BCS field keys should be camelCase, not snake_case" + ); + + // Coexists additively with the snake_case parsedJson string type. + assert!(output.contains("export type Trade = {")); + assert!(output.contains("readonly trade_amount: string;")); + } + + #[test] + fn event_bcs_triggers_bcs_import_for_event_only_module() { + let config = CodegenConfig { + package_id_env_var: "MY_PROJECT_PACKAGE_ID".to_string(), + project_name: "my_project".to_string(), + include_events: true, + }; + let output = generate_module(&event_module(false), &config); + // Address-bearing event selects the Sui bcs re-export (which provides bcs.Address). + assert!( + output.contains("import { bcs } from '@mysten/sui/bcs';"), + "event with address field should import Sui bcs, got:\n{output}" + ); + } + + #[test] + fn no_event_bcs_schema_when_events_disabled() { + let config = CodegenConfig { + package_id_env_var: "MY_PROJECT_PACKAGE_ID".to_string(), + project_name: "my_project".to_string(), + include_events: false, + }; + let output = generate_module(&event_module(false), &config); + assert!( + !output.contains("export const Trade = bcs.struct"), + "no event BCS schema should be emitted without --events" + ); + assert!(!output.contains("@mysten/bcs") && !output.contains("@mysten/sui/bcs")); + } + + #[test] + fn event_bcs_schema_emitted_once_when_also_param() { + let config = CodegenConfig { + package_id_env_var: "MY_PROJECT_PACKAGE_ID".to_string(), + project_name: "my_project".to_string(), + include_events: true, + }; + let output = generate_module(&event_module(true), &config); + + // Exactly one top-level schema const, despite Trade also being a param. + assert_eq!( + output.matches("export const Trade = bcs.struct").count(), + 1, + "schema const should be emitted exactly once, got:\n{output}" + ); + // The param version is suffixed and keeps its own interface — no clash. + assert!(output.contains("export type TradeEvent = {")); + assert!(output.contains("export interface Trade {")); + } + #[test] fn no_bcs_import_when_only_key_structs() { let module = ModuleInfo { diff --git a/tests/integration_test.rs b/tests/integration_test.rs index c99eb43..95f70c4 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -452,6 +452,27 @@ fn full_pipeline_events() { // TradeInfo should also have BCS interface (for the param usage) assert!(ts_output.contains("export interface TradeInfo {")); + // Each emitted event also gets a BCS schema const for decoding raw event bytes. + assert!( + ts_output.contains( + "export const ItemPurchased = bcs.struct('ItemPurchased', { buyer: bcs.Address, seller: bcs.Address, price: bcs.u64(), itemId: bcs.Address });" + ), + "ItemPurchased should get a BCS schema with camelCase keys, got:\n{ts_output}" + ); + assert!(ts_output.contains("export const ListingCreated = bcs.struct('ListingCreated',")); + assert!(ts_output.contains("export const FeeCollected = bcs.struct('FeeCollected',")); + // Emitted-and-param struct gets exactly one schema const (no name clash with its interface). + assert_eq!( + ts_output + .matches("export const TradeInfo = bcs.struct") + .count(), + 1 + ); + // Non-emitted param struct gets no event BCS schema const. + assert!(!ts_output.contains("export const PriceRange = bcs.struct")); + // Address-bearing events select the Sui bcs re-export (provides bcs.Address). + assert!(ts_output.contains("import { bcs } from '@mysten/sui/bcs';")); + // PriceRange is NOT emitted — no event type assert!(!ts_output.contains("export type PriceRange")); // PriceRange should still have BCS interface (used as param)