From 8a5902b0074537f96384ea0829d54543adeb2367 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 22 Jun 2024 10:30:09 +0800 Subject: [PATCH] feat(sozo): add Cartridge's Controller-based account (#2069) --- Cargo.lock | 350 +++++++++++++++++- Cargo.toml | 4 + bin/sozo/Cargo.toml | 9 +- bin/sozo/src/commands/auth.rs | 11 +- bin/sozo/src/commands/execute.rs | 5 +- bin/sozo/src/commands/migrate.rs | 22 +- .../commands/options/account/controller.rs | 238 ++++++++++++ bin/sozo/src/commands/options/account/mod.rs | 67 +++- bin/sozo/src/commands/options/account/type.rs | 27 ++ bin/sozo/src/commands/register.rs | 3 +- bin/sozo/src/utils.rs | 23 +- 11 files changed, 711 insertions(+), 48 deletions(-) create mode 100644 bin/sozo/src/commands/options/account/controller.rs diff --git a/Cargo.lock b/Cargo.lock index 5c48da049b..12305480f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,42 @@ dependencies = [ "regex", ] +[[package]] +name = "account_sdk" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/controller?rev=e8276cfffe48b5d09f0598026d13635374204b72#e8276cfffe48b5d09f0598026d13635374204b72" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "cairo-lang-starknet 2.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "coset", + "ecdsa", + "futures", + "indexmap 2.2.6", + "js-sys", + "lazy_static", + "p256", + "primitive-types", + "rand_core", + "serde", + "serde_json", + "sha2 0.10.8", + "starknet", + "starknet-crypto 0.6.2", + "thiserror", + "tokio", + "toml 0.8.13", + "u256-literal", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "wasm-webauthn", + "web-sys", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -766,6 +802,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -1247,7 +1289,11 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower", "tower-layer", "tower-service", @@ -1760,6 +1806,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "cainome" +version = "0.2.3" +source = "git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f#a84dfe0f54116116f5a3c79642cd4111b1d4a5ac" +dependencies = [ + "anyhow", + "async-trait", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "camino", + "clap", + "clap_complete", + "convert_case 0.6.0", + "serde", + "serde_json", + "starknet", + "thiserror", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "cainome" version = "0.2.3" @@ -1767,9 +1836,9 @@ source = "git+https://github.com/cartridge-gg/cainome?rev=ec18eea5#ec18eea55a5c3 dependencies = [ "anyhow", "async-trait", - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "cainome-rs-macro", "camino", "clap", @@ -1784,6 +1853,16 @@ dependencies = [ "url", ] +[[package]] +name = "cainome-cairo-serde" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f#a84dfe0f54116116f5a3c79642cd4111b1d4a5ac" +dependencies = [ + "serde", + "starknet", + "thiserror", +] + [[package]] name = "cainome-cairo-serde" version = "0.1.0" @@ -1794,6 +1873,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-parser" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f#a84dfe0f54116116f5a3c79642cd4111b1d4a5ac" +dependencies = [ + "convert_case 0.6.0", + "quote", + "serde_json", + "starknet", + "syn 2.0.64", + "thiserror", +] + [[package]] name = "cainome-parser" version = "0.1.0" @@ -1807,14 +1899,32 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-rs" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f#a84dfe0f54116116f5a3c79642cd4111b1d4a5ac" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=a84dfe0f)", + "camino", + "prettyplease 0.2.20", + "proc-macro2", + "quote", + "serde_json", + "starknet", + "syn 2.0.64", + "thiserror", +] + [[package]] name = "cainome-rs" version = "0.1.0" source = "git+https://github.com/cartridge-gg/cainome?rev=ec18eea5#ec18eea55a5c3c563dc502c051a8df3dfab118a9" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "camino", "prettyplease 0.2.20", "proc-macro2", @@ -1831,9 +1941,9 @@ version = "0.1.0" source = "git+https://github.com/cartridge-gg/cainome?rev=ec18eea5#ec18eea55a5c3c563dc502c051a8df3dfab118a9" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "proc-macro2", "quote", "serde_json", @@ -3752,6 +3862,19 @@ dependencies = [ "yansi", ] +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + [[package]] name = "combine" version = "4.6.7" @@ -3950,6 +4073,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "coset" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8aad850c1f86daa47e812913051eb5a26c4d9fb4242a89178bf99b946e4e3c" +dependencies = [ + "ciborium", + "ciborium-io", +] + [[package]] name = "cpp_demangle" version = "0.4.3" @@ -4401,13 +4534,34 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro 0.12.0", +] + [[package]] name = "derive_builder" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" dependencies = [ - "derive_builder_macro", + "derive_builder_macro 0.20.0", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4422,13 +4576,23 @@ dependencies = [ "syn 2.0.64", ] +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core 0.12.0", + "syn 1.0.109", +] + [[package]] name = "derive_builder_macro" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ - "derive_builder_core", + "derive_builder_core 0.20.0", "syn 2.0.64", ] @@ -4580,7 +4744,7 @@ name = "dojo-bindgen" version = "0.7.2" dependencies = [ "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "camino", "chrono", "convert_case 0.6.0", @@ -4717,7 +4881,7 @@ dependencies = [ name = "dojo-types" version = "0.7.2" dependencies = [ - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "crypto-bigint", "hex", "itertools 0.12.1", @@ -4737,7 +4901,7 @@ dependencies = [ "assert_fs", "assert_matches", "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "cairo-lang-filesystem 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-project 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-starknet 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", @@ -6470,6 +6634,64 @@ dependencies = [ "minilp", ] +[[package]] +name = "graphql-introspection-query" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d" +dependencies = [ + "serde", +] + +[[package]] +name = "graphql-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" +dependencies = [ + "combine 3.8.1", + "thiserror", +] + +[[package]] +name = "graphql_client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa" +dependencies = [ + "graphql_query_derive", + "serde", + "serde_json", +] + +[[package]] +name = "graphql_client_codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506" +dependencies = [ + "graphql-introspection-query", + "graphql-parser", + "heck 0.4.1", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "graphql_query_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c" +dependencies = [ + "graphql_client_codegen", + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "group" version = "0.13.0" @@ -7488,7 +7710,7 @@ checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", - "combine", + "combine 4.6.7", "jni-sys", "log", "thiserror", @@ -11900,7 +12122,7 @@ dependencies = [ "create-output-dir", "data-encoding", "deno_task_shell", - "derive_builder", + "derive_builder 0.20.0", "directories", "dunce", "fs4", @@ -11974,7 +12196,7 @@ version = "1.12.0" source = "git+https://github.com/software-mansion/scarb?rev=f1aa7b09507a84d209d83b2fa80472c82605cc43#f1aa7b09507a84d209d83b2fa80472c82605cc43" dependencies = [ "camino", - "derive_builder", + "derive_builder 0.20.0", "semver 1.0.23", "serde", "serde_json", @@ -12209,6 +12431,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_bytes" version = "0.11.14" @@ -12262,6 +12495,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -12561,6 +12804,31 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slot" +version = "0.8.0" +source = "git+https://github.com/cartridge-gg/slot?rev=1d0c9f7#1d0c9f79122202bb448279f68c97092413e72cd3" +dependencies = [ + "anyhow", + "axum", + "dirs 5.0.1", + "graphql_client", + "hyper 0.14.28", + "reqwest 0.11.27", + "serde", + "serde_json", + "serde_with 2.3.3", + "starknet", + "tempfile", + "thiserror", + "tokio", + "tower-http", + "tracing", + "url", + "urlencoding", + "webbrowser", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -12655,11 +12923,12 @@ dependencies = [ name = "sozo" version = "0.7.2" dependencies = [ + "account_sdk", "anyhow", "assert_fs", "async-trait", "bigdecimal 0.4.3", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "cairo-lang-compiler 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-defs 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-filesystem 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", @@ -12698,6 +12967,7 @@ dependencies = [ "semver 1.0.23", "serde", "serde_json", + "slot", "smol_str", "snapbox", "sozo-ops", @@ -12718,7 +12988,7 @@ dependencies = [ "anyhow", "assert_fs", "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "cairo-lang-compiler 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-defs 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", "cairo-lang-filesystem 2.6.3 (git+https://github.com/starkware-libs/cairo?rev=d9984ef58e2f704909e271f2f01327f520ded632)", @@ -14118,7 +14388,7 @@ dependencies = [ "anyhow", "async-trait", "base64 0.21.7", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "camino", "chrono", "crypto-bigint", @@ -14241,7 +14511,7 @@ version = "0.7.2" dependencies = [ "anyhow", "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?rev=ec18eea5)", "chrono", "crypto-bigint", "dojo-test-utils", @@ -14598,6 +14868,16 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "types-test" version = "0.7.2" +[[package]] +name = "u256-literal" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39a1ed842845c7cdbcf413a186dba1fb3cf8b13753c21e5572bf64aadec4778" +dependencies = [ + "primitive-types", + "syn 1.0.109", +] + [[package]] name = "ucd-trie" version = "0.1.6" @@ -14710,6 +14990,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "unsigned-varint" version = "0.7.2" @@ -14904,6 +15193,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -15029,6 +15320,25 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-webauthn" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/wasm-webauthn?rev=972693f#972693fdeaa5dbcf7eee181c1e4aad5dfdb73a82" +dependencies = [ + "ciborium", + "coset", + "derive_builder 0.12.0", + "js-sys", + "serde", + "serde-wasm-bindgen", + "serde_bytes", + "thiserror", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" diff --git a/Cargo.toml b/Cargo.toml index 6bf2173cee..7048dcf5ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -215,6 +215,10 @@ alloy-sol-types = { version = "0.7.2", default-features = false } criterion = "0.5.1" +# Controller integration +account_sdk = { git = "https://github.com/cartridge-gg/controller", rev = "e8276cfffe48b5d09f0598026d13635374204b72" } +slot = { git = "https://github.com/cartridge-gg/slot", rev = "1d0c9f7" } + [patch.crates-io] cairo-felt = { git = "https://github.com/dojoengine/cairo-rs.git", rev = "1031381" } cairo-vm = { git = "https://github.com/dojoengine/cairo-rs.git", rev = "1031381" } diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 9ddf45dae6..cf01f1302f 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -6,6 +6,9 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +account_sdk = { workspace = true, optional = true } +slot = { workspace = true, optional = true } + anyhow.workspace = true async-trait.workspace = true bigdecimal = "0.4.1" @@ -25,11 +28,11 @@ clap-verbosity-flag.workspace = true clap.workspace = true clap_complete.workspace = true console.workspace = true +derive_more.workspace = true dojo-bindgen.workspace = true dojo-lang.workspace = true dojo-types.workspace = true dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] } -derive_more.workspace = true futures.workspace = true hex = "0.4.3" hex-literal = "0.4.1" @@ -63,3 +66,7 @@ assert_fs.workspace = true dojo-test-utils = { workspace = true, features = [ "build-examples" ] } katana-runner.workspace = true snapbox = "0.4.6" + +[features] +controller = [ "dep:account_sdk", "dep:slot" ] +default = [ "controller" ] diff --git a/bin/sozo/src/commands/auth.rs b/bin/sozo/src/commands/auth.rs index ef6a687406..01e3c6189d 100644 --- a/bin/sozo/src/commands/auth.rs +++ b/bin/sozo/src/commands/auth.rs @@ -73,6 +73,7 @@ impl AuthArgs { env_metadata, kind, transaction, + config, )) } AuthCommand::Revoke { kind, world, starknet, account, transaction } => { @@ -84,6 +85,7 @@ impl AuthArgs { env_metadata, kind, transaction, + config, )) } } @@ -115,6 +117,7 @@ pub enum AuthKind { }, } +#[allow(clippy::too_many_arguments)] pub async fn grant( ui: &Ui, world: WorldOptions, @@ -123,10 +126,11 @@ pub async fn grant( env_metadata: Option, kind: AuthKind, transaction: TransactionOptions, + config: &Config, ) -> Result<()> { trace!(?kind, ?world, ?starknet, ?account, ?transaction, "Executing Grant command."); let world = - utils::world_from_env_metadata(world, account, starknet, &env_metadata).await.unwrap(); + utils::world_from_env_metadata(world, account, starknet, &env_metadata, config).await?; match kind { AuthKind::Writer { models_contracts } => { @@ -146,6 +150,7 @@ pub async fn grant( } } +#[allow(clippy::too_many_arguments)] pub async fn revoke( ui: &Ui, world: WorldOptions, @@ -154,10 +159,12 @@ pub async fn revoke( env_metadata: Option, kind: AuthKind, transaction: TransactionOptions, + config: &Config, ) -> Result<()> { trace!(?kind, ?world, ?starknet, ?account, ?transaction, "Executing Revoke command."); let world = - utils::world_from_env_metadata(world, account, starknet, &env_metadata).await.unwrap(); + utils::world_from_env_metadata(world, account, starknet, &env_metadata, config).await?; + match kind { AuthKind::Writer { models_contracts } => { trace!( diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index 40e3305e42..243c1595a8 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -55,9 +55,10 @@ impl ExecuteArgs { self.account, self.starknet, &env_metadata, + config, ) - .await - .unwrap(); + .await?; + let tx_config = self.transaction.into(); trace!( diff --git a/bin/sozo/src/commands/migrate.rs b/bin/sozo/src/commands/migrate.rs index 02a8bf46aa..4ab758ed9e 100644 --- a/bin/sozo/src/commands/migrate.rs +++ b/bin/sozo/src/commands/migrate.rs @@ -6,18 +6,18 @@ use dojo_world::migration::TxnConfig; use katana_rpc_api::starknet::RPC_SPEC_VERSION; use scarb::core::{Config, Workspace}; use sozo_ops::migration; -use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; +use starknet::accounts::{Account, ConnectedAccount}; use starknet::core::types::{BlockId, BlockTag, FieldElement, StarknetError}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider, ProviderError}; -use starknet::signers::LocalWallet; use tracing::trace; -use super::options::account::AccountOptions; +use super::options::account::{AccountOptions, SozoAccount}; use super::options::starknet::StarknetOptions; use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; +use crate::commands::options::account::WorldAddressOrName; #[derive(Debug, Args)] pub struct MigrateArgs { @@ -156,11 +156,7 @@ pub async fn setup_env<'a>( world: WorldOptions, name: &str, env: Option<&'a Environment>, -) -> Result<( - Option, - SingleOwnerAccount, LocalWallet>, - String, -)> { +) -> Result<(Option, SozoAccount>, String)> { trace!("Setting up environment."); let ui = ws.config().ui(); @@ -190,8 +186,14 @@ pub async fn setup_env<'a>( .with_context(|| "Cannot parse chain_id as string")?; trace!(chain_id); - let mut account = account.account(provider, env).await?; - account.set_block_id(BlockId::Tag(BlockTag::Pending)); + let account = { + // This is mainly for controller account for creating policies. + let world_address_or_name = world_address + .map(WorldAddressOrName::Address) + .unwrap_or(WorldAddressOrName::Name(name.to_string())); + + account.account(provider, world_address_or_name, &starknet, env, ws.config()).await? + }; let address = account.address(); diff --git a/bin/sozo/src/commands/options/account/controller.rs b/bin/sozo/src/commands/options/account/controller.rs new file mode 100644 index 0000000000..f110a9b7f3 --- /dev/null +++ b/bin/sozo/src/commands/options/account/controller.rs @@ -0,0 +1,238 @@ +use account_sdk::account::session::hash::{AllowedMethod, Session}; +use account_sdk::account::session::SessionAccount; +use account_sdk::deploy_contract::UDC_ADDRESS; +use account_sdk::signers::HashSigner; +use anyhow::{Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; +use dojo_world::manifest::{BaseManifest, DojoContract, Manifest}; +use dojo_world::migration::strategy::generate_salt; +use scarb::core::Config; +use slot::session::Policy; +use starknet::core::types::contract::{AbiEntry, StateMutability}; +use starknet::core::types::FieldElement; +use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; +use starknet::macros::short_string; +use starknet::providers::Provider; +use starknet::signers::SigningKey; +use starknet_crypto::poseidon_hash_single; +use tracing::trace; +use url::Url; + +use super::WorldAddressOrName; + +pub type ControllerSessionAccount

= SessionAccount; + +/// Create a new Catridge Controller account based on session key. +#[tracing::instrument( + name = "create_controller", + skip(rpc_url, provider, world_addr_or_name, config) +)] +pub async fn create_controller

( + // Ideally we can get the url from the provider so we dont have to pass an extra url param here + rpc_url: Url, + provider: P, + // Use to either specify the world address or compute the world address from the world name + world_addr_or_name: WorldAddressOrName, + config: &Config, +) -> Result> +where + P: Provider, + P: Send + Sync, +{ + let chain_id = provider.chain_id().await?; + let credentials = slot::credential::Credentials::load()?; + + let username = credentials.account.id; + let contract_address = credentials.account.contract_address; + + trace!( + %username, + chain = format!("{chain_id:#x}"), + address = format!("{contract_address:#x}"), + "Creating Controller session account" + ); + + // Check if the session exists, if not create a new one + let session_details = match slot::session::get(chain_id)? { + Some(session) => { + trace!(expires_at = %session.expires_at, policies = session.policies.len(), "Found existing session."); + + // Perform policies diff check. For security reasons, we will always create a new + // session here if the current policies are different from the existing + // session. TODO(kariy): maybe don't need to update if current policies is a + // subset of the existing policies. + let policies = collect_policies(world_addr_or_name, contract_address, config)?; + + if policies != session.policies { + trace!( + new_policies = policies.len(), + existing_policies = session.policies.len(), + "Policies have changed. Creating new session." + ); + + let session = slot::session::create(rpc_url, &policies).await?; + slot::session::store(chain_id, &session)?; + session + } else { + session + } + } + + // Create a new session if not found + None => { + trace!(%username, chain = format!("{chain_id:#}"), "Creating new session."); + let policies = collect_policies(world_addr_or_name, contract_address, config)?; + let session = slot::session::create(rpc_url, &policies).await?; + slot::session::store(chain_id, &session)?; + session + } + }; + + let methods = session_details + .policies + .into_iter() + .map(|p| AllowedMethod::new(p.target, &p.method)) + .collect::, _>>()?; + + // Copied from `account-wasm` + let guardian = SigningKey::from_secret_scalar(short_string!("CARTRIDGE_GUARDIAN")); + let signer = SigningKey::from_secret_scalar(session_details.credentials.private_key); + // TODO(kariy): make `expires_at` a `u64` type in the session struct + let expires_at = session_details.expires_at.parse::()?; + let session = Session::new(methods, expires_at, &signer.signer())?; + + let session_account = SessionAccount::new( + provider, + signer, + guardian, + contract_address, + chain_id, + session_details.credentials.authorization, + session, + ); + + Ok(session_account) +} + +/// Policies are the building block of a session key. It's what defines what methods are allowed for +/// an external signer to execute using the session key. +/// +/// This function collect all the contracts' methods in the current project according to the +/// project's base manifest ( `/manifests//base` ) and convert them into policies. +fn collect_policies( + world_addr_or_name: WorldAddressOrName, + user_address: FieldElement, + config: &Config, +) -> Result> { + let root_dir = config.root(); + let manifest = get_project_base_manifest(root_dir, config.profile().as_str())?; + let policies = + collect_policies_from_base_manifest(world_addr_or_name, user_address, root_dir, manifest)?; + trace!(policies_count = policies.len(), "Extracted policies from project."); + Ok(policies) +} + +fn get_project_base_manifest(root_dir: &Utf8Path, profile: &str) -> Result { + let mut manifest_path = root_dir.to_path_buf(); + manifest_path.extend(["manifests", profile, "base"]); + Ok(BaseManifest::load_from_path(&manifest_path)?) +} + +fn collect_policies_from_base_manifest( + world_address: WorldAddressOrName, + user_address: FieldElement, + base_path: &Utf8Path, + manifest: BaseManifest, +) -> Result> { + let mut policies: Vec = Vec::new(); + let base_path: Utf8PathBuf = base_path.to_path_buf(); + + // compute the world address here if it's a name + let world_address = get_dojo_world_address(world_address, &manifest)?; + + // get methods from all project contracts + for contract in manifest.contracts { + let contract_address = get_dojo_contract_address(world_address, &contract); + let abis = contract.inner.abi.unwrap().load_abi_string(&base_path)?; + let abis = serde_json::from_str::>(&abis)?; + policies_from_abis(&mut policies, &contract.name, contract_address, &abis); + } + + // get method from world contract + let abis = manifest.world.inner.abi.unwrap().load_abi_string(&base_path)?; + let abis = serde_json::from_str::>(&abis)?; + policies_from_abis(&mut policies, &manifest.world.name, world_address, &abis); + + // special policy for sending declare tx + // corresponds to [account_sdk::account::DECLARATION_SELECTOR] + let method = "__declare_transaction__".to_string(); + policies.push(Policy { target: user_address, method }); + trace!("Adding declare transaction policy"); + + // for deploying using udc + let method = "deployContract".to_string(); + policies.push(Policy { target: *UDC_ADDRESS, method }); + trace!("Adding UDC deployment policy"); + + Ok(policies) +} + +/// Recursively extract methods and convert them into policies from the all the +/// ABIs in the project. +fn policies_from_abis( + policies: &mut Vec, + contract_name: &str, + contract_address: FieldElement, + entries: &[AbiEntry], +) { + for entry in entries { + match entry { + AbiEntry::Function(f) => { + // we only create policies for non-view functions + if let StateMutability::External = f.state_mutability { + let policy = Policy { target: contract_address, method: f.name.to_string() }; + trace!(name = contract_name, target = format!("{:#x}", policy.target), method = %policy.method, "Adding policy"); + policies.push(policy); + } + } + + AbiEntry::Interface(i) => { + policies_from_abis(policies, contract_name, contract_address, &i.items) + } + + _ => {} + } + } +} + +fn get_dojo_contract_address( + world_address: FieldElement, + manifest: &Manifest, +) -> FieldElement { + if let Some(address) = manifest.inner.address { + address + } else { + let salt = generate_salt(&manifest.name); + get_contract_address(salt, manifest.inner.base_class_hash, &[], world_address) + } +} + +fn get_dojo_world_address( + world_address: WorldAddressOrName, + manifest: &BaseManifest, +) -> Result { + match world_address { + WorldAddressOrName::Address(addr) => Ok(addr), + WorldAddressOrName::Name(name) => { + let seed = cairo_short_string_to_felt(&name).context("Failed to parse World name.")?; + let salt = poseidon_hash_single(seed); + let address = get_contract_address( + salt, + manifest.world.inner.original_class_hash, + &[manifest.base.inner.original_class_hash], + FieldElement::ZERO, + ); + Ok(address) + } + } +} diff --git a/bin/sozo/src/commands/options/account/mod.rs b/bin/sozo/src/commands/options/account/mod.rs index eba8d53d63..8a33772848 100644 --- a/bin/sozo/src/commands/options/account/mod.rs +++ b/bin/sozo/src/commands/options/account/mod.rs @@ -3,17 +3,35 @@ use std::str::FromStr; use anyhow::{anyhow, Context, Result}; use clap::Args; use dojo_world::metadata::Environment; +use scarb::core::Config; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::types::{BlockId, BlockTag, FieldElement}; use starknet::providers::Provider; use starknet::signers::LocalWallet; use tracing::trace; +use url::Url; use super::signer::SignerOptions; +use super::starknet::StarknetOptions; use super::DOJO_ACCOUNT_ADDRESS_ENV_VAR; +#[cfg(feature = "controller")] +pub mod controller; mod r#type; +#[cfg(feature = "controller")] +use controller::ControllerSessionAccount; +pub use r#type::*; + +/// Helper type for identifying how the world address will be provided. +/// If it's a name, it will be used as the seed for computing the address. +/// Else if it's an address, it will be used directly. +#[derive(Debug)] +pub enum WorldAddressOrName { + Address(FieldElement), + Name(String), +} + // INVARIANT: // - For commandline: we can either specify `private_key` or `keystore_path` along with // `keystore_password`. This is enforced by Clap. @@ -31,7 +49,6 @@ pub struct AccountOptions { #[arg(help_heading = "Controller options")] #[arg(help = "Use Slot's Controller account")] #[cfg(feature = "controller")] - #[arg(conflicts_with = "signer")] pub controller: bool, #[command(flatten)] @@ -45,7 +62,48 @@ pub struct AccountOptions { } impl AccountOptions { + /// Create a new Catridge Controller account based on session key. + #[cfg(feature = "controller")] + pub async fn controller

( + &self, + rpc_url: Url, + provider: P, + world_address_or_name: WorldAddressOrName, + config: &Config, + ) -> Result> + where + P: Provider, + P: Send + Sync, + { + controller::create_controller(rpc_url, provider, world_address_or_name, config) + .await + .context("Failed to create a Controller account") + } + pub async fn account

( + &self, + provider: P, + world_address_or_name: WorldAddressOrName, + starknet: &StarknetOptions, + env_metadata: Option<&Environment>, + config: &Config, + ) -> Result> + where + P: Provider, + P: Send + Sync, + { + #[cfg(feature = "controller")] + if self.controller { + let url = starknet.url(env_metadata)?; + let account = self.controller(url, provider, world_address_or_name, config).await?; + return Ok(SozoAccount::from(account)); + } + + let account = self.std_account(provider, env_metadata).await?; + Ok(SozoAccount::from(account)) + } + + pub async fn std_account

( &self, provider: P, env_metadata: Option<&Environment>, @@ -54,14 +112,13 @@ impl AccountOptions { P: Provider, P: Send + Sync, { - trace!(account_options=?self, "Creating account."); let account_address = self.account_address(env_metadata)?; trace!(?account_address, "Account address determined."); let signer = self.signer.signer(env_metadata, false)?; trace!(?signer, "Signer obtained."); - let chain_id = provider.chain_id().await.context("Failed to retrieve network chain id.")?; + let chain_id = provider.chain_id().await?; trace!(?chain_id); let encoding = if self.legacy { ExecutionEncoding::Legacy } else { ExecutionEncoding::New }; @@ -177,7 +234,7 @@ mod tests { // HACK: SingleOwnerAccount doesn't expose a way to check `encoding` type used in struct, so // checking it by encoding a dummy call and checking which method it used to encode the call - let account = cmd.account.account(runner.provider(), None).await.unwrap(); + let account = cmd.account.std_account(runner.provider(), None).await.unwrap(); let result = account.encode_calls(&dummy_call); // 0x0 is the data offset. assert!(*result.get(3).unwrap() == FieldElement::from_hex_be("0x0").unwrap()); @@ -197,7 +254,7 @@ mod tests { // HACK: SingleOwnerAccount doesn't expose a way to check `encoding` type used in struct, so // checking it by encoding a dummy call and checking which method it used to encode the call - let account = cmd.account.account(runner.provider(), None).await.unwrap(); + let account = cmd.account.std_account(runner.provider(), None).await.unwrap(); let result = account.encode_calls(&dummy_call); // 0x2 is the Calldata len. assert!(*result.get(3).unwrap() == FieldElement::from_hex_be("0x2").unwrap()); diff --git a/bin/sozo/src/commands/options/account/type.rs b/bin/sozo/src/commands/options/account/type.rs index eb388398b0..6368fabb71 100644 --- a/bin/sozo/src/commands/options/account/type.rs +++ b/bin/sozo/src/commands/options/account/type.rs @@ -11,10 +11,17 @@ use starknet::core::types::{FieldElement, FlattenedSierraClass}; use starknet::providers::Provider; use starknet::signers::LocalWallet; +#[cfg(feature = "controller")] +use super::controller::ControllerSessionAccount; + #[derive(Debug, thiserror::Error)] pub enum SozoAccountSignError { #[error(transparent)] Standard(#[from] SignError), + + #[cfg(feature = "controller")] + #[error(transparent)] + Controller(#[from] account_sdk::signers::SignError), } /// To unify the account types, we define a wrapper type that implements the @@ -30,6 +37,9 @@ where P: Provider, { Standard(SingleOwnerAccount), + + #[cfg(feature = "controller")] + Controller(ControllerSessionAccount

), } #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -44,12 +54,16 @@ where fn address(&self) -> FieldElement { match self { Self::Standard(account) => account.address(), + #[cfg(feature = "controller")] + Self::Controller(account) => account.address(), } } fn chain_id(&self) -> FieldElement { match self { Self::Standard(account) => account.chain_id(), + #[cfg(feature = "controller")] + Self::Controller(account) => account.chain_id(), } } @@ -79,6 +93,8 @@ where ) -> Result, Self::SignError> { let result = match self { Self::Standard(account) => account.sign_execution(execution, query_only).await?, + #[cfg(feature = "controller")] + Self::Controller(account) => account.sign_execution(execution, query_only).await?, }; Ok(result) } @@ -90,6 +106,8 @@ where ) -> Result, Self::SignError> { let result = match self { Self::Standard(account) => account.sign_declaration(declaration, query_only).await?, + #[cfg(feature = "controller")] + Self::Controller(account) => account.sign_declaration(declaration, query_only).await?, }; Ok(result) } @@ -104,6 +122,11 @@ where let result = account.sign_legacy_declaration(declaration, query_only).await?; Ok(result) } + #[cfg(feature = "controller")] + Self::Controller(account) => { + let result = account.sign_legacy_declaration(declaration, query_only).await?; + Ok(result) + } } } } @@ -116,6 +139,8 @@ where fn encode_calls(&self, calls: &[Call]) -> Vec { match self { Self::Standard(account) => account.encode_calls(calls), + #[cfg(feature = "controller")] + Self::Controller(account) => account.encode_calls(calls), } } } @@ -130,6 +155,8 @@ where fn provider(&self) -> &Self::Provider { match self { Self::Standard(account) => account.provider(), + #[cfg(feature = "controller")] + Self::Controller(account) => account.provider(), } } } diff --git a/bin/sozo/src/commands/register.rs b/bin/sozo/src/commands/register.rs index 8893cea2d4..f69fac41d2 100644 --- a/bin/sozo/src/commands/register.rs +++ b/bin/sozo/src/commands/register.rs @@ -60,7 +60,8 @@ impl RegisterArgs { config.tokio_handle().block_on(async { let world = - utils::world_from_env_metadata(world, account, starknet, &env_metadata).await?; + utils::world_from_env_metadata(world, account, starknet, &env_metadata, config) + .await?; let provider = world.account.provider(); let mut world_reader = WorldContractReader::new(world_address, &provider); world_reader.set_block(BlockId::Tag(BlockTag::Pending)); diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index e65c0a2838..51d59579cf 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -7,12 +7,10 @@ use dojo_world::contracts::WorldContractReader; use dojo_world::metadata::{dojo_metadata_from_workspace, Environment}; use scarb::core::{Config, TomlManifest}; use semver::Version; -use starknet::accounts::SingleOwnerAccount; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; -use starknet::signers::LocalWallet; -use crate::commands::options::account::AccountOptions; +use crate::commands::options::account::{AccountOptions, SozoAccount, WorldAddressOrName}; use crate::commands::options::starknet::StarknetOptions; use crate::commands::options::world::WorldOptions; @@ -61,11 +59,22 @@ pub async fn world_from_env_metadata( account: AccountOptions, starknet: StarknetOptions, env_metadata: &Option, -) -> Result, LocalWallet>>, Error> { - let world_address = world.address(env_metadata.as_ref())?; - let provider = starknet.provider(env_metadata.as_ref())?; + config: &Config, +) -> Result>>, Error> { + let env_metadata = env_metadata.as_ref(); + + let world_address = world.address(env_metadata)?; + let provider = starknet.provider(env_metadata)?; + let account = account + .account( + provider, + WorldAddressOrName::Address(world_address), + &starknet, + env_metadata, + config, + ) + .await?; - let account = account.account(provider, env_metadata.as_ref()).await?; Ok(WorldContract::new(world_address, account)) }