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..c49a22f 100644 --- a/README.md +++ b/README.md @@ -1 +1,140 @@ -# sov-snap-generator \ No newline at end of file +# sov-snap-generator + +This utility creates [Metamask Snaps](https://metamask.io/snaps/) for Sovereign SDK modules. + +## Requirements + +- [Git](https://git-scm.com/) +- [Rust](https://www.rust-lang.org/tools/install) +- [Yarn](https://yarnpkg.com/) +- [binaryen](https://github.com/WebAssembly/binaryen) +- [wabt](https://github.com/WebAssembly/wabt) +- [Metamask Flask](https://metamask.io/flask/) (optional for development environment) + +## 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 +``` + +Next, 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, fetch the default `constants.json` required for module compilation. + +```bash +wget https://raw.githubusercontent.com/Sovereign-Labs/sovereign-sdk/d42e289f26b9824b5ed54dbfbda94007dee305b2/constants.json +``` + +Finally, 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 defaults to searching for `Context`, `DaSpec`, and `RuntimeCall` definitions at the project root. However, these can be replaced with other paths. + +For a sanity check, run the following: + +```bash +cargo check +``` + +#### Generate the Snap + +Some prompts can be specified through CLI arguments. For more information, run: + +```bash +sov-snap-generator --help +``` + +To skip all prompts and checks, run: + +```bash +sov-snap-generator --defaults --force +``` + +To run the interactive mode, at the project root, execute: + +```bash +sov-snap-generator +``` + +The first prompt will inquire about the project `path`. 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 a dependency. It defaults to the parent directory of the resolved project manifest file. + +The generated WASM file requires certain dependencies. The subsequent prompts will default to the dependencies specified in the project manifest file, if present. It will then 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 adjacent to the current project, suffixed by `-snap`. + +The WASM file depends on specific definitions, typically customized by the module implementation. By default, it searches the root of the project for exports of `Context`, `DaSpec`, and `RuntimeCall`. The subsequent prompts will ask for these paths. If your implementation diverges from this standard, replace these items with their fully qualified paths. + +The template snap will be downloaded from a GitHub release. The next prompts inquire about the repository origin and its branch/tag. + +After the installation is executed, you should see a message indicating the project generated on ``. + +#### Run a local development environment + +Requirements: [Metamask Flask](https://metamask.io/flask/) + +To initiate a web development environment with your Snap, execute the following: + +```bash +cd ../sov-runtime-snap +yarn start +``` + +Your Snap will be accessible by default at http://localhost:8000. Click Connect/Reconnect to load the Snap into your Metamask Flask, enabling you to sign transactions. + +This development environment allows you 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 query 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 up a proxy to handle CORS requests, forwarding 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..cebfd6d --- /dev/null +++ b/src/interface.rs @@ -0,0 +1,402 @@ +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 { + if args.defaults { + anyhow::bail!( + "The `defaults` flag is set, but the target `{}` already exists. This operation is not permitted. To bypass this check for the `--defaults` arguments, combine it with `--force`.", + target.display() + ); + } + + 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, + } + } +}