diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..09c98de --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,21 @@ +name: Rust + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -D warnings + +jobs: + build: + name: build + steps: + - uses: actions/checkout@v3 + - name: Install Rust + run: rustup show + - name: Build + run: cargo build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1fd2da8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,555 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rustix" +version = "0.38.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "rustyline-derive", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a32af5427251d2e4be14fc151eabe18abb4a7aad5efee7044da9f096c906a43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "sov-snap-generator" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "rustyline", + "toml", +] + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..793a396 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sov-snap-generator" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +authors = ["Sovereign Labs "] +homepage = "https://www.sovereign.xyz" +repository = "https://github.com/Sovereign-Labs/sov-snap-generator" +description = "Utility to generate Metamask Snaps from Sovereign SDK projects" +readme = "README.md" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.4", features = ["derive"] } +rustyline = { version = "12.0", features = ["derive"] } +toml = "0.8" diff --git a/README.md b/README.md index e8eb4ff..6d61a95 100644 --- a/README.md +++ b/README.md @@ -1 +1,129 @@ -# sov-snap-generator \ No newline at end of file +# sov-snap-generator + +This utility generates [Metamask Snaps](https://metamask.io/snaps/) for Sovereign SDK module implementations. + +## Requirements + +- [Rust](https://www.rust-lang.org/tools/install) + +## Installation + +```bash +cargo install --git https://github.com/Sovereign-Labs/sov-snap-generator --tag "v0.1.0" +``` + +## Usage + +#### Example module + +This example will re-export the `RuntimeCall` from `demo-stf`. + +First, we create the project: + +```bash +cargo new --lib sov-runtime +cd sov-runtime +``` + +Then, we update the `Cargo.toml` with the following: + +```toml +[package] +name = "sov-runtime" +version = "0.1.0" +edition = "2021" + +[dependencies] + +## Required dependencies for the Snap +borsh = "0.10.3" +serde_json = "1.0" +sov-modules-api = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be", features = ["serde"] } + +## Example definition of a module `RuntimeCall` +## Will be replaced by the user module implementation +demo-stf = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be", features = ["serde"] } +sov-mock-da = { git = "https://github.com/Sovereign-Labs/sovereign-sdk.git", rev = "df169be" } +``` + +Then, we fetch a default `constants.json`, required for module compilation: + +```bash +wget https://raw.githubusercontent.com/Sovereign-Labs/sovereign-sdk/d42e289f26b9824b5ed54dbfbda94007dee305b2/constants.json +``` + +Finally, we update the `src/lib.rs` with the following: + +```rust +/// The `Context` will be used to define the asymmetric key pair. +pub use sov_modules_api::default_context::ZkDefaultContext as Context; + +/// The `DaSpec` will be used to define the runtime specification. +pub use sov_mock_da::MockDaSpec as DaSpec; + +/// The `RuntimeCall` will be the call message of the transaction to be signed. This is normally generated automatically by the SDK via the `DispatchCall` derive macro. +pub use demo_stf::runtime::RuntimeCall; +``` + +The utility will look, by default, for definitions of `Context`, `DaSpec`, and `RuntimeCall` at the root of the project. They, however, can be replaced by other paths. + +For a sanity check, run the following: + +```bash +cargo check +``` + +#### Generate the Snap + +Some prompts can be provided via CLI arguments. For more information, run: + +```bash +sov-snap-generator --help +``` + +On the root of the project, run: + +```bash +sov-snap-generator +``` + +The first prompt will ask for the `path` of the project. It defaults to the current directory, so you can simply press `Enter`. + +```bash +Insert the path to your `Cargo.toml` +> /home/sovereign/sov-runtime +``` + +The next prompt asks for the manifest definition to use the module as dependency. It defaults to the parent directory of the resolved project manifest file. + +The generated WASM file must have some dependencies. The next prompts will default to the dependency specified on the project manifest file, if present, and will ask for the following items, in order: + +- Base project, usually a path pointing to the parent directory of the project manifest file. +- [borsh](https://crates.io/crates/borsh) +- [serde_json](https://crates.io/crates/serde_json) +- [sov-modules-api](https://github.com/Sovereign-Labs/sovereign-sdk/tree/d42e289f26b9824b5ed54dbfbda94007dee305b2/module-system/sov-modules-api) + +The next step is to define the target directory in which the generated Snap will be placed. It defaults to a new directory, neighbor to the current project, suffixed by `-snap`. + +The WASM files depends on a couple of definitions, usually customized by the module implementation. It will, by default, search the root of the project for exports of `Context`, `DaSpec`, and `RuntimeCall`. The next prompts will ask for such paths; if your implementation diverges from this standard, your just replace these items for their fully qualified paths. + +The template snap will be downloaded from a github release. The next prompts queries for the repository origin and its branch/tag. + +After the installation is executed, you should see a `Snap generated on ` message. + +#### Run a local development environment + +Requirements: [Metamask Flask](https://metamask.io/flask/) + +To start a web development environment with your Snap, run the following: + +```bash +cd ../sov-runtime-snap +yarn start +``` + +Your Snap will be available, by default, at `http://localhost:8000`. Click Connect/Reconnect to load the Snap into your Metamask Flask, and you can sign messages. + +This development environment will provide the possibility to submit a signed transaction to a [sov-sequencer](https://github.com/Sovereign-Labs/sovereign-sdk/tree/d42e289f26b9824b5ed54dbfbda94007dee305b2/full-node/sov-sequencer). However, most modern browsers queries external services for a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policy. + +You can either disable CORS in your browser, or set a proxy that will handle the CORS requests and forward the payload to the sequencer. diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..fe0615b --- /dev/null +++ b/src/args.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Parser)] +pub struct Cli { + /// Path to the directory that contains the manifest TOML file of the project. + #[arg(short, long)] + pub path: Option, + + /// Target directory to output the generated project. + #[arg(short, long)] + pub target: Option, + + /// Git remote to use when cloning the origin repository. + #[arg(short, long)] + pub origin: Option, + + /// Branch to use when cloning the origin repository. + #[arg(short, long)] + pub branch: Option, + + /// Context definition of the runtime spec. + #[arg(short, long)] + pub context: Option, + + /// DA definition of the runtime. + #[arg(short, long)] + pub da_spec: Option, + + /// Runtime call definition. + #[arg(short, long)] + pub runtime: Option, + + /// Defaults all inputs. + #[arg(long)] + pub defaults: bool, + + /// Skips all confirmations. + #[arg(long)] + pub force: bool, +} diff --git a/src/definitions.rs b/src/definitions.rs new file mode 100644 index 0000000..ef11726 --- /dev/null +++ b/src/definitions.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Definitions { + pub context: String, + pub da_spec: String, + pub runtime: String, +} diff --git a/src/interface.rs b/src/interface.rs new file mode 100644 index 0000000..52bf2e0 --- /dev/null +++ b/src/interface.rs @@ -0,0 +1,395 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + process::{self, Command, Output}, +}; + +use rustyline::{ + completion::{Completer, FilenameCompleter, Pair}, + error::ReadlineError, + hint::Hinter, + history::{DefaultHistory, History}, + Context, DefaultEditor, Editor, Helper, Highlighter, Validator, +}; + +use super::{ + args::Cli, + definitions::Definitions, + manifest::{Dependencies, Manifest}, +}; + +type FilenameEditor = Editor; + +#[derive(Debug, Default)] +pub struct Interface { + editor: Option, + filename_editor: Option, +} + +impl Interface { + const PROMPT: &'static str = "> "; + + /// Lazy load the inner editor. + fn editor(&mut self) -> anyhow::Result<&mut DefaultEditor> { + if self.editor.is_none() { + self.editor.replace(DefaultEditor::new()?); + } + + self.editor + .as_mut() + .ok_or_else(|| anyhow::Error::msg("unavailable editor")) + } + + /// Lazy load the inner editor. + fn filename_editor(&mut self) -> anyhow::Result<&mut FilenameEditor> { + if self.filename_editor.is_none() { + self.filename_editor.replace(FilenameEditor::new()?); + } + + self.filename_editor + .as_mut() + .ok_or_else(|| anyhow::Error::msg("unavailable editor")) + } + + fn read_line( + rl: &mut Editor, + args: &Cli, + prompt: Option<&str>, + mut initial_value: Option<&str>, + ) -> String + where + T: Helper, + U: History, + { + if args.defaults { + if let Some(value) = initial_value { + return value.to_string(); + } + } + + if let Some(prompt) = prompt { + println!("{}", prompt); + } + + loop { + let readline = match initial_value.take() { + Some(initial) => rl.readline_with_initial(Self::PROMPT, (initial, "")), + None => rl.readline(Self::PROMPT), + }; + + match readline { + Ok(line) => return line, + Err(ReadlineError::Interrupted) => { + process::exit(0); + } + Err(ReadlineError::Eof) => { + eprintln!("CTRL-D"); + process::exit(0); + } + Err(err) => { + eprintln!("Input error: {}", err); + } + } + } + } + + fn expect_char( + &mut self, + args: &Cli, + prompt: Option<&str>, + initial_value: Option, + expected: char, + ) -> anyhow::Result<()> { + // TODO implement an actual read char function + let rl = self.editor()?; + let initial_value = initial_value.map(|c| c.to_string()); + let expected = expected.to_lowercase().to_string(); + let line = Self::read_line(rl, args, prompt, initial_value.as_ref().map(|s| s.as_str())) + .to_lowercase(); + + anyhow::ensure!(line == expected, "Expected {}, got {}", expected, line); + Ok(()) + } + + fn read_path( + &mut self, + args: &Cli, + prompt: Option<&str>, + initial_value: Option<&str>, + ) -> anyhow::Result { + let rl = self.filename_editor()?; + let path = Self::read_line(rl, args, prompt, initial_value); + + Ok(PathBuf::from(path)) + } + + pub fn manifest(&mut self, args: &Cli) -> anyhow::Result { + let dir_or_file = match args.path.as_ref().cloned() { + Some(path) => path, + None => { + let cwd = env::current_dir()?.display().to_string(); + self.read_path( + args, + Some("Insert the path to your `Cargo.toml`"), + Some(&cwd), + )? + } + }; + + let path = if dir_or_file.is_dir() { + dir_or_file.join("Cargo.toml") + } else { + dir_or_file + }; + + if !path.exists() { + anyhow::bail!( + "Failed to locate `Cargo.toml`; {} does not exist", + path.display() + ); + } + + if !path.is_file() { + anyhow::bail!( + "Failed to locate `Cargo.toml`; {} is not a file", + path.display() + ); + } + + let path = path.canonicalize()?; + + println!("Using manifest `{}`...", path.display()); + let mut manifest = Manifest::try_from(path.as_path())?; + + if let Dependencies::Unresolved { + borsh, + serde_json, + sov_modules_api, + } = &manifest.dependencies + { + let rl = self.editor()?; + + let prompt = Some("Enter the manifest dependency for the base library"); + let initial = format!("{{ path = \"{}\" }}", manifest.parent.display()); + let base = Self::read_line(rl, args, prompt, Some(&initial)); + + let prompt = Some("Enter the manifest dependency for borsh"); + let borsh = Self::read_line(rl, args, prompt, borsh.as_ref().map(|s| s.as_str())); + + let prompt = Some("Enter the manifest dependency for serde_json"); + let serde_json = + Self::read_line(rl, args, prompt, serde_json.as_ref().map(|s| s.as_str())); + + let prompt = Some("Enter the manifest dependency for sov-modules-api"); + let sov_modules_api = Self::read_line( + rl, + args, + prompt, + sov_modules_api.as_ref().map(|s| s.as_str()), + ); + + manifest.dependencies = Dependencies::Resolved { + base, + borsh, + serde_json, + sov_modules_api, + }; + } + + println!("Reading project `{}`...", manifest.name); + Ok(manifest) + } + + pub fn target_dir(&mut self, args: &Cli, manifest: &Manifest) -> anyhow::Result { + let target = match args.target.as_ref().cloned() { + Some(target) => target, + None => { + let name = format!("{}-snap", manifest.name); + let target = manifest + .parent + .parent() + .unwrap_or(&manifest.parent) + .join(name) + .display() + .to_string(); + + self.read_path( + args, + Some("Insert the target directory"), + Some(target.as_str()), + )? + } + }; + + if target.is_file() { + anyhow::bail!( + "The provided target `{}` is a file; use a directory", + target.display() + ); + } + + if target.exists() { + if fs::remove_dir(&target).is_err() { + if !args.force { + let prompt = format!( + "The provided target `{}` is not empty and will be erased; confirm? [y/n]", + target.display() + ); + + if self + .expect_char(args, Some(&prompt), Some('n'), 'y') + .is_err() + { + anyhow::bail!("Operation aborted"); + } + } + + fs::remove_dir_all(&target)?; + } + } + + Ok(target) + } + + pub fn git_clone

