diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..1892780 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,99 @@ +# blatantly lifted from https://github.com/init4tech/actions/blob/main/.github/workflows/rust-base.yml +name: Rust + +on: + push: + branches: [main] + pull_request: + +jobs: + unit-test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup ssh-agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - uses: Swatinem/rust-cache@v2 + + - name: Run Unit Tests + run: make test + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup ssh-agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + + - uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: make format-check + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup ssh-agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy + + - uses: Swatinem/rust-cache@v2 + + - name: Lint + run: make lint-check + + build: + name: Test Build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup ssh-agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: | + ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Initialize Git submodules + run: git submodule update --init --recursive + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - uses: Swatinem/rust-cache@v2 + + - name: Build + run: make build \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 390fdb8..78f2c77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,23 +46,28 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy-primitives" -version = "0.6.4" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600d34d8de81e23b6d909c094e23b3d357e01ca36b78a8c5424c501eedbe86f0" +checksum = "478bedf4d24e71ea48428d1bc278553bd7c6ae07c30ca063beb0b09fe58a9e74" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", "derive_more", - "hex-literal", + "foldhash", + "hashbrown 0.15.2", + "indexmap", "itoa", "k256", "keccak-asm", + "paste", "proptest", "rand", "ruint", + "rustc-hash", "serde", + "sha3", "tiny-keccak", ] @@ -323,9 +328,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -544,12 +549,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "core-foundation" version = "0.9.4" @@ -609,6 +608,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.3" @@ -735,15 +759,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version 0.4.1", "syn 2.0.90", + "unicode-xid", ] [[package]] @@ -767,6 +799,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -784,6 +837,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" + [[package]] name = "ecdsa" version = "0.16.9" @@ -945,6 +1004,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -982,6 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1016,6 +1082,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -1023,6 +1090,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1103,7 +1188,7 @@ dependencies = [ "gix-utils", "itoa", "thiserror", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -1156,7 +1241,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -1331,7 +1416,7 @@ dependencies = [ "itoa", "smallvec", "thiserror", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -1415,7 +1500,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -1559,25 +1644,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.7" @@ -1589,7 +1655,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.2.0", + "http", "indexmap", "slab", "tokio", @@ -1612,6 +1678,9 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -1625,12 +1694,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" - [[package]] name = "hmac" version = "0.12.1" @@ -1649,17 +1712,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.2.0" @@ -1671,17 +1723,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1689,7 +1730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http", ] [[package]] @@ -1700,8 +1741,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.2.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1719,59 +1760,56 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.32" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] -name = "hyper" -version = "1.5.2" +name = "hyper-rustls" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ - "bytes", - "futures-channel", "futures-util", - "h2 0.4.7", - "http 1.2.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", "tokio", + "tokio-rustls", + "tower-service", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.32", + "http-body-util", + "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -1781,12 +1819,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.5.2", + "http", + "http-body", + "hyper", "pin-project-lite", + "socket2", "tokio", + "tower-service", + "tracing", ] [[package]] @@ -1970,6 +2012,23 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2043,7 +2102,15 @@ dependencies = [ "elliptic-curve", "once_cell", "sha2", - "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", ] [[package]] @@ -2143,6 +2210,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.3" @@ -2164,10 +2243,10 @@ dependencies = [ "bytes", "colored", "futures-util", - "http 1.2.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.5.2", + "hyper", "hyper-util", "log", "rand", @@ -2195,6 +2274,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "ntapi" version = "0.4.1" @@ -2307,6 +2395,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parity-scale-codec" version = "3.6.12" @@ -2370,7 +2464,7 @@ dependencies = [ "clap", "eyre", "pcl-common", - "pcl-da", + "pcl-core", "pcl-phoundry", "tokio", "vergen-gix", @@ -2385,18 +2479,23 @@ dependencies = [ ] [[package]] -name = "pcl-da" +name = "pcl-core" version = "0.0.1" dependencies = [ "alloy-primitives", "clap", + "dirs", + "inquire", "mockito", "pcl-common", "pcl-phoundry", "reqwest", "serde", "serde_json", + "tempfile", "thiserror", + "tokio", + "toml", ] [[package]] @@ -2618,6 +2717,17 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.11.1" @@ -2649,20 +2759,24 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -2679,12 +2793,13 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] @@ -2697,6 +2812,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rlp" version = "0.5.2" @@ -2745,6 +2875,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -2782,13 +2918,43 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2935,6 +3101,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2964,6 +3139,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sha3-asm" version = "0.1.4" @@ -2990,6 +3175,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3040,6 +3236,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -3098,9 +3300,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3129,20 +3334,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3187,6 +3392,16 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.37" @@ -3263,7 +3478,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3293,6 +3508,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -3306,23 +3531,61 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.7.2", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3405,6 +3668,30 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -3659,7 +3946,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -3685,6 +3972,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3694,6 +3992,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3852,13 +4169,12 @@ dependencies = [ ] [[package]] -name = "winreg" -version = "0.50.0" +name = "winnow" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fa79c86..e32bae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "bin/pcl", "crates/phoundry", "crates/common", - "crates/da", + "crates/core" ] default-members = ["bin/pcl"] resolver = '2' @@ -22,7 +22,7 @@ repository = "https://github.com/phylaxsystems/pcl" [workspace.dependencies] pcl-phoundry = { path = "crates/phoundry" } pcl-common = { path = "crates/common" } -pcl-da = { path = "crates/da" } +pcl-core= { path = "crates/core" } tokio = { version = "1.39.0", features = ["full"] } clap = { version = "4.5.23", features = ["derive", "env"] } eyre = "0.6.12" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ae08991 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +# Build the binary +build: + cargo build --verbose --release + +# Build the contract mocks and run the rust tests +test: + cargo test --verbose + +# Validate formatting +format-check: + cargo fmt --check + +# Format +format: + cargo fmt --check + +# Errors if there is a warning with clippy +lint-check: + cargo clippy -- -D warnings + +# Can be used as a manual pre-commit check +pre-commit: + cargo fmt && make lint \ No newline at end of file diff --git a/README.md b/README.md index 73c3c01..2ccb599 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ The Credible CLI is a command-line interface for the Credible Layer. It requires the following: -- `just` - `Rust >= 1.86 nightly` - `git` @@ -17,7 +16,7 @@ After you have installed the above, you can build the CLI by running the followi ```bash git clone git@github.com:phylaxsystems/pcl.git cd pcl -just build-all +cargo build --release ``` This will build the CLI and install it in the `target/release` directory. diff --git a/bin/pcl/Cargo.toml b/bin/pcl/Cargo.toml index a7ab922..96fdca6 100644 --- a/bin/pcl/Cargo.toml +++ b/bin/pcl/Cargo.toml @@ -11,7 +11,7 @@ build = "build.rs" [dependencies] pcl-phoundry = { workspace = true } -pcl-da = { workspace = true } +pcl-core = { workspace = true } pcl-common = { workspace = true } clap = { workspace = true } diff --git a/bin/pcl/build.rs b/bin/pcl/build.rs index df5c23b..ce81e43 100644 --- a/bin/pcl/build.rs +++ b/bin/pcl/build.rs @@ -1,14 +1,72 @@ +use std::{env, fs, path::Path, process::Command}; + use anyhow::Result; use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder, RustcBuilder, SysinfoBuilder}; pub fn main() -> Result<()> { - println!("cargo:rerun-if-changed=build.rs"); - Emitter::default() .add_instructions(&BuildBuilder::all_build()?)? .add_instructions(&CargoBuilder::all_cargo()?)? .add_instructions(&GixBuilder::all_git()?)? .add_instructions(&RustcBuilder::all_rustc()?)? .add_instructions(&SysinfoBuilder::all_sysinfo()?)? - .emit() + .emit()?; + + let profile = env::var("PROFILE").unwrap(); + println!("cargo:warning=Building in {} mode", profile); + + // Get the workspace root directory (where Cargo.toml is located) + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let workspace_root = Path::new(&manifest_dir) + .parent() // up from bin/pcl + .unwrap() + .parent() // up to workspace root + .unwrap(); + + // Update phoundry submodule + update_phoundry(workspace_root).expect("Failed to update phoundry submodule"); + + // Build phoundry/forge + build_phoundry(workspace_root, &profile).expect("Failed to build phoundry"); + + // Copy the forge binary to the main target directory instead of OUT_DIR + let source = workspace_root + .join("phoundry") + .join("target") + .join(&profile) + .join("forge"); + + let dest = workspace_root.join("target").join(&profile).join("phorge"); + + println!( + "cargo:warning=Copying {} to {}", + source.display(), + dest.display() + ); + fs::copy(&source, &dest).expect("Failed to copy forge binary"); + + println!("cargo:rerun-if-changed={}", source.display()); + println!("cargo:rerun-if-changed=phoundry"); + Ok(()) +} + +fn update_phoundry(workspace_root: &Path) -> std::io::Result<()> { + Command::new("git") + .current_dir(workspace_root) + .args(["submodule", "update", "--init", "--recursive", "--remote"]) + .status()?; + Ok(()) +} + +fn build_phoundry(workspace_root: &Path, mode: &str) -> std::io::Result<()> { + let mut args = vec!["build", "--bin", "forge"]; + if mode == "release" { + args.push("--release"); + } + + Command::new("cargo") + .current_dir(workspace_root.join("phoundry")) + .args(&args) + .status()?; + Ok(()) } diff --git a/bin/pcl/src/main.rs b/bin/pcl/src/main.rs index ea8d3da..05729cd 100644 --- a/bin/pcl/src/main.rs +++ b/bin/pcl/src/main.rs @@ -1,8 +1,11 @@ use clap::{command, Parser}; -use eyre::Result; +use eyre::{Context, Result}; use pcl_common::args::CliArgs; -use pcl_da::submit::DASubmitArgs; -use pcl_phoundry::{build::BuildArgs, Phorge, PhoundryError}; +use pcl_core::{ + assertion_da::DASubmitArgs, assertion_submission::DappSubmitArgs, config::CliConfig, +}; +use pcl_phoundry::{build::BuildArgs, phorge::Phorge}; + const VERSION_MESSAGE: &str = concat!( env!("CARGO_PKG_VERSION"), "\nCommit: ", @@ -29,27 +32,32 @@ enum Commands { Phorge(Phorge), Build(BuildArgs), DASubmit(DASubmitArgs), + DappSubmit(DappSubmitArgs), } #[tokio::main] async fn main() -> Result<()> { // Check if forge is installed Phorge::forge_must_be_installed()?; + let mut config = CliConfig::read_or_default(); let cli = Cli::parse(); match cli.command { Commands::Phorge(phorge) => { - let _ = phorge.run(cli.args.clone(), true)?; - Ok::<(), PhoundryError>(()) + phorge.run(cli.args.clone(), true)?; } Commands::Build(build) => { build.run(cli.args.clone())?; - Ok::<(), PhoundryError>(()) } Commands::DASubmit(submit) => { - submit.run(cli.args.clone())?; - Ok::<(), PhoundryError>(()) + config.must_be_authenticated().wrap_err("Authentication required for DA submission. Please authenticate first using 'pcl auth'")?; + submit.run(cli.args.clone(), &mut config).await?; + } + Commands::DappSubmit(submit) => { + config.must_be_authenticated().wrap_err("Authentication required for dapp submission. Please authenticate first using 'pcl auth'")?; + submit.run(cli.args.clone(), &mut config).await?; } - }?; + }; + config.write_to_file()?; Ok(()) } diff --git a/crates/da/Cargo.toml b/crates/core/Cargo.toml similarity index 67% rename from crates/da/Cargo.toml rename to crates/core/Cargo.toml index 99a1bb4..861dec1 100644 --- a/crates/da/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "pcl-da" +name = "pcl-core" version.workspace = true authors.workspace = true edition.workspace = true @@ -14,13 +14,16 @@ pcl-common = { workspace = true } pcl-phoundry = { workspace = true } clap = { workspace = true } thiserror = { workspace = true } - serde = { workspace = true } serde_json = { workspace = true } +reqwest = { version = "0.12", features = ["json", "blocking"] } +alloy-primitives = "0.8.21" +inquire = "0.7.5" +toml = "0.8.2" +dirs = "6.0.0" -reqwest = { version = "0.11", features = ["json", "blocking"] } - -alloy-primitives = "0.6" [dev-dependencies] mockito = "1.2" +tempfile = "3.6.0" +tokio = { workspace = true } diff --git a/crates/da/src/submit.rs b/crates/core/src/assertion_da.rs similarity index 72% rename from crates/da/src/submit.rs rename to crates/core/src/assertion_da.rs index 49e6ae4..e6e2e1f 100644 --- a/crates/da/src/submit.rs +++ b/crates/core/src/assertion_da.rs @@ -1,16 +1,17 @@ +use crate::{config::CliConfig, error::DaSubmitError}; use alloy_primitives::keccak256; use pcl_common::{args::CliArgs, utils::bytecode}; use pcl_phoundry::build::BuildArgs; -use pcl_phoundry::PhoundryError; -use reqwest::{blocking::Client, Error as ReqwestError}; +use reqwest::Client; use serde::{Deserialize, Serialize}; -use thiserror::Error; #[derive(Deserialize)] struct JsonRpcResponse { - jsonrpc: String, + #[serde(rename = "jsonrpc")] + _json_rpc: String, result: SubmissionResponse, - id: u64, + #[serde(rename = "id")] + _id: u64, } #[derive(Deserialize)] @@ -21,7 +22,8 @@ struct SubmissionResponse { #[derive(Serialize)] struct JsonRpcRequest { - jsonrpc: String, + #[serde(rename = "jsonrpc")] + json_rpc: String, method: String, params: Vec, id: u64, @@ -29,15 +31,20 @@ struct JsonRpcRequest { #[derive(clap::Parser)] pub struct DASubmitArgs { + // FIXME(Odysseas): Replace localhost with the actual DA URL from our infrastructure /// URL of the assertion-DA - #[clap(long, env = "PCL_DA_URL")] + #[clap(long, env = "PCL_DA_URL", default_value = "http://localhost:3000")] url: String, /// Name of the assertion contract to submit assertion: String, } impl DASubmitArgs { - pub fn run(&self, cli_args: CliArgs) -> Result<(), SubmitError> { + pub async fn run( + &self, + cli_args: CliArgs, + _config: &mut CliConfig, + ) -> Result<(), DaSubmitError> { let build_args = BuildArgs { assertions: vec![self.assertion.clone()], }; @@ -46,15 +53,15 @@ impl DASubmitArgs { let bytecode = self.get_bytecode(&self.assertion)?; let id = self.calculate_id(&bytecode)?; let request = self.create_jsonrpc_request(&id, &bytecode)?; - self.submit_request(&request) + self.submit_request(&request).await } - fn get_bytecode(&self, assertion: &str) -> Result { + fn get_bytecode(&self, assertion: &str) -> Result { let artifact_path = format!("{}.sol:{}", assertion, assertion); Ok(bytecode(&artifact_path)) } - fn calculate_id(&self, bytecode: &str) -> Result { + fn calculate_id(&self, bytecode: &str) -> Result { // TODO: Need to align with the correct calculation of the id let id = keccak256(bytecode.as_bytes()); Ok(id.to_string()) @@ -64,9 +71,9 @@ impl DASubmitArgs { &self, id: &str, bytecode: &str, - ) -> Result { + ) -> Result { Ok(JsonRpcRequest { - jsonrpc: "2.0".to_string(), + json_rpc: "2.0".to_string(), method: "da_submit_assertion".to_string(), params: vec![ format!("0x{}", id), // keccak256 hash as id @@ -76,15 +83,17 @@ impl DASubmitArgs { }) } - fn submit_request(&self, request: &JsonRpcRequest) -> Result<(), SubmitError> { + async fn submit_request(&self, request: &JsonRpcRequest) -> Result<(), DaSubmitError> { let client = Client::new(); - let response = client.post(&self.url).json(request).send()?; + let response = client.post(&self.url).json(request).send().await?; if !response.status().is_success() { - return Err(SubmitError::SubmissionFailed(response.status().to_string())); + return Err(DaSubmitError::SubmissionFailed( + response.status().to_string(), + )); } - let result: JsonRpcResponse = response.json()?; + let result: JsonRpcResponse = response.json().await?; println!( "Submitted assertion '{}': ID {}: Status {}", self.assertion, result.result.id, result.result.status @@ -94,16 +103,6 @@ impl DASubmitArgs { } } -#[derive(Error, Debug)] -pub enum SubmitError { - #[error("HTTP request failed: {0}")] - RequestFailed(#[from] ReqwestError), - #[error("Submission failed: {0}")] - SubmissionFailed(String), - #[error("Build failed: {0}")] - BuildError(#[from] PhoundryError), -} - #[cfg(test)] mod tests { use super::*; @@ -129,16 +128,16 @@ mod tests { let request = args .create_jsonrpc_request("test_id", "test_bytecode") .unwrap(); - assert_eq!(request.jsonrpc, "2.0"); + assert_eq!(request.json_rpc, "2.0"); assert_eq!(request.method, "da_submit_assertion"); assert_eq!(request.params.len(), 2); assert_eq!(request.params[0], "0xtest_id"); assert_eq!(request.params[1], "0xtest_bytecode"); } - #[test] - fn test_submit_request() { - let mut server = Server::new(); + #[tokio::test] + async fn test_submit_request() { + let mut server = Server::new_async().await; let mock = server .mock("POST", "/") .match_body(r#"{"jsonrpc":"2.0","method":"da_submit_assertion","params":["0xtest_id","0xtest_bytecode"],"id":1}"#) @@ -155,14 +154,14 @@ mod tests { let request = args .create_jsonrpc_request("test_id", "test_bytecode") .unwrap(); - let result = args.submit_request(&request); + let result = args.submit_request(&request).await; assert!(result.is_ok()); mock.assert(); } - #[test] - fn test_submit_request_failure() { - let mut server = Server::new(); + #[tokio::test] + async fn test_submit_request_failure() { + let mut server = Server::new_async().await; let mock = server .mock("POST", "/") .with_status(400) @@ -179,8 +178,8 @@ mod tests { let request = args .create_jsonrpc_request("test_id", "test_bytecode") .unwrap(); - let result = args.submit_request(&request); - assert!(matches!(result, Err(SubmitError::SubmissionFailed(_)))); + let result = args.submit_request(&request).await; + assert!(matches!(result, Err(DaSubmitError::SubmissionFailed(_)))); mock.assert(); } } diff --git a/crates/core/src/assertion_submission.rs b/crates/core/src/assertion_submission.rs new file mode 100644 index 0000000..8bc3933 --- /dev/null +++ b/crates/core/src/assertion_submission.rs @@ -0,0 +1,241 @@ +use crate::{ + config::{AssertionForSubmission, CliConfig}, + error::DappSubmitError, +}; +use inquire::{MultiSelect, Select}; +use pcl_common::args::CliArgs; +use serde::Deserialize; +use serde_json::json; + +#[derive(Deserialize)] +struct Project { + _project_id: String, + project_name: String, + _project_description: Option, + _profile_image_url: Option, + _project_networks: Vec, + _project_manager: String, + _assertion_adopters: Vec, + _created_at: String, + _updated_at: String, +} + +/// Arguments for submitting assertions to the Credible Layer dApp +/// +/// This struct handles CLI arguments for the assertion submission process, +/// including the dApp URL, project name, and assertion names. +#[derive(clap::Parser)] +#[clap(about = "Submit assertions to the Credible Layer dApp")] +pub struct DappSubmitArgs { + /// Base URL for the Credible Layer dApp API + #[clap( + short, + long, + default_value = "https://credible-layer-dapp.pages.dev/api/v1" + )] + dapp_url: String, + + /// Optional project name to skip interactive selection + #[clap(short, long)] + project_name: Option, + + /// Optional list of assertion names to skip interactive selection + #[clap(short, long)] + assertion_name: Option>, +} + +impl DappSubmitArgs { + /// Executes the assertion submission workflow + /// + /// # Arguments + /// * `_cli_args` - General CLI arguments + /// * `config` - Configuration containing assertions and auth details + /// + /// # Returns + /// * `Result<(), DappSubmitError>` - Success or specific error + pub async fn run( + &self, + _cli_args: CliArgs, + config: &mut CliConfig, + ) -> Result<(), DappSubmitError> { + let projects = self.get_projects(config).await?; + let assertions_for_submission = config + .assertions_for_submission + .iter() + .map(|a| a.assertion_contract.clone()) + .collect(); + + let project_name = self.provide_or_select( + self.project_name.clone(), + projects.iter().map(|p| p.project_name.clone()).collect(), + "Select a project to submit the assertion to:".to_string(), + )?; + let project = projects + .iter() + .find(|p| p.project_name == project_name) + .unwrap(); + + let assertion_names = self.provide_or_multi_select( + self.assertion_name.clone(), + assertions_for_submission, + "Select an assertion to submit:".to_string(), + )?; + + let assertions: Vec<&AssertionForSubmission> = assertion_names + .iter() + .map(|n| { + config + .assertions_for_submission + .iter() + .find(|a| a.assertion_contract == *n) + .unwrap() + }) + .collect(); + + self.submit_assertion(project, &assertions).await?; + // TOOD: remove assertion from config + + Ok(()) + } + + /// Submits selected assertions to the specified project + /// + /// # Arguments + /// * `project` - Target project for submission + /// * `assertions` - List of assertions to submit + /// + /// # Returns + /// * `Result<(), DappSubmitError>` - Success or API error + async fn submit_assertion( + &self, + project: &Project, + assertions: &[&AssertionForSubmission], + ) -> Result<(), DappSubmitError> { + let client = reqwest::Client::new(); + // TODO: Update payload structure once API spec is finalized + let body = json!({ + "project_id": project._project_id, + "assertions": assertions.iter().map(|a| &a.assertion_contract).collect::>() + }); + + let response = client + .post(format!("{}/assertions", self.dapp_url)) + .json(&body) + .send() + .await?; + + if response.status().is_success() { + Ok(()) + } else { + Err(DappSubmitError::SubmissionFailed(response.text().await?)) + } + } + + /// Handles interactive or direct selection of a single value + /// + /// # Arguments + /// * `maybe_key` - Optional pre-selected value + /// * `values` - Available options + /// * `message` - Prompt message for interactive selection + /// + /// # Returns + /// * `Result` - Selected value or error + fn provide_or_select( + &self, + maybe_key: Option, + values: Vec, + message: String, + ) -> Result { + match maybe_key { + None => Select::new(message.as_str(), values) + .prompt() + .map_err(|_| DappSubmitError::ProjectSelectionCancelled), + Some(key) => { + let exists = values + .iter() + .any(|p| key.to_lowercase() == p.to_lowercase()); + if exists { + Ok(key.to_string()) + } else { + println!("{} does not exist", key); + let choice = Select::new(message.as_str(), values) + .prompt() + .map_err(|_| DappSubmitError::ProjectSelectionCancelled)?; + Ok(choice) + } + } + } + } + + /// Handles interactive or direct selection of multiple values + /// + /// # Arguments + /// * `maybe_keys` - Optional pre-selected values + /// * `values` - Available options + /// * `message` - Prompt message for interactive selection + /// + /// # Returns + /// * `Result, DappSubmitError>` - Selected values or error + fn provide_or_multi_select( + &self, + maybe_keys: Option>, + values: Vec, + message: String, + ) -> Result, DappSubmitError> { + match maybe_keys { + None => MultiSelect::new(message.as_str(), values) + .prompt() + .map_err(|_| DappSubmitError::ProjectSelectionCancelled), + Some(key) => { + let exists = key + .iter() + .all(|k| values.iter().any(|v| k.to_lowercase() == v.to_lowercase())); + if exists { + Ok(values) + } else { + println!("{} does not exist", key.join(", ")); + let choice = MultiSelect::new(message.as_str(), values) + .prompt() + .map_err(|_| DappSubmitError::ProjectSelectionCancelled)?; + Ok(choice) + } + } + } + } + async fn get_projects(&self, config: &mut CliConfig) -> Result, DappSubmitError> { + let client = reqwest::Client::new(); + let projects: Vec = client + .get(format!( + "{}/projects?user={}", + self.dapp_url, + config.auth.as_ref().unwrap().user_address + )) + .send() + .await? + .json() + .await?; + Ok(projects) + } +} + +/// TODO(ODYSSEAS): Add tests for the DappSubmitArgs struct +#[cfg(test)] +mod tests { + use crate::assertion_submission::DappSubmitArgs; + + #[test] + fn test_provide_or_select_with_valid_input() { + let args = DappSubmitArgs { + dapp_url: "".to_string(), + project_name: Some("Project1".to_string()), + assertion_name: None, + }; + + let values = vec!["Project1".to_string(), "Project2".to_string()]; + let result = + args.provide_or_select(Some("Project1".to_string()), values, "Select:".to_string()); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "Project1"); + } +} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs new file mode 100644 index 0000000..9dc8bea --- /dev/null +++ b/crates/core/src/config.rs @@ -0,0 +1,152 @@ +use crate::error::ConfigError; +use dirs::home_dir; +use serde::{Deserialize, Serialize}; + +pub const CONFIG_DIR: &str = ".pcl"; +pub const CONFIG_FILE: &str = "config.toml"; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct CliConfig { + pub auth: Option, + pub assertions_for_submission: Vec, +} + +impl CliConfig { + pub fn write_to_file(&self) -> Result<(), ConfigError> { + let config_dir = home_dir().unwrap().join(CONFIG_DIR); + std::fs::create_dir_all(&config_dir).map_err(ConfigError::WriteError)?; + let config_file = config_dir.join(CONFIG_FILE); + let config_str = toml::to_string(self).unwrap(); + std::fs::write(config_file, config_str).map_err(ConfigError::WriteError)?; + Ok(()) + } + + pub fn read_or_default() -> Self { + Self::read_from_file().unwrap_or_default() + } + + pub fn read_from_file() -> Result { + let config_dir = home_dir().unwrap().join(CONFIG_DIR); + let config_file = config_dir.join(CONFIG_FILE); + let config_str = std::fs::read_to_string(config_file).map_err(ConfigError::ReadError)?; + Ok(toml::from_str(&config_str).unwrap()) + } + + pub fn must_be_authenticated(&self) -> Result<(), ConfigError> { + if self.auth.is_none() { + return Err(ConfigError::NotAuthenticated); + } + Ok(()) + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct UserAuth { + pub access_token: String, + pub refresh_token: String, + pub user_address: String, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct AssertionForSubmission { + pub assertion_contract: String, + pub assertion_id: String, + pub signature: String, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + use tempfile::TempDir; + + // Helper function to set up a temporary config directory + fn setup_temp_config() -> TempDir { + let temp_dir = TempDir::new().unwrap(); + env::set_var("HOME", temp_dir.path()); + temp_dir + } + + #[test] + fn test_write_and_read_config() { + let temp_dir = setup_temp_config(); + + let config = CliConfig { + auth: Some(UserAuth { + access_token: "test_access".to_string(), + refresh_token: "test_refresh".to_string(), + user_address: "test_address".to_string(), + }), + assertions_for_submission: vec![AssertionForSubmission { + assertion_contract: "contract1".to_string(), + assertion_id: "id1".to_string(), + signature: "sig1".to_string(), + }], + }; + + // Test writing + assert!(config.write_to_file().is_ok()); + + // Test reading + let read_config = CliConfig::read_from_file().unwrap(); + assert_eq!( + read_config.auth.as_ref().unwrap().access_token, + "test_access" + ); + assert_eq!( + read_config.auth.as_ref().unwrap().refresh_token, + "test_refresh" + ); + assert_eq!( + read_config.auth.as_ref().unwrap().user_address, + "test_address" + ); + assert_eq!(read_config.assertions_for_submission.len(), 1); + assert_eq!( + read_config.assertions_for_submission[0].assertion_contract, + "contract1" + ); + + temp_dir.close().unwrap(); + } + + #[test] + fn test_read_nonexistent_config() { + let temp_dir = setup_temp_config(); + + // Try reading without creating a file + let result = CliConfig::read_from_file(); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ConfigError::ReadError(_))); + + temp_dir.close().unwrap(); + } + + #[test] + fn test_read_or_default() { + let temp_dir = setup_temp_config(); + + // Should return default when no file exists + let config = CliConfig::read_or_default(); + assert!(config.auth.is_none()); + assert!(config.assertions_for_submission.is_empty()); + + temp_dir.close().unwrap(); + } + + #[test] + fn test_authentication_check() { + let config = CliConfig::default(); + assert!(config.must_be_authenticated().is_err()); + + let config = CliConfig { + auth: Some(UserAuth { + access_token: "test".to_string(), + refresh_token: "test".to_string(), + user_address: "test".to_string(), + }), + assertions_for_submission: vec![], + }; + assert!(config.must_be_authenticated().is_ok()); + } +} diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs new file mode 100644 index 0000000..d251959 --- /dev/null +++ b/crates/core/src/error.rs @@ -0,0 +1,56 @@ +use pcl_phoundry::error::PhoundryError; +use reqwest::Error as ReqwestError; +use thiserror::Error; + +/// Errors that can occur during assertion submission to the Data Availability (DA) layer +#[derive(Error, Debug)] +pub enum DaSubmitError { + /// Error when HTTP request to the DA layer fails + #[error("HTTP request failed: {0}")] + RequestFailed(#[from] ReqwestError), + + /// Error when the submission is rejected by the DA layer + #[error("Submission failed: {0}")] + SubmissionFailed(String), + + /// Error during the build process of the assertion + #[error("Build failed: {0}")] + BuildError(#[from] PhoundryError), +} + +/// Errors that can occur during assertion submission to the Credible Layer dApp +#[derive(Error, Debug)] +pub enum DappSubmitError { + /// Error when no authentication token is found in the config + #[error("No auth token found")] + NoAuthToken, + + /// Error when user cancels the project selection process + #[error("Project selection cancelled")] + ProjectSelectionCancelled, + + /// Error when connection to the dApp API fails + #[error("Failed to connect to the dApp API")] + ApiConnectionError(#[from] ReqwestError), + + /// Error when the submission is rejected by the dApp + #[error("Submission failed: {0}")] + SubmissionFailed(String), +} + +/// Errors that can occur during configuration operations +#[derive(Error, Debug)] +pub enum ConfigError { + /// Error when reading the config file from ~/.pcl/config.toml fails + #[error("Failed to read config file: {0}")] + ReadError(std::io::Error), + + /// Error when writing to the config file at ~/.pcl/config.toml fails + #[error("Failed to write config file: {0}")] + WriteError(std::io::Error), + + /// Error when attempting an operation that requires authentication + /// but no authentication token is present in the config + #[error("No Authentication Token Found")] + NotAuthenticated, +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 0000000..fa6a322 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,4 @@ +pub mod assertion_da; +pub mod assertion_submission; +pub mod config; +pub mod error; diff --git a/crates/da/src/lib.rs b/crates/da/src/lib.rs deleted file mode 100644 index d53a5d1..0000000 --- a/crates/da/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod submit; diff --git a/crates/phoundry/src/build.rs b/crates/phoundry/src/build.rs index ba72ef0..8897d46 100644 --- a/crates/phoundry/src/build.rs +++ b/crates/phoundry/src/build.rs @@ -1,7 +1,7 @@ use clap::Parser; use pcl_common::args::CliArgs; -use crate::{Phorge, PhoundryError}; +use crate::{error::PhoundryError, phorge::Phorge}; #[derive(Debug)] pub struct AssertionBuildOutput { diff --git a/crates/phoundry/src/error.rs b/crates/phoundry/src/error.rs new file mode 100644 index 0000000..1434fed --- /dev/null +++ b/crates/phoundry/src/error.rs @@ -0,0 +1,12 @@ +use std::fmt::Debug; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PhoundryError { + #[error("forge is not installed or not available in PATH")] + ForgeNotInstalled, + #[error("forge command failed")] + ForgeCommandFailed(#[from] std::io::Error), + #[error("invalid forge output")] + InvalidForgeOutput(&'static str), +} diff --git a/crates/phoundry/src/lib.rs b/crates/phoundry/src/lib.rs index 8f0e5e3..67c91ee 100644 --- a/crates/phoundry/src/lib.rs +++ b/crates/phoundry/src/lib.rs @@ -1,84 +1,3 @@ -use std::{ - env, - path::PathBuf, - process::{Command, Output}, -}; - -use pcl_common::args::CliArgs; -use thiserror::Error; - pub mod build; - -const FORGE_BINARY_NAME: &str = "phorge"; - -// Remove the const and add a function to get the forge binary path -fn get_forge_binary_path() -> PathBuf { - let exe_path = env::current_exe().expect("Failed to get current executable path"); - exe_path - .parent() - .expect("Failed to get executable directory") - .join(FORGE_BINARY_NAME) -} - -#[derive(clap::Parser)] -pub struct Phorge { - pub args: Vec, -} - -impl Phorge { - /// Run the forge command with the given arguments. - /// Phoundry should be installed as part of the pcl workspace, meaning that we - /// can assume that forge is available in the PATH. - /// We do this so that we don't have to re-write the forge command in the CLI, as - /// a lot of the functionality is implemented as part of the forge binary, which we can't import - /// as a crate. - pub fn run(&self, cli_args: CliArgs, print_output: bool) -> Result { - // Execute forge and pass through all output exactly as-is - let mut command = Command::new(get_forge_binary_path()); - - command.args(self.args.clone()); - - // Only valid for the context of this binary execution - env::set_var( - "FOUNDRY_SRC", - cli_args.assertions_src().as_os_str().to_str().unwrap(), - ); - env::set_var( - "FOUNDRY_TEST", - cli_args.assertions_test().as_os_str().to_str().unwrap(), - ); - - let output = command.output()?; - - // Pass through stdout/stderr exactly as forge produced them - if print_output && !output.stdout.is_empty() { - print!("{}", String::from_utf8_lossy(&output.stdout)); - } - if !output.stderr.is_empty() { - eprint!("{}", String::from_utf8_lossy(&output.stderr)); - } - Ok(output) - } - - /// Check if forge is installed and available in the PATH. - pub fn forge_must_be_installed() -> Result<(), PhoundryError> { - if Command::new(get_forge_binary_path()) - .arg("--version") - .output() - .is_err() - { - return Err(PhoundryError::ForgeNotInstalled); - } - Ok(()) - } -} - -#[derive(Error, Debug)] -pub enum PhoundryError { - #[error("forge is not installed or not available in PATH")] - ForgeNotInstalled, - #[error("forge command failed")] - ForgeCommandFailed(#[from] std::io::Error), - #[error("invalid forge output")] - InvalidForgeOutput(&'static str), -} +pub mod error; +pub mod phorge; diff --git a/crates/phoundry/src/phorge.rs b/crates/phoundry/src/phorge.rs new file mode 100644 index 0000000..8f36cea --- /dev/null +++ b/crates/phoundry/src/phorge.rs @@ -0,0 +1,73 @@ +use pcl_common::args::CliArgs; +use std::{ + env, + path::PathBuf, + process::{Command, Output}, +}; + +use crate::error::PhoundryError; + +const FORGE_BINARY_NAME: &str = "phorge"; + +// Remove the const and add a function to get the forge binary path +fn get_forge_binary_path() -> PathBuf { + let exe_path = env::current_exe().expect("Failed to get current executable path"); + exe_path + .parent() + .expect("Failed to get executable directory") + .join(FORGE_BINARY_NAME) +} + +#[derive(clap::Parser)] +pub struct Phorge { + pub args: Vec, +} + +impl Phorge { + /// Run the forge command with the given arguments. + /// Phoundry should be installed as part of the pcl workspace, meaning that we + /// can assume that forge is available in the PATH. + /// We do this so that we don't have to re-write the forge command in the CLI, as + /// a lot of the functionality is implemented as part of the forge binary, which we can't import + /// as a crate. + pub fn run(&self, cli_args: CliArgs, print_output: bool) -> Result { + // Execute forge and pass through all output exactly as-is + let mut command = Command::new(get_forge_binary_path()); + + command.args(self.args.clone()); + + // Only valid for the context of this binary execution + env::set_var( + "FOUNDRY_SRC", + cli_args.assertions_src().as_os_str().to_str().unwrap(), + ); + env::set_var( + "FOUNDRY_TEST", + cli_args.assertions_test().as_os_str().to_str().unwrap(), + ); + + let output = command.output()?; + + // Pass through stdout/stderr exactly as forge produced them + if print_output && !output.stdout.is_empty() { + print!("{}", String::from_utf8_lossy(&output.stdout)); + } + if !output.stderr.is_empty() { + eprint!("{}", String::from_utf8_lossy(&output.stderr)); + } + + Ok(output) + } + + /// Check if forge is installed and available in the PATH. + pub fn forge_must_be_installed() -> Result<(), PhoundryError> { + if Command::new(get_forge_binary_path()) + .arg("--version") + .output() + .is_err() + { + return Err(PhoundryError::ForgeNotInstalled); + } + Ok(()) + } +} diff --git a/justfile b/justfile deleted file mode 100644 index d438bed..0000000 --- a/justfile +++ /dev/null @@ -1,25 +0,0 @@ -phoundry-repo := "https://github.com/phylaxsystems/phoundry" -phoundry-dir := "phoundry" - -cargo-mode mode="release": - if [ {{mode}} == "release" ]; then echo "--release"; else echo ""; fi - -setup-phoundry mode="release": - #!/usr/bin/env sh - cd {{phoundry-dir}} && cargo build --bin forge `just cargo-mode {{mode}}` - -build-all skip_update="false" mode="release": - cargo build `just cargo-mode {{mode}}` - if [ "{{skip_update}}" = "false" ]; then just update-phoundry; fi - just setup-phoundry {{mode}} - just place-phoundry-bin {{mode}} - -update-phoundry: - git submodule update --init --recursive --remote - cd {{phoundry-dir}} - -place-phoundry-bin mode="release": - cp {{phoundry-dir}}/target/{{mode}}/forge target/{{mode}}/phorge - -test: - cargo test --workspace