Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
resolver = "2"
resolver = "3"
members = [
"sui-move",
"sui-move-derive",
Expand All @@ -11,6 +11,8 @@ members = [

[workspace.package]
license = "Apache-2.0"
edition = "2021"
version = "0.1.0"
repository = "https://github.com/Talus-Network/move-binding"
homepage = "https://github.com/Talus-Network/move-binding"

Expand All @@ -19,9 +21,9 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
bcs = "0.1.6"
sui-sdk-types = { version = "0.3.1", git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"] }
sui-rpc = { version = "0.3.1", git = "https://github.com/mystenlabs/sui-rust-sdk" }
sui-crypto = { version = "0.3.0", git = "https://github.com/mystenlabs/sui-rust-sdk" }
sui-sdk-types = { version = "0.3.1", features = ["serde"] }
sui-rpc = { version = "0.3.1" }
sui-crypto = { version = "0.3.0" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
Expand Down
4 changes: 2 additions & 2 deletions sui-move-call/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "sui-move-call"
version = "0.1.0"
edition = "2021"
version.workspace = true
edition.workspace = true
description = "Typed call arguments and call specifications for Move calls on Sui."
readme = "README.md"
license.workspace = true
Expand Down
7 changes: 5 additions & 2 deletions sui-move-codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "sui-move-codegen"
version = "0.1.0"
edition = "2021"
version.workspace = true
edition.workspace = true
description = "Generate typed Rust bindings (types + CallSpec builders) from Sui Move package metadata."
readme = "README.md"
license.workspace = true
Expand All @@ -18,3 +18,6 @@ syn = { workspace = true }
prettyplease = { workspace = true }
sui-rpc = { workspace = true }
sui-sdk-types = { workspace = true }

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
113 changes: 113 additions & 0 deletions sui-move-codegen/examples/localnet_workspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! Generate a bindings workspace for a Move package (plus its dependency closure) on localnet.
//!
//! By default this connects to `http://127.0.0.1:9000` (override with `--grpc` or `SUI_GRPC`)
//! and writes output under `../target/bindings/<package_id_without_0x>`.
//!
//! Example:
//! - `cargo run -p sui-move-codegen --example localnet_workspace -- --check`
//! - `cargo run -p sui-move-codegen --example localnet_workspace -- 0x4cc... --check`
//! - `cargo run -p sui-move-codegen --example localnet_workspace -- 0x4cc... --external 0x0aaa...=../my-primitives-crate --check`

use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process::Command;

use sui_move_codegen::render::RenderOptions;
use sui_move_codegen::workspace::{generate_bindings_workspace, WorkspaceOptions};
use sui_rpc::Client;
use sui_sdk_types::Address;

const DEFAULT_PACKAGE: &str = "0x4cc38b7c23bf14d7555503ab38a9748f9544c2c29c6519df412b4f6fb6971640";
const DEFAULT_GRPC: &str = "http://127.0.0.1:9000";

fn usage() -> ! {
eprintln!(
"Usage: localnet_workspace [package_id] [--grpc <url>] [--out <dir>] [--external <pkg>=<crate_dir>]... [--check]\n\n\
Defaults:\n\
- package_id: {DEFAULT_PACKAGE}\n\
- grpc: {DEFAULT_GRPC} (or $SUI_GRPC)\n\
- out: <repo>/target/bindings/<package_id_without_0x>\n"
);
std::process::exit(2);
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1).peekable();

let mut package_id = DEFAULT_PACKAGE.to_string();
let mut grpc = std::env::var("SUI_GRPC").unwrap_or_else(|_| DEFAULT_GRPC.to_string());
let mut out_dir: Option<PathBuf> = None;
let mut externals: BTreeMap<String, PathBuf> = BTreeMap::new();
let mut check: bool = false;

// Optional first positional arg: package id.
if let Some(first) = args.peek() {
if !first.starts_with('-') {
package_id = args.next().unwrap();
}
}

while let Some(arg) = args.next() {
match arg.as_str() {
"--grpc" => grpc = args.next().unwrap_or_else(|| usage()),
"--out" => out_dir = Some(PathBuf::from(args.next().unwrap_or_else(|| usage()))),
"--external" => {
let spec = args.next().unwrap_or_else(|| usage());
let (pkg, path) = spec
.split_once('=')
.ok_or("expected --external <pkg_id>=<crate_dir>")?;
externals.insert(pkg.to_string(), PathBuf::from(path));
}
"--check" => check = true,
"--help" | "-h" => usage(),
other => return Err(format!("unknown argument `{other}`").into()),
}
}

let root_pkg: Address = package_id.parse()?;
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.canonicalize()?;
let default_out = repo_root
.join("target")
.join("bindings")
.join(package_id.trim_start_matches("0x"));
let out_dir = out_dir.unwrap_or(default_out);

println!("grpc: {grpc}");
println!("package: {package_id}");
println!("out: {}", out_dir.display());

let mut client = Client::new(grpc)?;
let render_opts = RenderOptions::default();
let ws_opts = WorkspaceOptions {
move_binding_root: Some(repo_root),
force_non_flattened: true,
};

generate_bindings_workspace(
&mut client,
root_pkg,
&out_dir,
&render_opts,
externals,
ws_opts,
)
.await?;

println!("generated: {}", out_dir.display());
if check {
let status = Command::new("cargo")
.arg("check")
.current_dir(&out_dir)
.status()?;
if !status.success() {
return Err(format!("cargo check failed with status {status}").into());
}
} else {
println!("next: (optional) cd {} && cargo check", out_dir.display());
}

Ok(())
}
3 changes: 3 additions & 0 deletions sui-move-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub mod ir;
/// Render normalized metadata into Rust source.
pub mod render;

/// High-level helpers for generating bindings across packages.
pub mod workspace;

pub use crate::source::fetch_package;

/// Errors from sourcing or normalizing package metadata.
Expand Down
11 changes: 11 additions & 0 deletions sui-move-codegen/src/render/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,16 @@ pub(crate) fn map_builtin(type_name: &TypeName, use_aliases: bool) -> Option<Bui
(sm(use_aliases, quote! { type_name::TypeName }), false)
}
("0x1", "ascii", "String") => (sm(use_aliases, quote! { ascii::String }), false),
("0x1", "string", "String") => (sm(use_aliases, quote! { string::String }), false),
("0x2", "vec_map", "VecMap") => (sm(use_aliases, quote! { vec_map::VecMap }), false),
("0x2", "vec_set", "VecSet") => (sm(use_aliases, quote! { vec_set::VecSet }), false),
("0x2", "priority_queue", "Entry") => {
(sm(use_aliases, quote! { priority_queue::Entry }), false)
}
("0x2", "priority_queue", "PriorityQueue") => (
sm(use_aliases, quote! { priority_queue::PriorityQueue }),
false,
),
("0x2", "object_bag", "ObjectBag") => {
(sm(use_aliases, quote! { object_bag::ObjectBag }), true)
}
Expand All @@ -47,6 +55,9 @@ pub(crate) fn map_builtin(type_name: &TypeName, use_aliases: bool) -> Option<Bui
}
("0x1", "option", "Option") => (sm(use_aliases, quote! { containers::MoveOption }), false),
("0x2", "table", "Table") => (sm(use_aliases, quote! { containers::Table }), false),
("0x2", "table_vec", "TableVec") => {
(sm(use_aliases, quote! { containers::TableVec }), false)
}
("0x2", "dynamic_field", "Field") => {
(sm(use_aliases, quote! { containers::DynamicField }), false)
}
Expand Down
23 changes: 17 additions & 6 deletions sui-move-codegen/src/render/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ use quote::{format_ident, quote};

use crate::ir::{Ability, Function, NormalizedModule, NormalizedPackage, TypeRef, Visibility};

use super::{builtins, idents, types, RenderOptions};
use super::{builtins, idents, types, ExternalResolver, RenderOptions};

pub(crate) fn render_functions(
module: &NormalizedModule,
pkg: &NormalizedPackage,
opts: &RenderOptions,
resolver: Option<&ExternalResolver>,
) -> Vec<TokenStream> {
module
.functions
.iter()
.filter(|f| matches!(f.visibility, Visibility::Public))
.map(|f| render_function(module, f, pkg, opts))
.map(|f| render_function(module, f, pkg, opts, resolver))
.collect()
}

Expand All @@ -30,6 +31,7 @@ fn render_function(
f: &Function,
pkg: &NormalizedPackage,
opts: &RenderOptions,
resolver: Option<&ExternalResolver>,
) -> TokenStream {
let sm_call = if opts.use_aliases {
quote! { sm_call }
Expand All @@ -53,7 +55,8 @@ fn render_function(
.iter()
.map(|ty| quote! { spec.push_type_arg::<#ty>(); });

let (params, pushes, skipped_tx_context) = render_params_and_pushes(module, f, pkg, opts);
let (params, pushes, skipped_tx_context) =
render_params_and_pushes(module, f, pkg, opts, resolver);

let signature = move_signature_string(module, f);
let doc = doc_lines(&[
Expand Down Expand Up @@ -90,6 +93,7 @@ fn render_params_and_pushes(
f: &Function,
pkg: &NormalizedPackage,
opts: &RenderOptions,
resolver: Option<&ExternalResolver>,
) -> (Vec<TokenStream>, Vec<TokenStream>, bool) {
let sm_call = if opts.use_aliases {
quote! { sm_call }
Expand All @@ -112,10 +116,10 @@ fn render_params_and_pushes(
arg_idx += 1;
let (ref_mutable, inner) = split_ref(&p.ty);

let is_object = is_object_type(inner, f, pkg, opts);
let is_object = is_object_type(inner, f, pkg, opts, resolver);

if is_object {
let obj_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts);
let obj_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver);
let param_ty = if ref_mutable {
quote! { &mut impl #sm_call::ObjectArg<#obj_ty> }
} else {
Expand All @@ -128,7 +132,8 @@ fn render_params_and_pushes(
pushes.push(quote! { spec.push_arg(#arg_ident).expect("encode arg"); });
}
} else {
let value_ty = types::render_type_ref_in_module(inner, &module.name, pkg, opts);
let value_ty =
types::render_type_ref_in_module(inner, &module.name, pkg, opts, resolver);
params.push(quote! { #arg_ident: #value_ty });
pushes.push(quote! { spec.push_arg(&#arg_ident).expect("encode arg"); });
}
Expand Down Expand Up @@ -161,12 +166,18 @@ fn is_object_type(
f: &Function,
pkg: &NormalizedPackage,
opts: &RenderOptions,
resolver: Option<&ExternalResolver>,
) -> bool {
match ty {
TypeRef::Datatype { type_name, .. } => {
if let Some(builtin) = builtins::map_builtin(type_name, opts.use_aliases) {
return builtin.is_key;
}
if let Some(resolver) = resolver {
if let Some(is_key) = resolver.type_has_key(type_name) {
return is_key;
}
}
pkg.modules
.get(&type_name.module)
.and_then(|m| {
Expand Down
74 changes: 74 additions & 0 deletions sui-move-codegen/src/render/externals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! External package resolution for rendering cross-package type references.
//!
//! Codegen is deterministic by default: if a type reference points at an unknown external
//! package, rendering emits a `compile_error!` to force the caller to either:
//! - provide an external mapping, or
//! - generate bindings for that package as well.

use std::collections::BTreeMap;

use crate::ir::{Ability, NormalizedPackage, TypeName};

/// Resolver for packages that are not the one currently being rendered.
///
/// This lets the renderer:
/// - map external Move types to Rust paths like `dep_crate::module::Type`
/// - determine whether external types have the `key` ability (to generate object-arg signatures)
#[derive(Clone, Debug, Default)]
pub struct ExternalResolver {
crate_by_address: BTreeMap<String, String>,
has_key_by_type: BTreeMap<TypeName, bool>,
}

impl ExternalResolver {
/// Create an empty resolver.
pub fn new() -> Self {
Self::default()
}

/// Register a package under a Rust crate name.
///
/// Both `storage_id` and `original_id` (if present) are mapped to the same crate name to
/// make resolution robust across package upgrades.
pub fn add_package(&mut self, pkg: &NormalizedPackage, crate_name: impl Into<String>) {
let crate_name = crate_name.into();
self.crate_by_address
.insert(pkg.storage_id.clone(), crate_name.clone());
if let Some(orig) = &pkg.original_id {
self.crate_by_address
.insert(orig.clone(), crate_name.clone());
}

for module in pkg.modules.values() {
for dt in &module.datatypes {
let has_key = dt.abilities.contains(&Ability::Key);
self.has_key_by_type.insert(dt.type_name.clone(), has_key);

// Be robust to package upgrades: callers may reference either the storage id or
// original id in type signatures.
if let Some(orig) = &pkg.original_id {
if dt.type_name.address != *orig {
let mut alias = dt.type_name.clone();
alias.address = orig.clone();
self.has_key_by_type.insert(alias, has_key);
}
}
if dt.type_name.address != pkg.storage_id {
let mut alias = dt.type_name.clone();
alias.address = pkg.storage_id.clone();
self.has_key_by_type.insert(alias, has_key);
}
}
}
}

/// Look up the Rust crate name for a Move package address.
pub fn crate_name_for_address(&self, address: &str) -> Option<&str> {
self.crate_by_address.get(address).map(|s| s.as_str())
}

/// Whether a fully-qualified Move type has the `key` ability.
pub fn type_has_key(&self, type_name: &TypeName) -> Option<bool> {
self.has_key_by_type.get(type_name).copied()
}
}
Loading
Loading