(&mut self, args: &Cli, target: P) -> anyhow::Result + where + P: AsRef, + { + let origin = match args.origin.as_ref().cloned() { + Some(origin) => origin, + None => { + let rl = self.editor()?; + Self::read_line( + rl, + args, + Some("Insert the origin git repository of the snap template"), + Some("https://github.com/Sovereign-Labs/sov-snap"), + ) + } + }; + + let branch = match args.branch.as_ref().cloned() { + Some(branch) => branch, + None => { + let rl = self.editor()?; + Self::read_line( + rl, + args, + Some("Insert the branch of the snap template"), + Some("v0.1.1"), + ) + } + }; + + Command::new("git") + .arg("clone") + .arg("--quiet") + .arg("--progress") + .arg("-c") + .arg("advice.detachedHead=false") + .arg("--branch") + .arg(branch) + .arg("--single-branch") + .arg("--depth") + .arg("1") + .arg(origin) + .arg(target.as_ref()) + .output() + .map_err(Into::into) + } + + pub fn definitions(&mut self, args: &Cli, manifest: &Manifest) -> anyhow::Result { + let context = match args.context.as_ref().cloned() { + Some(context) => context, + None => { + let rl = self.editor()?; + let initial = format!("{}::Context", manifest.name_replaced); + Self::read_line( + rl, + args, + Some("Insert the path of the runtime context"), + Some(&initial), + ) + } + }; + + let da_spec = match args.da_spec.as_ref().cloned() { + Some(da_spec) => da_spec, + None => { + let rl = self.editor()?; + let initial = format!("{}::DaSpec", manifest.name_replaced); + Self::read_line( + rl, + args, + Some("Insert the path of the DA runtime spec"), + Some(&initial), + ) + } + }; + + let runtime = match args.runtime.as_ref().cloned() { + Some(runtime) => runtime, + None => { + let rl = self.editor()?; + let initial = format!("{}::RuntimeCall", manifest.name_replaced); + Self::read_line( + rl, + args, + Some("Insert the path of the runtime call"), + Some(&initial), + ) + } + }; + + Ok(Definitions { + context, + da_spec, + runtime, + }) + } +} + +#[derive(Helper, Validator, Highlighter)] +pub struct FilenameParser { + filename_completer: FilenameCompleter, +} + +impl Completer for FilenameParser { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + // TODO filename completion is not being called; rustyline bug? + if line.is_empty() { + // hints current working dir by default + let cwd = env::current_dir() + .expect("failed to read current working dir") + .display() + .to_string(); + + let pair = Pair { + display: cwd.clone(), + replacement: cwd, + }; + + return Ok((0, vec![pair])); + } + + self.filename_completer.complete_path(line, pos) + } +} + +impl Hinter for FilenameParser { + type Hint = String; + + fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option { + self.complete(line, pos, ctx) + .ok() + .and_then(|(_, pairs)| pairs.first().cloned()) + .map(|pair| pair.replacement[pos..].to_string()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..585accc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,148 @@ +use std::{ + fs, + io::{self, Write}, + path::Path, + process::Command, +}; + +use clap::Parser; + +mod args; +mod definitions; +mod interface; +mod manifest; + +use args::Cli; +use definitions::Definitions; +use interface::Interface; +use manifest::Dependencies; + +fn main() -> anyhow::Result<()> { + let args = Cli::parse(); + let mut interface = Interface::default(); + let manifest = interface.manifest(&args)?; + let target = interface.target_dir(&args, &manifest)?; + let definitions = interface.definitions(&args, &manifest)?; + + git_clone(&mut interface, &args, &target)?; + generate_wasm_project(&manifest, &target, &definitions)?; + generate_snap(&target)?; + + println!( + "Snap generated on `{}`", + target.join("packages").join("snap").display() + ); + + println!( + "To run the web development server, install Metamask Flask and run `yarn start` on `{}`", + target.display() + ); + + Ok(()) +} + +fn git_clone

(interface: &mut Interface, args: &Cli, target: P) -> anyhow::Result<()> +where + P: AsRef, +{ + let target = target.as_ref(); + println!("Cloning into directory `{}`...", target.display()); + + let output = interface.git_clone(&args, target)?; + io::stderr().write_all(&output.stderr)?; + io::stdout().write_all(&output.stdout)?; + if !output.status.success() { + anyhow::bail!("Git clone failed; did you forget to install git?"); + } + + Ok(()) +} + +fn generate_wasm_project

( + manifest: &manifest::Manifest, + target: P, + definitions: &Definitions, +) -> anyhow::Result<()> +where + P: AsRef, +{ + let target_dir = target.as_ref().join("external").join("sov-wasm"); + let target_manifest = target_dir.join("Cargo.toml"); + + println!("Writing manifest to `{}`...", target_manifest.display()); + + let mut output = r#"[package] +name = "sov-wasm" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +"# + .to_string(); + + if let Dependencies::Resolved { + base, + borsh, + serde_json, + sov_modules_api, + } = &manifest.dependencies + { + output.push_str(&format!("{} = {}\n", manifest.name, base)); + output.push_str(&format!("borsh = {}\n", borsh)); + output.push_str(&format!("serde_json = {}\n", serde_json)); + output.push_str(&format!("sov-modules-api = {}\n", sov_modules_api)); + } + + fs::write(target_manifest, output.as_bytes())?; + + println!("Writing definitions..."); + + let target_definitions = target_dir.join("src").join("definitions.rs"); + let mut output = String::new(); + output.push_str(&format!("pub type Context = {};\n", definitions.context)); + output.push_str(&format!("pub type DaSpec = {};\n", definitions.da_spec)); + output.push_str(&format!( + "pub type RuntimeCall = {};\n", + definitions.runtime + )); + + fs::write(target_definitions, output.as_bytes())?; + + Ok(()) +} + +fn generate_snap

(target: P) -> anyhow::Result<()> +where + P: AsRef, +{ + let target = target.as_ref(); + + println!("Installing yarn dependencies on `{}`...", target.display()); + + let output = Command::new("yarn") + .arg("install") + .current_dir(target) + .output()?; + io::stderr().write_all(&output.stderr)?; + io::stdout().write_all(&output.stdout)?; + if !output.status.success() { + anyhow::bail!("Yarn command failed; did you forget to install yarn?"); + } + + println!("Installing yarn WASM file on `{}`...", target.display()); + + let output = Command::new("yarn") + .arg("update-wasm") + .current_dir(target) + .output()?; + io::stderr().write_all(&output.stderr)?; + io::stdout().write_all(&output.stdout)?; + if !output.status.success() { + anyhow::bail!("Yarn command failed; did you forget to install cargo, binaryen, or wabt?"); + } + + Ok(()) +} diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..d3b759e --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,74 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use toml::Table; + +pub struct Manifest { + pub path: PathBuf, + pub parent: PathBuf, + pub name: String, + pub name_replaced: String, + pub dependencies: Dependencies, +} + +impl TryFrom<&Path> for Manifest { + type Error = anyhow::Error; + + fn try_from(path: &Path) -> Result { + let parent = path + .parent() + .ok_or_else(|| anyhow::Error::msg("No parent path for the manifest dir"))? + .to_path_buf(); + + let manifest = fs::read_to_string(path)?; + let manifest: Table = toml::from_str(&manifest)?; + + let name = manifest["package"]["name"] + .as_str() + .ok_or_else(|| anyhow::Error::msg("Invalid `package.name`"))? + .to_string(); + + let dependencies = Dependencies::from(&manifest); + let name_replaced = name.replace("-", "_"); + + Ok(Self { + parent, + path: path.to_path_buf(), + name, + name_replaced, + dependencies, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Dependencies { + Unresolved { + borsh: Option, + serde_json: Option, + sov_modules_api: Option, + }, + Resolved { + base: String, + borsh: String, + serde_json: String, + sov_modules_api: String, + }, +} + +impl From<&Table> for Dependencies { + fn from(manifest: &Table) -> Self { + let dependencies = &manifest["dependencies"]; + let borsh = dependencies.get("borsh").map(|d| d.to_string()); + let serde_json = dependencies.get("serde_json").map(|d| d.to_string()); + let sov_modules_api = dependencies.get("sov-modules-api").map(|d| d.to_string()); + + Self::Unresolved { + borsh, + serde_json, + sov_modules_api, + } + } +}